From c5906c2dc1cf7ffc33cc4f89ad2d2b0806e8d7a1 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:23:35 -0500 Subject: [PATCH 01/16] =?UTF-8?q?#4329=E2=80=94Major=20Terminal.Gui=20v2?= =?UTF-8?q?=20Architecture=20Modernization:=20Application=20Decoupling,=20?= =?UTF-8?q?Terminology=20Improvements,=20and=20Nullable=20Migration=20(#43?= =?UTF-8?q?38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * Add comprehensive terminology proposal for Application.Top/Toplevel renaming Co-authored-by: tig <585482+tig@users.noreply.github.com> * Add terminology proposal summary document Co-authored-by: tig <585482+tig@users.noreply.github.com> * Add before/after comparison document for terminology proposal Co-authored-by: tig <585482+tig@users.noreply.github.com> * Add index document for terminology proposal Co-authored-by: tig <585482+tig@users.noreply.github.com> * Add Mermaid diagrams visualizing the terminology proposal Co-authored-by: tig <585482+tig@users.noreply.github.com> * Move terminology docs to docfx/docs per CONTRIBUTING.md guidelines Co-authored-by: tig <585482+tig@users.noreply.github.com> * Update terminology proposal to reflect recent modernization (post-MainLoop removal) Co-authored-by: tig <585482+tig@users.noreply.github.com> * Update terminology proposal - validate against recent driver refactoring changes Co-authored-by: tig <585482+tig@users.noreply.github.com> * Update proposal after massive v2_develop modernization - TopLevels now public, RunState→SessionToken Co-authored-by: tig <585482+tig@users.noreply.github.com> * Update proposal: RunStack → SessionStack to align with SessionToken terminology Co-authored-by: tig <585482+tig@users.noreply.github.com> * Phase 1: Core API rename - Application.Top→Current, TopLevels→SessionStack Co-authored-by: tig <585482+tig@users.noreply.github.com> * Phase 2: Fix test compilation errors for renamed properties Co-authored-by: tig <585482+tig@users.noreply.github.com> * Phase 3: Update documentation files with new terminology Co-authored-by: tig <585482+tig@users.noreply.github.com> * Refactor generic type names and remove unused field Renamed generic type parameters in `Dim` and `Pos` classes for clarity: - `T` was renamed to `TDim` in `Dim.Has` method. - `T` was renamed to `TPos` in `Pos.Has` method. Updated type casting and pattern matching logic to reflect these changes. Removed the unused `_stopAfterFirstIteration` field from the `ApplicationImpl` class to clean up the codebase. * Increase minimum code coverage target to 75% Updated the `codecov.yml` configuration file to raise the `project.default.target` value from 70% to 75%, enforcing stricter code coverage requirements for the overall project. * Add comprehensive unit tests for ApplicationImpl Begin/End logic Added ApplicationImplBeginEndTests with 16 tests covering: - Begin/End argument validation - SessionStack push/pop operations - Current property management - Balanced Begin/End enforcement - Multiple nested Begin/End scenarios - ResetState cleanup behavior - Toplevel activation/deactivation - SessionToken management Tests validate the fragile state management logic in ApplicationImpl.Lifecycle.cs and ApplicationImpl.Run.cs to catch regressions in Current/SessionStack handling. Co-authored-by: tig <585482+tig@users.noreply.github.com> * Refactor ApplicationImplBeginEndTests to work with ApplicationImpl instances directly Tests now: - Create ApplicationImpl instances directly instead of using global Application - Use try-finally blocks to ensure Shutdown() is always called - Work with app.Init() and app.Shutdown() properly - 13 of 16 tests passing (3 tests need adjustment for ResetState behavior) Tests validate fragile Begin/End state management logic without polluting global Application state. Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fix ApplicationImplBeginEndTests: Remove explicit ResetState calls, rely on Shutdown Per maintainer feedback: - Tests should not call ResetState explicitly if they call Shutdown (Shutdown calls ResetState) - Removed all explicit app.ResetState() calls from tests - Tests now only call Shutdown in finally blocks for cleanup - Ensured toplevels are disposed BEFORE Shutdown to satisfy DEBUG_IDISPOSABLE assertions - Fixed test cleanup order to prevent ResetState assertion failures - All 16 tests now pass successfully ResetState is idempotent - the issue was tests calling it unnecessarily and with incorrect disposal ordering. Co-authored-by: tig <585482+tig@users.noreply.github.com> * Phase 1: Enable nullable by default, add directives to all files - Added enable to Terminal.Gui.csproj - Removed #nullable enable from 37 files (now redundant with project setting) - Added #nullable disable to 170 files that don't have nullable enabled yet - Build successful with 279 nullable warnings (expected) Breakdown of files with #nullable disable: - 27 in Views/ directory (will remain disabled per plan) - 143 outside Views/ (need to be fixed in subsequent commits) Next: Systematically fix nullable issues in non-View files. Co-authored-by: tig <585482+tig@users.noreply.github.com> * Phase 2: Remove nullable disable from all non-View files - COMPLETE! - Removed #nullable disable from 142 non-View files - All non-View code is now nullable-enabled - Build successful with 0 errors, only 1 unrelated warning - Created NULLABLE_VIEWS_REMAINING.md documenting the 121 View files still with nullable disable Nullable cleanup summary: ✅ App/: 25 files - nullable enabled ✅ Configuration/: 24 files - nullable enabled ✅ ViewBase/: 30 files - nullable enabled ✅ Drivers/: 25 files - nullable enabled ✅ Drawing/: 18 files - nullable enabled ✅ FileServices/: 7 files - nullable enabled ✅ Input/: 6 files - nullable enabled ✅ Text/: 5 files - nullable enabled ✅ Resources/: 3 files - nullable enabled ⏸️ Views/: 121 files - remain disabled (as requested) Total: 143 files nullable-enabled, 121 View files remain disabled. Co-authored-by: tig <585482+tig@users.noreply.github.com> * WIP: fixing nullability issues. * Fixed final nullability issues. * Moved Arrangment tests * Refactor and improve modularity across multiple classes Refactored `FillRectangles` in `RegionScenario` and `Region` to accept an `IDriver` parameter, reducing reliance on global state. Updated `ApplicationImpl` to replace static method calls with instance methods for better encapsulation. Renamed `Toplevel` to `Current` in `IPopover` and related classes for clarity. Simplified `LineCanvas` methods by removing unnecessary `IDriver` parameters. Added `Id` and `App` properties to `View` for better state management and unique identification. Streamlined the `Driver` property in `View` with a concise getter. Improved formatting and consistency across files, including `Region` and `IntersectionRuneResolver`. Enhanced thread safety in `Region` and cleaned up redundant code. Updated tests to align with interface changes and ensure compatibility. * Refactor to make IDriver dependency explicit Updated `AnsiEscapeSequenceRequest.Send` to accept an `IDriver?` parameter, replacing reliance on `Application.Driver`. Refactored `AnsiRequestScheduler` methods (`SendOrSchedule`, `RunSchedule`, and private `Send`) to propagate the `IDriver?` parameter, ensuring explicit driver dependency. Modified `DriverImpl.QueueAnsiRequest` to pass `this` to `SendOrSchedule`. Updated `AnsiRequestSchedulerTests` to reflect new method signatures, passing `null` for the driver parameter where applicable. Added `` documentation for new parameters to improve clarity. These changes enhance flexibility, maintainability, and testability by reducing reliance on global state and allowing driver substitution in tests. * WIP: Started migrating to View.App Refactored `ApplicationImpl` to ensure proper handling of the `App` property for `Toplevel` instances, improving modularity. Replaced direct references to `Application` with `App` in `Border`, `ShadowView`, and other classes to enhance flexibility and maintainability. Introduced `GetApp` in `View` to allow overrides for retrieving the `App` instance. Updated `Adornment` to use this method. Moved mouse event subscriptions in `Border` to `BeginInit` for proper lifecycle management. Updated unit tests in `ArrangementTests` to use `App.Mouse` instead of `Application.Mouse`, ensuring alignment with the refactored design. Added `BeginInit` and `EndInit` calls for proper initialization during tests. Removed redundant code and improved test assertions. * WIP: Next set of View.App changes Updated `SetClipToScreen`, `SetClip`, and `GetClip` methods to accept an `IDriver` parameter, replacing reliance on the global `Application.Driver`. This improves modularity, testability, and reduces implicit global state usage. - Updated `Driver` property in `View` to use `App?.Driver` as fallback. - Refactored `DimAuto` to use `App?.Screen.Size` with a default for unit tests. - Updated all test cases to align with the new method signatures. - Performed general cleanup for consistency and readability. * Adds View clip tests. * Merged * Merged * wip * Fixed test bug. * Refactored Thickness.Draw to require driver. * Made TextFormatter.Draw require driver. * Code cleanup. * Un did stoopid idea. * Decouped Application.Navigation * MASSIVE - Almost completely decoupled Application from View etc... * Obsolete * Missed some * More cleanup and decoupling. Refactor `ToString` and remove legacy code Refactored `ToString` implementations across `Application`, `DriverImpl`, and `IDriver` to improve consistency and maintainability. Removed the legacy `ToString(IDriver? driver)` method and its associated references. Simplified `ToString` in `DriverImpl` to generate a string representation of the `Contents` buffer. Replaced redundant XML documentation with `` tags to reduce duplication. Cleaned up unused `global using` directives and removed deprecated methods and properties, including `Screen`, `SetCursorVisibility`, and `IsRuneSupported`. Updated test cases in `GuiTestContext` and `DriverAssert` to use the new `ToString` implementation. Improved error messages for better debugging output. Streamlined LINQ queries and removed redundant checks for better readability and performance. Enhanced maintainability by decluttering the codebase, aligning namespaces, and consolidating related changes. * Changes before error encountered Co-authored-by: tig <585482+tig@users.noreply.github.com> * Update docfx/docs to document View.App architecture and instance-based patterns Updated 16 documentation files to reflect the major architectural changes: NEW FILES: - application.md: Comprehensive deep dive on decoupled Application architecture UPDATED FILES: - View.md: Documents View.App property, GetApp(), and instance-based patterns - navigation.md: Shows View.App usage instead of static Application - drivers.md: Documents View.Driver and GetDriver() patterns - keyboard.md: Event handling through View.App - mouse.md: Mouse event handling via View.App - arrangement.md: Updated code examples to use View.App - drawing.md: Rendering examples with instance-based API - cursor.md: Cursor management through View.App - multitasking.md: SessionStack and session management via View.App - Popovers.md: Popover patterns with View.App - cancellable-work-pattern.md: Updated examples - command.md: Command pattern with View.App context - config.md: Configuration access through View.App - migratingfromv1.md: Migration guide for static→instance patterns - newinv2.md: Documents new instance-based architecture All code examples now demonstrate the instance-based API (view.App.Current) instead of obsolete static Application references. Documentation accurately reflects the massive architectural decoupling achieved in this PR. Co-authored-by: tig <585482+tig@users.noreply.github.com> * Add `ToAnsi` support for ANSI escape sequence generation Introduced `ToAnsi` in `IDriver` and `IOutput` interfaces to generate ANSI escape sequences representing the terminal's current state. This enables serialization of terminal content for debugging, testing, and exporting. Implemented `ToAnsi` in `DriverImpl` and `FakeOutput`, supporting both 16-color and RGB modes. Refactored `OutputBase` with helper methods `BuildAnsiForRegion` and `AppendCellAnsi` for efficient ANSI generation. Enhanced `GuiTestContext` with `AnsiScreenShot` for capturing terminal state during tests. Added `ToAnsiTests` for comprehensive validation, including edge cases, performance, and wide/Unicode character handling. Updated documentation to reflect `ToAnsi` functionality and modernized driver architecture. Improved testability, modularity, and performance while removing legacy driver references. * Improve null safety and cleanup in GuiTestContext Enhanced null safety across `GuiTestContext` and `GuiTestContextTests`: - Replaced `a` with `app` for better readability in tests. - Added null checks (`!`, `?.`) to prevent potential null reference exceptions. - Removed redundant `WaitIteration` and duplicate `ScreenShot` calls. Improved error handling and robustness: - Updated shutdown logic to use null-safe calls for `RequestStop` and `Shutdown`. - Applied null-safe invocation for `_applicationImpl.Invoke`. General cleanup: - Removed redundant method calls and improved naming consistency. - Ensured better maintainability and adherence to best practices. * Refactor docs: remove deprecated files, update architecture Removed outdated documentation files related to the terminology proposal (`terminology-before-after.md`, `terminology-diagrams.md`, `terminology-index.md`, `terminology-proposal-summary.md`, `terminology-proposal.md`) from the `Docs` project. These files were either deprecated or consolidated into other documentation. Updated `application.md`: - Added a "View Hierarchy and Run Stack" section with a Mermaid diagram to illustrate the relationship between the view hierarchy and the application session stack. - Added a "Usage Example Flow" section with a sequence diagram to demonstrate the flow of running and stopping views. These changes improve clarity, streamline documentation, and align with the finalized terminology updates for the `Application.Current` and `Application.SessionStack` APIs. * Refactor Init/Run methods to simplify driver handling The `Init` method in `Application` and `IApplication` now accepts only an optional `driverName` parameter, removing the `IDriver` parameter. This simplifies initialization by relying on driver names to determine the appropriate driver. The `Run` methods have been updated to use `driverName` instead of `driver`, ensuring consistency with the updated `Init` method. Replaced redundant inline documentation with `` tags to improve maintainability and consistency. Legacy `Application` methods (`Init`, `Shutdown`, `Run`) have been marked as `[Obsolete]` to signal their eventual deprecation. Test cases have been refactored to align with the updated `Init` method signature, removing unused `driver` parameters. Documentation files have also been updated to reflect these API changes. These changes improve clarity, reduce complexity, and ensure a more consistent API design. * Refactor: Introduce Application.Create() factory method Introduced a new static method `Application.Create()` to create instances of `IApplication`, replacing direct instantiation of `ApplicationImpl`. This enforces a cleaner, recommended pattern for creating application instances. Made the `ApplicationImpl` constructor `internal` to ensure `Application.Create()` is used for instance creation. Refactored test cases across multiple files to use `Application.Create()` instead of directly instantiating `ApplicationImpl`. Simplified object initialization in tests using target-typed `new()` expressions. Updated documentation and examples in `application.md` to reflect the new instance-based architecture and highlight its benefits, such as supporting multiple applications with different drivers. Improved code readability, formatting, and consistency in tests and documentation. Aligned `ApplicationImplBeginEndTests` to use `IApplication` directly, adhering to the new architecture. * Added `Application.StopAll` and fixed coupling issues. Refactored `ApplicationImpl` to use an instance-based approach, replacing the static singleton pattern and Lazy. Introduced `SetInstance` for configuring the singleton instance and updated tests to use `ApplicationImpl.Instance` or explicitly set the `Driver` property. Enabled nullable reference types across the codebase, updating fields and variables to nullable types where applicable. Added null checks to improve safety and prevent runtime errors. Refactored timeout management by introducing tokens for `Application.AddTimeout` and adding a `StopAll` method to `TimedEvents` for cleanup. Updated tests to use `System.Threading.Timer` for independent watchdog timers. Removed legacy code, improved logging for error cases, and updated view initialization to explicitly set `App` or `Driver` in tests. Enhanced test coverage and restructured `ScrollSliderTests` for better readability. Performed general code cleanup, including formatting changes, removal of unused imports, and improved naming consistency. * Refactor: Transition to IApplication interface Refactored the codebase to replace the static `Application` class with the `IApplication` interface, improving modularity, testability, and maintainability. Updated methods like `Application.Run`, `RequestStop`, and `Init` to use the new interface. Marked static members `SessionStack` and `Current` as `[Obsolete]` and delegated their functionality to `ApplicationImpl.Instance`. Updated XML documentation to reflect these changes. Simplified code by removing redundant comments, unused code, and converting methods like `GetMarginThickness` to single-line expressions. Improved null safety with null-conditional operators in `ToplevelTransitionManager`. Enhanced consistency with formatting updates, logging improvements, and better error handling. Updated `Shortcut` and other classes to align with the new interface-based design. Made breaking changes, including the removal of the `helpText` parameter in the `Shortcut` constructor. Updated `Wizard`, `Dialog`, and `GraphView` to use `IApplication` methods. Adjusted `ViewportSettings` and `HighlightStates` for better behavior. * Enhance null-safety and simplify codebase Improved null-safety by adopting nullable reference types and adding null-forgiving operators (`!`) where appropriate. Replaced direct method calls with null-safe calls using the null-conditional operator (`?.`) to prevent potential `NullReferenceException`. Removed default parameter values in test methods to enforce explicit parameter passing. Refactored test classes to remove unnecessary dependencies on `ITestOutputHelper`. Fixed a bug in `WindowsOutput.cs` by setting `_force16Colors` to `false` to avoid reliance on a problematic driver property. Updated `SessionTokenTests` to use null-forgiving operators for clarity in intentional null usage. Simplified graph and UI updates by ensuring safe access to properties and methods. Cleaned up namespaces and removed unused `using` directives for better readability. Updated `Dispose` methods to use null-safe calls and replaced nullable driver initialization with non-nullable initialization in `ScrollSliderTests` to ensure proper instantiation. * Refactor test code to use nullable `App` property Replaced direct `Application` references with `App` property across test classes to improve encapsulation and robustness. Updated `GuiTestContext` to use a nullable `App` property, replacing `_applicationImpl` for consistency. Refactored key event handling to use `App.Driver` and revised `InitializeApplication` and `CleanupApplication` methods to ensure safe usage of the nullable `App` property. Updated `Then` callbacks to explicitly pass `App` for clarity. Replaced `Application.QuitKey` with `context.App?.Keyboard.RaiseKeyDownEvent` to ensure context-specific event handling. Refactored `EnableForDesign` logic in `MenuBarv2Tests` and `PopoverMenuTests` to operate on the correct application instance. Improved null safety in test assertions and revised `RequestStop` and `Shutdown` calls to use `App?.RequestStop` and `App?.Shutdown`. Updated navigation logic to use `Terminal.Gui.App.Application` for namespace consistency. Enhanced exception handling in the `Invoke` method and performed general cleanup to align with modern C# practices, improving maintainability and readability. * Commented out exception handling in Application.Shutdown The `try-catch` block around `Application.Shutdown` was commented out, disabling the logging of exceptions thrown after a test exited. This change removes the `catch` block that used `Debug.WriteLine` for logging. The `finally` block remains intact, ensuring cleanup operations such as clearing `View.Instances` and resetting the application state are still executed. * Fixes #4394 - Changing Theme at Runtime does not Update Some Properties * Tweaks to config format. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Tig Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Examples/CommunityToolkitExample/Program.cs | 2 +- Examples/ReactiveExample/Program.cs | 2 +- Examples/ReactiveExample/TerminalScheduler.cs | 2 +- Examples/UICatalog/Resources/config.json | 5 +- Examples/UICatalog/Scenario.cs | 4 +- .../UICatalog/Scenarios/AllViewsTester.cs | 2 +- .../AnimationScenario/AnimationScenario.cs | 2 +- .../Scenarios/AnsiRequestsScenario.cs | 115 +-- Examples/UICatalog/Scenarios/Bars.cs | 16 +- .../UICatalog/Scenarios/CombiningMarks.cs | 6 +- .../Scenarios/ConfigurationEditor.cs | 2 +- Examples/UICatalog/Scenarios/ContextMenus.cs | 120 +-- Examples/UICatalog/Scenarios/CsvEditor.cs | 2 +- Examples/UICatalog/Scenarios/Images.cs | 2 +- Examples/UICatalog/Scenarios/Mazing.cs | 6 +- Examples/UICatalog/Scenarios/Menus.cs | 2 +- Examples/UICatalog/Scenarios/Navigation.cs | 2 +- Examples/UICatalog/Scenarios/Notepad.cs | 2 +- Examples/UICatalog/Scenarios/Progress.cs | 2 +- .../UICatalog/Scenarios/RegionScenario.cs | 38 +- Examples/UICatalog/Scenarios/Shortcuts.cs | 54 +- .../Scenarios/SingleBackgroundWorker.cs | 4 +- Examples/UICatalog/Scenarios/TableEditor.cs | 2 +- Examples/UICatalog/Scenarios/Themes.cs | 8 +- Examples/UICatalog/Scenarios/TreeUseCases.cs | 12 +- .../UICatalog/Scenarios/TreeViewFileSystem.cs | 2 +- .../UICatalog/Scenarios/ViewExperiments.cs | 10 +- .../Scenarios/WindowsAndFrameViews.cs | 2 +- Examples/UICatalog/UICatalog.cs | 21 +- Examples/UICatalog/UICatalog.csproj | 1 + Examples/UICatalog/UICatalogTop.cs | 3 +- NULLABLE_VIEWS_REMAINING.md | 163 ++++ PR_DESCRIPTION_UPDATED.md | 322 ++++++++ Terminal.Gui/App/Application.Current.cs | 18 + Terminal.Gui/App/Application.Driver.cs | 8 +- Terminal.Gui/App/Application.Keyboard.cs | 17 +- Terminal.Gui/App/Application.Lifecycle.cs | 66 +- Terminal.Gui/App/Application.Mouse.cs | 18 +- Terminal.Gui/App/Application.Navigation.cs | 5 +- Terminal.Gui/App/Application.Popover.cs | 4 +- Terminal.Gui/App/Application.Run.cs | 33 +- Terminal.Gui/App/Application.Screen.cs | 4 +- Terminal.Gui/App/Application.Toplevel.cs | 18 - Terminal.Gui/App/Application.cs | 89 +-- Terminal.Gui/App/ApplicationImpl.Driver.cs | 3 +- Terminal.Gui/App/ApplicationImpl.Lifecycle.cs | 36 +- Terminal.Gui/App/ApplicationImpl.Run.cs | 133 +-- Terminal.Gui/App/ApplicationImpl.Screen.cs | 23 +- Terminal.Gui/App/ApplicationImpl.cs | 87 +- Terminal.Gui/App/ApplicationNavigation.cs | 10 +- Terminal.Gui/App/ApplicationPopover.cs | 45 +- Terminal.Gui/App/CWP/CWPEventHelper.cs | 3 +- Terminal.Gui/App/CWP/CWPPropertyHelper.cs | 2 - Terminal.Gui/App/CWP/CWPWorkflowHelper.cs | 3 +- Terminal.Gui/App/CWP/CancelEventArgs.cs | 1 - Terminal.Gui/App/CWP/EventArgs.cs | 1 - Terminal.Gui/App/CWP/ResultEventArgs.cs | 3 +- Terminal.Gui/App/CWP/ValueChangedEventArgs.cs | 1 - .../App/CWP/ValueChangingEventArgs.cs | 3 +- Terminal.Gui/App/Clipboard/Clipboard.cs | 5 +- Terminal.Gui/App/Clipboard/ClipboardBase.cs | 1 + .../App/Clipboard/ClipboardProcessRunner.cs | 2 - Terminal.Gui/App/IApplication.cs | 60 +- Terminal.Gui/App/IPopover.cs | 11 +- Terminal.Gui/App/Keyboard/IKeyboard.cs | 5 +- Terminal.Gui/App/Keyboard/KeyboardImpl.cs | 39 +- .../App/MainLoop/ApplicationMainLoop.cs | 35 +- .../App/MainLoop/IApplicationMainLoop.cs | 10 +- .../App/MainLoop/IMainLoopCoordinator.cs | 3 +- .../App/MainLoop/MainLoopCoordinator.cs | 34 +- .../App/MainLoop/MainLoopSyncContext.cs | 73 +- Terminal.Gui/App/Mouse/IMouse.cs | 10 +- Terminal.Gui/App/Mouse/IMouseGrabHandler.cs | 1 - Terminal.Gui/App/Mouse/MouseGrabHandler.cs | 1 - Terminal.Gui/App/Mouse/MouseImpl.cs | 26 +- Terminal.Gui/App/PopoverBaseImpl.cs | 22 +- Terminal.Gui/App/SessionToken.cs | 2 +- Terminal.Gui/App/Timeout/ITimedEvents.cs | 11 +- .../App/Timeout/LogarithmicTimeout.cs | 1 + .../App/Timeout/SmoothAcceleratingTimeout.cs | 1 + Terminal.Gui/App/Timeout/TimedEvents.cs | 28 +- Terminal.Gui/App/Timeout/Timeout.cs | 2 +- .../Toplevel/IToplevelTransitionManager.cs | 8 +- .../App/Toplevel/ToplevelTransitionManager.cs | 15 +- .../Configuration/AppSettingsScope.cs | 3 +- .../Configuration/AttributeJsonConverter.cs | 4 +- .../Configuration/ColorJsonConverter.cs | 2 +- .../ConcurrentDictionaryJsonConverter.cs | 1 + Terminal.Gui/Configuration/ConfigLocations.cs | 3 +- Terminal.Gui/Configuration/ConfigProperty.cs | 3 +- .../Configuration/ConfigurationManager.cs | 4 +- .../ConfigurationManagerEventArgs.cs | 4 +- ...ConfigurationManagerNotEnabledException.cs | 3 +- .../ConfigurationPropertyAttribute.cs | 4 +- Terminal.Gui/Configuration/DeepCloner.cs | 2 +- .../Configuration/DictionaryJsonConverter.cs | 3 +- .../Configuration/KeyCodeJsonConverter.cs | 3 +- .../Configuration/KeyJsonConverter.cs | 2 +- .../Configuration/RuneJsonConverter.cs | 2 +- .../Configuration/SchemeJsonConverter.cs | 3 +- Terminal.Gui/Configuration/SchemeManager.cs | 3 +- Terminal.Gui/Configuration/Scope.cs | 3 +- .../Configuration/ScopeJsonConverter.cs | 3 +- Terminal.Gui/Configuration/SettingsScope.cs | 3 +- Terminal.Gui/Configuration/SourcesManager.cs | 3 +- Terminal.Gui/Configuration/ThemeManager.cs | 3 +- Terminal.Gui/Configuration/ThemeScope.cs | 3 +- Terminal.Gui/Drawing/Attribute.cs | 3 +- Terminal.Gui/Drawing/Cell.cs | 3 +- Terminal.Gui/Drawing/Color/AnsiColorCode.cs | 1 + .../Drawing/Color/AnsiColorNameResolver.cs | 2 - .../Drawing/Color/Color.ColorExtensions.cs | 1 - .../Color/Color.ColorParseException.cs | 1 - .../Drawing/Color/Color.Formatting.cs | 1 - Terminal.Gui/Drawing/Color/Color.Operators.cs | 1 - Terminal.Gui/Drawing/Color/Color.cs | 1 - Terminal.Gui/Drawing/Color/ColorModel.cs | 2 - Terminal.Gui/Drawing/Color/ColorStrings.cs | 1 - .../Drawing/Color/IColorNameResolver.cs | 2 - .../Drawing/Color/ICustomColorFormatter.cs | 1 - .../Color/MultiStandardColorNameResolver.cs | 2 - Terminal.Gui/Drawing/Color/StandardColors.cs | 1 - .../Color/StandardColorsNameResolver.cs | 4 +- Terminal.Gui/Drawing/Glyphs.cs | 3 +- .../LineCanvas/IntersectionDefinition.cs | 2 +- .../LineCanvas/IntersectionRuneType.cs | 2 +- .../Drawing/LineCanvas/IntersectionType.cs | 2 +- Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs | 15 +- Terminal.Gui/Drawing/LineCanvas/LineStyle.cs | 1 - .../Drawing/LineCanvas/StraightLine.cs | 1 - .../LineCanvas/StraightLineExtensions.cs | 6 +- .../Quant/PopularityPaletteWithThreshold.cs | 19 +- Terminal.Gui/Drawing/Region.cs | 63 +- Terminal.Gui/Drawing/RegionOp.cs | 3 +- Terminal.Gui/Drawing/Ruler.cs | 11 +- Terminal.Gui/Drawing/Scheme.cs | 1 - .../Drawing/Sixel/SixelSupportDetector.cs | 25 +- Terminal.Gui/Drawing/Sixel/SixelToRender.cs | 2 +- Terminal.Gui/Drawing/Thickness.cs | 19 +- Terminal.Gui/Drawing/VisualRoleEventArgs.cs | 5 +- .../AnsiHandling/AnsiEscapeSequence.cs | 1 - .../AnsiHandling/AnsiEscapeSequenceRequest.cs | 6 +- .../Drivers/AnsiHandling/AnsiMouseParser.cs | 1 - .../AnsiHandling/AnsiRequestScheduler.cs | 19 +- .../AnsiHandling/AnsiResponseExpectation.cs | 1 - .../AnsiHandling/AnsiResponseParser.cs | 2 - .../EscSeqUtils/EscSeqReqStatus.cs | 1 - .../EscSeqUtils/EscSeqRequests.cs | 2 - .../AnsiHandling/EscSeqUtils/EscSeqUtils.cs | 1 - .../Drivers/AnsiHandling/GenericHeld.cs | 1 - .../AnsiHandling/IAnsiResponseParser.cs | 1 - Terminal.Gui/Drivers/AnsiHandling/IHeld.cs | 1 - .../Keyboard/AnsiKeyboardParser.cs | 1 - .../Keyboard/AnsiKeyboardParserPattern.cs | 2 - .../AnsiHandling/Keyboard/CsiCursorPattern.cs | 1 - .../AnsiHandling/Keyboard/CsiKeyPattern.cs | 1 - .../AnsiHandling/Keyboard/EscAsAltPattern.cs | 1 - .../AnsiHandling/Keyboard/Ss3Pattern.cs | 1 - .../Drivers/AnsiHandling/Osc8UrlLinker.cs | 1 - .../Drivers/AnsiHandling/ReasonCannotSend.cs | 1 - .../Drivers/AnsiHandling/StringHeld.cs | 1 - Terminal.Gui/Drivers/ComponentFactoryImpl.cs | 3 +- Terminal.Gui/Drivers/CursorVisibility.cs | 1 - .../DotNetDriver/NetComponentFactory.cs | 1 - Terminal.Gui/Drivers/DotNetDriver/NetInput.cs | 3 +- .../Drivers/DotNetDriver/NetOutput.cs | 2 - .../Drivers/DotNetDriver/NetWinVTConsole.cs | 1 - Terminal.Gui/Drivers/DriverImpl.cs | 304 +++---- .../Drivers/FakeDriver/FakeClipboard.cs | 1 - .../FakeDriver/FakeComponentFactory.cs | 1 - Terminal.Gui/Drivers/FakeDriver/FakeInput.cs | 3 +- .../Drivers/FakeDriver/FakeInputProcessor.cs | 3 +- Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs | 26 +- Terminal.Gui/Drivers/IComponentFactory.cs | 3 +- Terminal.Gui/Drivers/IDriver.cs | 15 +- Terminal.Gui/Drivers/IInput.cs | 3 +- Terminal.Gui/Drivers/IInputProcessor.cs | 3 +- Terminal.Gui/Drivers/IOutput.cs | 8 + Terminal.Gui/Drivers/IOutputBuffer.cs | 3 +- Terminal.Gui/Drivers/ISizeMonitor.cs | 3 +- Terminal.Gui/Drivers/InputImpl.cs | 5 +- Terminal.Gui/Drivers/InputProcessorImpl.cs | 3 +- Terminal.Gui/Drivers/KeyCode.cs | 1 - Terminal.Gui/Drivers/MouseButtonStateEx.cs | 3 +- Terminal.Gui/Drivers/MouseInterpreter.cs | 3 +- Terminal.Gui/Drivers/OutputBase.cs | 131 ++- Terminal.Gui/Drivers/OutputBufferImpl.cs | 3 +- Terminal.Gui/Drivers/Platform.cs | 2 +- Terminal.Gui/Drivers/SizeMonitorImpl.cs | 3 +- .../Drivers/UnixDriver/UnixClipboard.cs | 1 + .../UnixDriver/UnixComponentFactory.cs | 1 - Terminal.Gui/Drivers/UnixDriver/UnixInput.cs | 1 - .../Drivers/UnixDriver/UnixKeyConverter.cs | 2 - Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs | 2 +- .../Drivers/WindowsDriver/ClipboardImpl.cs | 1 - .../WindowsDriver/WindowsComponentFactory.cs | 1 - .../Drivers/WindowsDriver/WindowsConsole.cs | 1 - .../Drivers/WindowsDriver/WindowsInput.cs | 1 - .../WindowsDriver/WindowsInputProcessor.cs | 1 - .../WindowsDriver/WindowsKeyConverter.cs | 2 - .../Drivers/WindowsDriver/WindowsKeyHelper.cs | 1 - .../WindowsDriver/WindowsKeyboardLayout.cs | 1 + .../Drivers/WindowsDriver/WindowsOutput.cs | 20 +- .../FileServices/DefaultSearchMatcher.cs | 8 +- .../FileServices/FileSystemIconProvider.cs | 1 - .../FileServices/FileSystemInfoStats.cs | 3 +- .../FileServices/FileSystemTreeBuilder.cs | 3 +- Terminal.Gui/Input/Command.cs | 2 +- Terminal.Gui/Input/CommandContext.cs | 5 +- Terminal.Gui/Input/CommandEventArgs.cs | 3 +- Terminal.Gui/Input/ICommandContext.cs | 3 +- Terminal.Gui/Input/IInputBinding.cs | 3 +- Terminal.Gui/Input/InputBindings.cs | 3 +- Terminal.Gui/Input/Keyboard/Key.cs | 1 - Terminal.Gui/Input/Keyboard/KeyBinding.cs | 2 +- Terminal.Gui/Input/Keyboard/KeyBindings.cs | 2 +- .../Input/Keyboard/KeyEqualityComparer.cs | 1 - .../Input/Mouse/GrabMouseEventArgs.cs | 1 - Terminal.Gui/Input/Mouse/MouseBinding.cs | 2 +- Terminal.Gui/Input/Mouse/MouseBindings.cs | 2 +- Terminal.Gui/Input/Mouse/MouseEventArgs.cs | 2 +- Terminal.Gui/Resources/GlobalResources.cs | 3 +- .../Resources/ResourceManagerWrapper.cs | 3 +- Terminal.Gui/Terminal.Gui.csproj | 11 + Terminal.Gui/Text/NerdFonts.cs | 7 +- Terminal.Gui/Text/RuneExtensions.cs | 3 +- Terminal.Gui/Text/StringExtensions.cs | 3 +- Terminal.Gui/Text/TextDirection.cs | 2 +- Terminal.Gui/Text/TextFormatter.cs | 36 +- Terminal.Gui/ViewBase/Adornment/Adornment.cs | 13 +- .../ViewBase/Adornment/Border.Arrangment.cs | 36 +- Terminal.Gui/ViewBase/Adornment/Border.cs | 32 +- .../ViewBase/Adornment/BorderSettings.cs | 3 - Terminal.Gui/ViewBase/Adornment/Margin.cs | 12 +- Terminal.Gui/ViewBase/Adornment/Padding.cs | 2 +- Terminal.Gui/ViewBase/Adornment/ShadowView.cs | 8 +- .../ViewBase/DrawAdornmentsEventArgs.cs | 3 +- Terminal.Gui/ViewBase/DrawContext.cs | 3 +- Terminal.Gui/ViewBase/DrawEventArgs.cs | 3 +- .../EnumExtensions/AddOrSubtractExtensions.cs | 1 - .../EnumExtensions/AlignmentExtensions.cs | 1 - .../AlignmentModesExtensions.cs | 1 - .../BorderSettingsExtensions.cs | 2 +- .../EnumExtensions/DimAutoStyleExtensions.cs | 1 - .../DimPercentModeExtensions.cs | 1 - .../EnumExtensions/DimensionExtensions.cs | 1 - .../ViewBase/EnumExtensions/SideExtensions.cs | 1 - .../ViewDiagnosticFlagsExtensions.cs | 1 - .../ViewBase/Helpers/StackExtensions.cs | 1 + Terminal.Gui/ViewBase/IDesignable.cs | 6 +- Terminal.Gui/ViewBase/IMouseHeldDown.cs | 3 +- Terminal.Gui/ViewBase/Layout/Aligner.cs | 1 + Terminal.Gui/ViewBase/Layout/Dim.cs | 11 +- Terminal.Gui/ViewBase/Layout/DimAbsolute.cs | 1 - Terminal.Gui/ViewBase/Layout/DimAuto.cs | 5 +- Terminal.Gui/ViewBase/Layout/DimAutoStyle.cs | 4 +- Terminal.Gui/ViewBase/Layout/DimCombine.cs | 1 - Terminal.Gui/ViewBase/Layout/DimFill.cs | 1 - Terminal.Gui/ViewBase/Layout/DimFunc.cs | 1 - Terminal.Gui/ViewBase/Layout/DimPercent.cs | 1 - .../ViewBase/Layout/DimPercentMode.cs | 4 +- Terminal.Gui/ViewBase/Layout/DimView.cs | 1 - Terminal.Gui/ViewBase/Layout/Dimension.cs | 4 +- .../ViewBase/Layout/LayoutException.cs | 1 - Terminal.Gui/ViewBase/Layout/Pos.cs | 11 +- Terminal.Gui/ViewBase/Layout/PosAbsolute.cs | 1 - Terminal.Gui/ViewBase/Layout/PosAlign.cs | 1 - Terminal.Gui/ViewBase/Layout/PosAnchorEnd.cs | 1 - Terminal.Gui/ViewBase/Layout/PosCenter.cs | 1 - Terminal.Gui/ViewBase/Layout/PosCombine.cs | 1 - Terminal.Gui/ViewBase/Layout/PosFunc.cs | 1 - Terminal.Gui/ViewBase/Layout/PosPercent.cs | 1 - Terminal.Gui/ViewBase/Layout/PosView.cs | 1 - Terminal.Gui/ViewBase/Layout/Side.cs | 4 +- .../Layout/SuperViewChangedEventArgs.cs | 6 +- Terminal.Gui/ViewBase/MouseHeldDown.cs | 3 +- .../ViewBase/Navigation/FocusEventArgs.cs | 9 +- .../ViewBase/Orientation/IOrientation.cs | 4 +- .../ViewBase/Orientation/Orientation.cs | 2 +- .../ViewBase/Orientation/OrientationHelper.cs | 2 +- Terminal.Gui/ViewBase/View.Adornments.cs | 3 +- Terminal.Gui/ViewBase/View.Arrangement.cs | 3 +- Terminal.Gui/ViewBase/View.Command.cs | 3 +- Terminal.Gui/ViewBase/View.Content.cs | 3 +- Terminal.Gui/ViewBase/View.Cursor.cs | 1 - Terminal.Gui/ViewBase/View.Diagnostics.cs | 3 +- .../ViewBase/View.Drawing.Attribute.cs | 3 +- .../ViewBase/View.Drawing.Clipping.cs | 27 +- .../ViewBase/View.Drawing.Primitives.cs | 10 +- Terminal.Gui/ViewBase/View.Drawing.Scheme.cs | 3 +- Terminal.Gui/ViewBase/View.Drawing.cs | 20 +- Terminal.Gui/ViewBase/View.Hierarchy.cs | 5 +- Terminal.Gui/ViewBase/View.Keyboard.cs | 3 +- Terminal.Gui/ViewBase/View.Layout.cs | 57 +- Terminal.Gui/ViewBase/View.Mouse.cs | 23 +- Terminal.Gui/ViewBase/View.Navigation.cs | 27 +- Terminal.Gui/ViewBase/View.ScrollBars.cs | 3 +- Terminal.Gui/ViewBase/View.Text.cs | 1 - Terminal.Gui/ViewBase/View.cs | 61 +- Terminal.Gui/ViewBase/ViewDiagnosticFlags.cs | 3 +- Terminal.Gui/ViewBase/ViewEventArgs.cs | 2 +- .../Views/Autocomplete/AppendAutocomplete.cs | 35 +- .../Views/Autocomplete/AutocompleteBase.cs | 1 + .../Views/Autocomplete/AutocompleteContext.cs | 1 + .../AutocompleteFilepathContext.cs | 1 + .../Views/Autocomplete/IAutocomplete.cs | 1 + .../Autocomplete/ISuggestionGenerator.cs | 1 + .../Autocomplete/PopupAutocomplete.PopUp.cs | 1 - .../Views/Autocomplete/PopupAutocomplete.cs | 10 +- .../SingleWordSuggestionGenerator.cs | 1 + Terminal.Gui/Views/Autocomplete/Suggestion.cs | 1 + Terminal.Gui/Views/Bar.cs | 19 +- Terminal.Gui/Views/Button.cs | 1 + Terminal.Gui/Views/CharMap/CharMap.cs | 7 +- Terminal.Gui/Views/CharMap/UcdApiClient.cs | 1 - Terminal.Gui/Views/CharMap/UnicodeRange.cs | 1 - Terminal.Gui/Views/CheckBox.cs | 2 +- Terminal.Gui/Views/CheckState.cs | 2 +- .../CollectionNavigator.cs | 3 +- .../CollectionNavigatorBase.cs | 4 +- .../DefaultCollectionNavigatorMatcher.cs | 2 +- .../ICollectionNavigator.cs | 2 +- .../ICollectionNavigatorMatcher.cs | 1 + .../IListCollectionNavigator.cs | 1 + .../TableCollectionNavigator.cs | 1 + Terminal.Gui/Views/Color/BBar.cs | 2 +- Terminal.Gui/Views/Color/ColorBar.cs | 2 +- .../Views/Color/ColorModelStrategy.cs | 2 +- Terminal.Gui/Views/Color/ColorPicker.16.cs | 2 +- .../Views/Color/ColorPicker.Prompt.cs | 1 + Terminal.Gui/Views/Color/ColorPicker.Style.cs | 2 +- Terminal.Gui/Views/Color/ColorPicker.cs | 2 +- Terminal.Gui/Views/Color/GBar.cs | 2 +- Terminal.Gui/Views/Color/HueBar.cs | 2 +- Terminal.Gui/Views/Color/IColorBar.cs | 1 + Terminal.Gui/Views/Color/LightnessBar.cs | 2 +- Terminal.Gui/Views/Color/RBar.cs | 2 +- Terminal.Gui/Views/Color/SaturationBar.cs | 2 +- Terminal.Gui/Views/Color/ValueBar.cs | 2 +- Terminal.Gui/Views/ComboBox.cs | 1 + Terminal.Gui/Views/DatePicker.cs | 2 +- Terminal.Gui/Views/Dialog.cs | 6 +- Terminal.Gui/Views/FileDialog.cs | 3 +- Terminal.Gui/Views/FileDialogs/AllowedType.cs | 1 + .../FileDialogs/DefaultFileOperations.cs | 1 + Terminal.Gui/Views/FileDialogs/FileDialog.cs | 17 +- .../FileDialogCollectionNavigator.cs | 1 + .../Views/FileDialogs/FileDialogHistory.cs | 1 + .../Views/FileDialogs/FileDialogState.cs | 1 + .../Views/FileDialogs/FileDialogStyle.cs | 1 + .../FileDialogs/FileDialogTableSource.cs | 2 +- .../FileDialogs/FilesSelectedEventArgs.cs | 1 + Terminal.Gui/Views/FileDialogs/OpenDialog.cs | 3 +- Terminal.Gui/Views/FileDialogs/OpenMode.cs | 1 + Terminal.Gui/Views/FileDialogs/SaveDialog.cs | 3 +- Terminal.Gui/Views/FrameView.cs | 2 +- Terminal.Gui/Views/GraphView/Axis.cs | 1 + Terminal.Gui/Views/GraphView/BarSeriesBar.cs | 1 + .../Views/GraphView/GraphCellToRender.cs | 1 + Terminal.Gui/Views/GraphView/GraphView.cs | 5 +- Terminal.Gui/Views/GraphView/IAnnotation.cs | 1 + .../Views/GraphView/LegendAnnotation.cs | 1 + Terminal.Gui/Views/GraphView/LineF.cs | 1 + .../Views/GraphView/PathAnnotation.cs | 1 + Terminal.Gui/Views/GraphView/Series.cs | 1 - .../Views/GraphView/TextAnnotation.cs | 1 + Terminal.Gui/Views/HexView.cs | 2 +- Terminal.Gui/Views/HexViewEventArgs.cs | 1 + Terminal.Gui/Views/IListDataSource.cs | 2 +- Terminal.Gui/Views/Label.cs | 6 +- Terminal.Gui/Views/Line.cs | 2 - Terminal.Gui/Views/ListView.cs | 1 + Terminal.Gui/Views/ListViewEventArgs.cs | 1 + Terminal.Gui/Views/Menu/MenuBarItemv2.cs | 5 +- Terminal.Gui/Views/Menu/MenuBarv2.cs | 43 +- Terminal.Gui/Views/Menu/MenuItemv2.cs | 4 +- Terminal.Gui/Views/Menu/Menuv2.cs | 4 +- Terminal.Gui/Views/Menu/PopoverMenu.cs | 34 +- Terminal.Gui/Views/Menuv1/Menu.cs | 23 +- Terminal.Gui/Views/Menuv1/MenuBar.cs | 13 +- Terminal.Gui/Views/Menuv1/MenuBarItem.cs | 1 - .../Views/Menuv1/MenuClosingEventArgs.cs | 3 +- Terminal.Gui/Views/Menuv1/MenuItem.cs | 1 - .../Views/Menuv1/MenuItemCheckStyle.cs | 3 +- .../Views/Menuv1/MenuOpenedEventArgs.cs | 3 +- .../Views/Menuv1/MenuOpeningEventArgs.cs | 3 +- Terminal.Gui/Views/MessageBox.cs | 1 + Terminal.Gui/Views/NumericUpDown.cs | 1 - Terminal.Gui/Views/ProgressBar.cs | 14 +- .../Views/ReadOnlyCollectionExtensions.cs | 1 + Terminal.Gui/Views/ScrollBar/ScrollBar.cs | 2 +- Terminal.Gui/Views/ScrollBar/ScrollSlider.cs | 2 +- Terminal.Gui/Views/SelectedItemChangedArgs.cs | 2 +- Terminal.Gui/Views/Selectors/FlagSelector.cs | 4 - .../Views/Selectors/FlagSelectorTEnum.cs | 1 - .../Views/Selectors/OptionSelector.cs | 1 - .../Views/Selectors/OptionSelectorTEnum.cs | 1 - Terminal.Gui/Views/Selectors/SelectorBase.cs | 1 - .../Views/Selectors/SelectorStyles.cs | 2 +- Terminal.Gui/Views/Shortcut.cs | 108 ++- Terminal.Gui/Views/Slider/Slider.cs | 5 +- Terminal.Gui/Views/Slider/SliderAttributes.cs | 2 +- .../Views/Slider/SliderConfiguration.cs | 4 +- Terminal.Gui/Views/Slider/SliderEventArgs.cs | 3 +- Terminal.Gui/Views/Slider/SliderOption.cs | 3 +- .../Views/Slider/SliderOptionEventArgs.cs | 3 +- Terminal.Gui/Views/Slider/SliderStyle.cs | 2 +- Terminal.Gui/Views/Slider/SliderType.cs | 2 +- Terminal.Gui/Views/SpinnerView/SpinnerView.cs | 12 +- Terminal.Gui/Views/StatusBar.cs | 19 +- Terminal.Gui/Views/TabView/Tab.cs | 2 +- .../Views/TabView/TabChangedEventArgs.cs | 1 + .../Views/TabView/TabMouseEventArgs.cs | 2 +- Terminal.Gui/Views/TabView/TabRow.cs | 1 - Terminal.Gui/Views/TabView/TabStyle.cs | 1 + Terminal.Gui/Views/TabView/TabView.cs | 1 - .../Views/TableView/CellActivatedEventArgs.cs | 1 + .../Views/TableView/CellColorGetterArgs.cs | 1 + .../Views/TableView/CellToggledEventArgs.cs | 1 + .../TableView/CheckBoxTableSourceWrapper.cs | 1 + .../CheckBoxTableSourceWrapperByIndex.cs | 1 + .../CheckBoxTableSourceWrapperByObject.cs | 1 + Terminal.Gui/Views/TableView/ColumnStyle.cs | 1 + .../Views/TableView/DataTableSource.cs | 1 + .../Views/TableView/EnumerableTableSource.cs | 1 + .../Views/TableView/IEnumerableTableSource.cs | 1 + Terminal.Gui/Views/TableView/ITableSource.cs | 1 + .../Views/TableView/ListColumnStyle.cs | 2 +- .../Views/TableView/ListTableSource.cs | 1 + .../Views/TableView/RowColorGetterArgs.cs | 1 + .../TableView/SelectedCellChangedEventArgs.cs | 1 + .../Views/TableView/TableSelection.cs | 1 + Terminal.Gui/Views/TableView/TableStyle.cs | 1 + Terminal.Gui/Views/TableView/TableView.cs | 1 + .../Views/TableView/TreeTableSource.cs | 1 + .../TextInput/ContentsChangedEventArgs.cs | 1 + Terminal.Gui/Views/TextInput/DateField.cs | 2 +- Terminal.Gui/Views/TextInput/HistoryText.cs | 1 - .../TextInput/HistoryTextItemEventArgs.cs | 2 +- .../Views/TextInput/ITextValidateProvider.cs | 2 +- .../Views/TextInput/NetMaskedTextProvider.cs | 3 +- .../Views/TextInput/TextEditingLineStatus.cs | 1 + Terminal.Gui/Views/TextInput/TextField.cs | 32 +- Terminal.Gui/Views/TextInput/TextModel.cs | 1 - .../Views/TextInput/TextRegexProvider.cs | 2 +- .../Views/TextInput/TextValidateField.cs | 2 +- Terminal.Gui/Views/TextInput/TextView.cs | 34 +- Terminal.Gui/Views/TextInput/TimeField.cs | 1 + .../Views/TextInput/WordWrapManager.cs | 1 - Terminal.Gui/Views/Toplevel.cs | 34 +- Terminal.Gui/Views/ToplevelEventArgs.cs | 1 + .../Views/TreeView/AspectGetterDelegate.cs | 1 + Terminal.Gui/Views/TreeView/Branch.cs | 2 +- .../Views/TreeView/DelegateTreeBuilder.cs | 1 + .../TreeView/DrawTreeViewLineEventArgs.cs | 1 + Terminal.Gui/Views/TreeView/ITreeBuilder.cs | 1 + .../Views/TreeView/ITreeViewFilter.cs | 1 + .../TreeView/ObjectActivatedEventArgs.cs | 1 + .../TreeView/SelectionChangedEventArgs.cs | 1 + Terminal.Gui/Views/TreeView/TreeBuilder.cs | 1 + Terminal.Gui/Views/TreeView/TreeNode.cs | 1 + .../Views/TreeView/TreeNodeBuilder.cs | 1 + Terminal.Gui/Views/TreeView/TreeStyle.cs | 1 + Terminal.Gui/Views/TreeView/TreeView.cs | 1 + .../Views/TreeView/TreeViewTextFilter.cs | 1 + Terminal.Gui/Views/View.cs | 1 - Terminal.Gui/Views/Window.cs | 2 +- Terminal.Gui/Views/Wizard/Wizard.cs | 8 +- Terminal.Gui/Views/Wizard/WizardEventArgs.cs | 1 + Terminal.Gui/Views/Wizard/WizardStep.cs | 2 +- Terminal.sln | 37 +- .../FluentTests/FileDialogFluentTests.cs | 6 +- .../GuiTestContextKeyEventTests.cs | 36 +- .../GuiTestContextMouseEventTests.cs | 2 +- .../FluentTests/GuiTestContextTests.cs | 20 +- .../FluentTests/MenuBarv2Tests.cs | 156 ++-- .../FluentTests/NavigationTests.cs | 6 +- .../FluentTests/PopverMenuTests.cs | 171 ++-- .../FluentTests/TreeViewFluentTests.cs | 4 +- .../UICatalog/ScenarioTests.cs | 39 +- Tests/StressTests/ApplicationStressTests.cs | 4 +- Tests/StressTests/ScenariosStressTests.cs | 4 +- .../FakeDriver/FakeApplicationFactory.cs | 9 +- .../FakeDriver/FakeApplicationLifecycle.cs | 6 +- .../GuiTestContext.ContextMenu.cs | 10 +- .../GuiTestContext.Input.cs | 16 +- .../GuiTestContext.Navigation.cs | 6 +- .../GuiTestContext.ViewBase.cs | 10 +- .../GuiTestContext.cs | 111 +-- .../Application.NavigationTests.cs | 58 +- .../ApplicationImplBeginEndTests.cs | 505 ++++++++++++ .../Application/ApplicationImplTests.cs | 522 ++++-------- .../Application/ApplicationPopoverTests.cs | 82 +- .../Application/ApplicationScreenTests.cs | 12 +- .../UnitTests/Application/ApplicationTests.cs | 275 +++---- Tests/UnitTests/Application/CursorTests.cs | 34 +- .../Application/MainLoopCoordinatorTests.cs | 2 +- .../Mouse/ApplicationMouseEnterLeaveTests.cs | 78 +- .../Mouse/ApplicationMouseTests.cs | 16 +- .../Application/SessionTokenTests.cs | 15 +- .../Application/SynchronizatonContextTests.cs | 8 +- .../UnitTests/Application/TimedEventsTests.cs | 31 + Tests/UnitTests/AutoInitShutdownAttribute.cs | 28 +- .../Configuration/ThemeScopeTests.cs | 37 + Tests/UnitTests/Dialogs/DialogTests.cs | 30 +- Tests/UnitTests/Dialogs/MessageBoxTests.cs | 14 +- Tests/UnitTests/DriverAssert.cs | 10 +- Tests/UnitTests/Drivers/ClipRegionTests.cs | 6 +- Tests/UnitTests/Drivers/DriverTests.cs | 38 +- .../SetupFakeApplicationAttribute.cs | 1 + .../View/Adornment/AdornmentSubViewTests.cs | 26 +- .../View/Adornment/AdornmentTests.cs | 8 +- Tests/UnitTests/View/Adornment/BorderTests.cs | 16 +- Tests/UnitTests/View/Adornment/MarginTests.cs | 32 +- .../UnitTests/View/Adornment/PaddingTests.cs | 8 +- .../View/Adornment/ShadowStyleTests.cs | 9 +- Tests/UnitTests/View/ArrangementTests.cs | 217 +++++ .../UnitTests/View/Draw/ClearViewportTests.cs | 20 +- Tests/UnitTests/View/Draw/ClipTests.cs | 33 +- Tests/UnitTests/View/Draw/DrawTests.cs | 40 +- Tests/UnitTests/View/Draw/TransparentTests.cs | 2 + .../View/Keyboard/KeyBindingsTests.cs | 6 +- .../View/Layout/GetViewsUnderLocationTests.cs | 226 +++--- .../UnitTests/View/Layout/Pos.CombineTests.cs | 26 +- Tests/UnitTests/View/Layout/Pos.Tests.cs | 10 +- Tests/UnitTests/View/Layout/SetLayoutTests.cs | 16 +- .../View/Navigation/CanFocusTests.cs | 21 +- .../View/Navigation/NavigationTests.cs | 13 +- Tests/UnitTests/View/TextTests.cs | 11 +- Tests/UnitTests/View/ViewCommandTests.cs | 12 +- Tests/UnitTests/View/ViewTests.cs | 1 + .../ViewportSettings.TransparentMouseTests.cs | 6 +- .../Views/AppendAutocompleteTests.cs | 86 +- Tests/UnitTests/Views/ButtonTests.cs | 17 +- Tests/UnitTests/Views/CheckBoxTests.cs | 9 +- Tests/UnitTests/Views/ColorPickerTests.cs | 12 +- Tests/UnitTests/Views/ComboBoxTests.cs | 19 +- Tests/UnitTests/Views/GraphViewTests.cs | 26 +- Tests/UnitTests/Views/HexViewTests.cs | 78 +- Tests/UnitTests/Views/LabelTests.cs | 88 +- Tests/UnitTests/Views/MenuBarTests.cs | 60 +- .../UnitTests/Views/Menuv1/MenuBarv1Tests.cs | 102 +-- Tests/UnitTests/Views/ProgressBarTests.cs | 25 +- Tests/UnitTests/Views/ScrollBarTests.cs | 2 + Tests/UnitTests/Views/ScrollSliderTests.cs | 340 -------- Tests/UnitTests/Views/ShortcutTests.cs | 83 +- Tests/UnitTests/Views/SpinnerViewTests.cs | 8 +- Tests/UnitTests/Views/TabViewTests.cs | 50 +- Tests/UnitTests/Views/TableViewTests.cs | 171 ++-- Tests/UnitTests/Views/TextFieldTests.cs | 44 +- Tests/UnitTests/Views/TextViewTests.cs | 20 +- Tests/UnitTests/Views/ToplevelTests.cs | 72 +- Tests/UnitTests/Views/TreeTableSourceTests.cs | 19 +- Tests/UnitTests/Views/TreeViewTests.cs | 50 +- .../Application/ApplicationPopoverTests.cs | 12 +- .../Application/MouseInterfaceTests.cs | 2 +- .../Application/MouseTests.cs | 2 +- .../Application/PopoverBaseImplTests.cs | 8 +- .../Configuration/SourcesManagerTests.cs | 27 + .../Drawing/RulerTests.cs | 14 +- .../Drawing/ThicknessTests.cs | 21 +- .../Drivers/AnsiRequestSchedulerTests.cs | 30 +- .../Drivers/ToAnsiTests.cs | 342 ++++++++ Tests/UnitTestsParallelizable/TestSetup.cs | 8 +- .../Text/TextFormatterDrawTests.cs | 36 +- .../Text/TextFormatterJustificationTests.cs | 2 +- .../Text/TextFormatterTests.cs | 31 +- .../UnitTests.Parallelizable.csproj | 3 + .../View/ArrangementTests.cs | 454 +++-------- .../View/Draw/NeedsDrawTests.cs | 16 +- .../View/Draw/ViewClearViewportTests.cs | 371 +++++++++ .../Draw/ViewDrawTextAndLineCanvasTests.cs | 495 ++++++++++++ .../View/Draw/ViewDrawingClippingTests.cs | 754 ++++++++++++++++++ .../View/Draw/ViewDrawingFlowTests.cs | 701 ++++++++++++++++ .../View/Layout/GetViewsUnderLocationTests.cs | 4 +- .../Views/AllViewsDrawTests.cs | 1 + .../Views/LineTests.cs | 12 +- .../Views/ScrollSliderTests.cs | 341 +++++++- .../Views/ShortcutTests.cs | 13 +- codecov.yml | 2 +- docfx/docs/View.md | 5 + docfx/docs/application.md | 552 +++++++++++++ docfx/docs/config.md | 2 +- docfx/docs/drawing.md | 40 +- docfx/docs/drivers.md | 110 +-- docfx/docs/keyboard.md | 50 +- docfx/docs/migratingfromv1.md | 6 +- docfx/docs/mouse.md | 29 +- docfx/docs/navigation.md | 2 +- docfx/docs/toc.yml | 2 + docfx/schemas/tui-config-schema.json | 2 +- 591 files changed, 8850 insertions(+), 4437 deletions(-) create mode 100644 NULLABLE_VIEWS_REMAINING.md create mode 100644 PR_DESCRIPTION_UPDATED.md create mode 100644 Terminal.Gui/App/Application.Current.cs delete mode 100644 Terminal.Gui/App/Application.Toplevel.cs delete mode 100644 Terminal.Gui/Views/View.cs create mode 100644 Tests/UnitTests/Application/ApplicationImplBeginEndTests.cs create mode 100644 Tests/UnitTests/View/ArrangementTests.cs delete mode 100644 Tests/UnitTests/Views/ScrollSliderTests.cs create mode 100644 Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs create mode 100644 Tests/UnitTestsParallelizable/View/Draw/ViewClearViewportTests.cs create mode 100644 Tests/UnitTestsParallelizable/View/Draw/ViewDrawTextAndLineCanvasTests.cs create mode 100644 Tests/UnitTestsParallelizable/View/Draw/ViewDrawingClippingTests.cs create mode 100644 Tests/UnitTestsParallelizable/View/Draw/ViewDrawingFlowTests.cs create mode 100644 docfx/docs/application.md diff --git a/Examples/CommunityToolkitExample/Program.cs b/Examples/CommunityToolkitExample/Program.cs index 265c979aa..74e45ce06 100644 --- a/Examples/CommunityToolkitExample/Program.cs +++ b/Examples/CommunityToolkitExample/Program.cs @@ -16,7 +16,7 @@ public static class Program Services = ConfigureServices (); Application.Init (); Application.Run (Services.GetRequiredService ()); - Application.Top?.Dispose (); + Application.Current?.Dispose (); Application.Shutdown (); } diff --git a/Examples/ReactiveExample/Program.cs b/Examples/ReactiveExample/Program.cs index 910d1f4a4..f73aa807d 100644 --- a/Examples/ReactiveExample/Program.cs +++ b/Examples/ReactiveExample/Program.cs @@ -16,7 +16,7 @@ public static class Program RxApp.MainThreadScheduler = TerminalScheduler.Default; RxApp.TaskpoolScheduler = TaskPoolScheduler.Default; Application.Run (new LoginView (new LoginViewModel ())); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.Shutdown (); } } diff --git a/Examples/ReactiveExample/TerminalScheduler.cs b/Examples/ReactiveExample/TerminalScheduler.cs index 9c8286722..3b24cc6d8 100644 --- a/Examples/ReactiveExample/TerminalScheduler.cs +++ b/Examples/ReactiveExample/TerminalScheduler.cs @@ -22,7 +22,7 @@ public class TerminalScheduler : LocalScheduler var cancellation = new CancellationDisposable (); Application.Invoke ( - () => + (_) => { if (!cancellation.Token.IsCancellationRequested) { diff --git a/Examples/UICatalog/Resources/config.json b/Examples/UICatalog/Resources/config.json index 916743d23..e47ea567c 100644 --- a/Examples/UICatalog/Resources/config.json +++ b/Examples/UICatalog/Resources/config.json @@ -86,7 +86,7 @@ "Menu": { "Normal": { "Foreground": "Black", - "Background": "WHite" + "Background": "White" }, "Focus": { "Foreground": "White", @@ -136,17 +136,16 @@ { "UI Catalog Theme": { "Window.DefaultShadow": "Transparent", + "Button.DefaultShadow": "None", "CheckBox.DefaultHighlightStates": "In, Pressed, PressedOutside", "MessageBox.DefaultButtonAlignment": "Start", "StatusBar.DefaultSeparatorLineStyle": "Single", "Dialog.DefaultMinimumWidth": 80, - "MessageBox.DefaultBorderStyle": "Dotted", "NerdFonts.Enable": false, "MessageBox.DefaultMinimumWidth": 0, "Window.DefaultBorderStyle": "Double", "Dialog.DefaultShadow": "Opaque", "Dialog.DefaultButtonAlignment": "Start", - "Button.DefaultShadow": "Transparent", "FrameView.DefaultBorderStyle": "Double", "MessageBox.DefaultMinimumHeight": 0, "Button.DefaultHighlightStates": "In, Pressed", diff --git a/Examples/UICatalog/Scenario.cs b/Examples/UICatalog/Scenario.cs index 76fc5dc2a..0531d06c9 100644 --- a/Examples/UICatalog/Scenario.cs +++ b/Examples/UICatalog/Scenario.cs @@ -221,7 +221,7 @@ public class Scenario : IDisposable private void OnApplicationSessionBegun (object? sender, SessionTokenEventArgs e) { - SubscribeAllSubViews (Application.Top!); + SubscribeAllSubViews (Application.Current!); _demoKeys = GetDemoKeyStrokes (); @@ -241,7 +241,7 @@ public class Scenario : IDisposable return; - // Get a list of all subviews under Application.Top (and their subviews, etc.) + // Get a list of all subviews under Application.Current (and their subviews, etc.) // and subscribe to their DrawComplete event void SubscribeAllSubViews (View view) { diff --git a/Examples/UICatalog/Scenarios/AllViewsTester.cs b/Examples/UICatalog/Scenarios/AllViewsTester.cs index c3695121e..2f6e9e016 100644 --- a/Examples/UICatalog/Scenarios/AllViewsTester.cs +++ b/Examples/UICatalog/Scenarios/AllViewsTester.cs @@ -28,7 +28,7 @@ public class AllViewsTester : Scenario public override void Main () { - // Don't create a sub-win (Scenario.Win); just use Application.Top + // Don't create a sub-win (Scenario.Win); just use Application.Current Application.Init (); var app = new Window diff --git a/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs b/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs index e058ea4bf..7d1b36020 100644 --- a/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs +++ b/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs @@ -92,7 +92,7 @@ public class AnimationScenario : Scenario { // When updating from a Thread/Task always use Invoke Application.Invoke ( - () => + (_) => { _imageView.NextFrame (); _imageView.SetNeedsDraw (); diff --git a/Examples/UICatalog/Scenarios/AnsiRequestsScenario.cs b/Examples/UICatalog/Scenarios/AnsiRequestsScenario.cs index ad4f881ba..a8d7c72ad 100644 --- a/Examples/UICatalog/Scenarios/AnsiRequestsScenario.cs +++ b/Examples/UICatalog/Scenarios/AnsiRequestsScenario.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; +#nullable enable using System.Text; namespace UICatalog.Scenarios; @@ -9,16 +7,19 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Tests")] public sealed class AnsiEscapeSequenceRequests : Scenario { - private GraphView _graphView; + private GraphView? _graphView; - private ScatterSeries _sentSeries; - private ScatterSeries _answeredSeries; + private ScatterSeries? _sentSeries; + private ScatterSeries? _answeredSeries; private readonly List _sends = new (); private readonly object _lockAnswers = new object (); private readonly Dictionary _answers = new (); - private Label _lblSummary; + private Label? _lblSummary; + + private object? _updateTimeoutToken; + private object? _sendDarTimeoutToken; public override void Main () { @@ -32,7 +33,7 @@ public sealed class AnsiEscapeSequenceRequests : Scenario CanFocus = true }; - Tab single = new Tab (); + Tab single = new (); single.DisplayText = "Single"; single.View = BuildSingleTab (); @@ -57,6 +58,8 @@ public sealed class AnsiEscapeSequenceRequests : Scenario single.View.Dispose (); appWindow.Dispose (); + Application.RemoveTimeout (_updateTimeoutToken!); + Application.RemoveTimeout (_sendDarTimeoutToken!); // Shutdown - Calling Application.Shutdown is required. Application.Shutdown (); } @@ -70,7 +73,7 @@ public sealed class AnsiEscapeSequenceRequests : Scenario CanFocus = true }; - w.Padding.Thickness = new (1); + w!.Padding!.Thickness = new (1); var scrRequests = new List { @@ -103,7 +106,7 @@ public sealed class AnsiEscapeSequenceRequests : Scenario } var selAnsiEscapeSequenceRequestName = scrRequests [cbRequests.SelectedItem]; - AnsiEscapeSequence selAnsiEscapeSequenceRequest = null; + AnsiEscapeSequence? selAnsiEscapeSequenceRequest = null; switch (selAnsiEscapeSequenceRequestName) { @@ -163,12 +166,12 @@ public sealed class AnsiEscapeSequenceRequests : Scenario Value = string.IsNullOrEmpty (tfValue.Text) ? null : tfValue.Text }; - Application.Driver.QueueAnsiRequest ( + Application.Driver?.QueueAnsiRequest ( new () { Request = ansiEscapeSequenceRequest.Request, Terminator = ansiEscapeSequenceRequest.Terminator, - ResponseReceived = (s) => OnSuccess (s, tvResponse, tvError, tvValue, tvTerminator, lblSuccess), + ResponseReceived = (s) => OnSuccess (s!, tvResponse, tvError, tvValue, tvTerminator, lblSuccess), Abandoned = () => OnFail (tvResponse, tvError, tvValue, tvTerminator, lblSuccess) }); }; @@ -218,21 +221,21 @@ public sealed class AnsiEscapeSequenceRequests : Scenario Width = Dim.Fill () }; - Application.AddTimeout ( - TimeSpan.FromMilliseconds (1000), - () => - { - lock (_lockAnswers) - { - UpdateGraph (); + _updateTimeoutToken = Application.AddTimeout ( + TimeSpan.FromMilliseconds (1000), + () => + { + lock (_lockAnswers) + { + UpdateGraph (); - UpdateResponses (); - } + UpdateResponses (); + } - return true; - }); + return true; + }); var tv = new TextView () { @@ -266,28 +269,28 @@ public sealed class AnsiEscapeSequenceRequests : Scenario int lastSendTime = Environment.TickCount; object lockObj = new object (); - Application.AddTimeout ( - TimeSpan.FromMilliseconds (50), - () => - { - lock (lockObj) - { - if (cbDar.Value > 0) - { - int interval = 1000 / cbDar.Value; // Calculate the desired interval in milliseconds - int currentTime = Environment.TickCount; // Current system time in milliseconds + _sendDarTimeoutToken = Application.AddTimeout ( + TimeSpan.FromMilliseconds (50), + () => + { + lock (lockObj) + { + if (cbDar.Value > 0) + { + int interval = 1000 / cbDar.Value; // Calculate the desired interval in milliseconds + int currentTime = Environment.TickCount; // Current system time in milliseconds - // Check if the time elapsed since the last send is greater than the interval - if (currentTime - lastSendTime >= interval) - { - SendDar (); // Send the request - lastSendTime = currentTime; // Update the last send time - } - } - } + // Check if the time elapsed since the last send is greater than the interval + if (currentTime - lastSendTime >= interval) + { + SendDar (); // Send the request + lastSendTime = currentTime; // Update the last send time + } + } + } - return true; - }); + return true; + }); _graphView = new GraphView () @@ -318,7 +321,7 @@ public sealed class AnsiEscapeSequenceRequests : Scenario } private void UpdateResponses () { - _lblSummary.Text = GetSummary (); + _lblSummary!.Text = GetSummary (); _lblSummary.SetNeedsDraw (); } @@ -340,8 +343,8 @@ public sealed class AnsiEscapeSequenceRequests : Scenario private void SetupGraph () { - _graphView.Series.Add (_sentSeries = new ScatterSeries ()); - _graphView.Series.Add (_answeredSeries = new ScatterSeries ()); + _graphView!.Series.Add (_sentSeries = new ScatterSeries ()); + _graphView!.Series.Add (_answeredSeries = new ScatterSeries ()); _sentSeries.Fill = new GraphCellToRender (new Rune ('.'), new Attribute (ColorName16.BrightGreen, ColorName16.Black)); _answeredSeries.Fill = new GraphCellToRender (new Rune ('.'), new Attribute (ColorName16.BrightRed, ColorName16.Black)); @@ -358,17 +361,17 @@ public sealed class AnsiEscapeSequenceRequests : Scenario private void UpdateGraph () { - _sentSeries.Points = _sends + _sentSeries!.Points = _sends .GroupBy (ToSeconds) .Select (g => new PointF (g.Key, g.Count ())) .ToList (); - _answeredSeries.Points = _answers.Keys + _answeredSeries!.Points = _answers.Keys .GroupBy (ToSeconds) .Select (g => new PointF (g.Key, g.Count ())) .ToList (); // _graphView.ScrollOffset = new PointF(,0); - _graphView.SetNeedsDraw (); + _graphView!.SetNeedsDraw (); } @@ -379,13 +382,13 @@ public sealed class AnsiEscapeSequenceRequests : Scenario private void SendDar () { - Application.Driver.QueueAnsiRequest ( - new () - { - Request = EscSeqUtils.CSI_SendDeviceAttributes.Request, - Terminator = EscSeqUtils.CSI_SendDeviceAttributes.Terminator, - ResponseReceived = HandleResponse - }); + Application.Driver?.QueueAnsiRequest ( + new () + { + Request = EscSeqUtils.CSI_SendDeviceAttributes.Request, + Terminator = EscSeqUtils.CSI_SendDeviceAttributes.Terminator, + ResponseReceived = HandleResponse! + }); _sends.Add (DateTime.Now); } diff --git a/Examples/UICatalog/Scenarios/Bars.cs b/Examples/UICatalog/Scenarios/Bars.cs index 50153f07d..03f908611 100644 --- a/Examples/UICatalog/Scenarios/Bars.cs +++ b/Examples/UICatalog/Scenarios/Bars.cs @@ -28,7 +28,7 @@ public class Bars : Scenario // QuitKey and it only sticks if changed after init private void App_Loaded (object sender, EventArgs e) { - Application.Top!.Title = GetQuitKeyAndName (); + Application.Current!.Title = GetQuitKeyAndName (); ObservableCollection eventSource = new (); ListView eventLog = new ListView () @@ -41,7 +41,7 @@ public class Bars : Scenario Source = new ListWrapper (eventSource) }; eventLog.Border!.Thickness = new (0, 1, 0, 0); - Application.Top.Add (eventLog); + Application.Current.Add (eventLog); FrameView menuBarLikeExamples = new () { @@ -51,7 +51,7 @@ public class Bars : Scenario Width = Dim.Fill () - Dim.Width (eventLog), Height = Dim.Percent(33), }; - Application.Top.Add (menuBarLikeExamples); + Application.Current.Add (menuBarLikeExamples); Label label = new Label () { @@ -98,7 +98,7 @@ public class Bars : Scenario Width = Dim.Fill () - Dim.Width (eventLog), Height = Dim.Percent (33), }; - Application.Top.Add (menuLikeExamples); + Application.Current.Add (menuLikeExamples); label = new Label () { @@ -212,7 +212,7 @@ public class Bars : Scenario Width = Dim.Width (menuLikeExamples), Height = Dim.Percent (33), }; - Application.Top.Add (statusBarLikeExamples); + Application.Current.Add (statusBarLikeExamples); label = new Label () { @@ -249,7 +249,7 @@ public class Bars : Scenario ConfigStatusBar (bar); statusBarLikeExamples.Add (bar); - foreach (FrameView frameView in Application.Top.SubViews.Where (f => f is FrameView)!) + foreach (FrameView frameView in Application.Current.SubViews.Where (f => f is FrameView)!) { foreach (Bar barView in frameView.SubViews.Where (b => b is Bar)!) { @@ -269,8 +269,8 @@ public class Bars : Scenario //private void SetupContentMenu () //{ - // Application.Top.Add (new Label { Text = "Right Click for Context Menu", X = Pos.Center (), Y = 4 }); - // Application.Top.MouseClick += ShowContextMenu; + // Application.Current.Add (new Label { Text = "Right Click for Context Menu", X = Pos.Center (), Y = 4 }); + // Application.Current.MouseClick += ShowContextMenu; //} //private void ShowContextMenu (object s, MouseEventEventArgs e) diff --git a/Examples/UICatalog/Scenarios/CombiningMarks.cs b/Examples/UICatalog/Scenarios/CombiningMarks.cs index 7d8437a23..47b8dfbc7 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.Top!.SetNeedsDraw (); + Application.Current!.SetNeedsDraw (); var i = -1; top.AddStr ("Terminal.Gui only supports combining marks that normalize. See Issue #2616."); @@ -58,9 +58,9 @@ public class CombiningMarks : Scenario top.Move (0, ++i); top.AddStr ("From now on we are using TextFormatter"); TextFormatter tf = new () { Text = "[e\u0301\u0301\u0328]<- \"[e\\u0301\\u0301\\u0328]\" using TextFormatter." }; - tf.Draw (new (0, ++i, tf.Text.Length, 1), top.GetAttributeForRole (VisualRole.Normal), top.GetAttributeForRole (VisualRole.Normal)); + tf.Draw (driver: Application.Driver, screen: new (0, ++i, tf.Text.Length, 1), normalColor: top.GetAttributeForRole (VisualRole.Normal), hotColor: top.GetAttributeForRole (VisualRole.Normal)); tf.Text = "[e\u0328\u0301]<- \"[e\\u0328\\u0301]\" using TextFormatter."; - tf.Draw (new (0, ++i, tf.Text.Length, 1), top.GetAttributeForRole (VisualRole.Normal), top.GetAttributeForRole (VisualRole.Normal)); + tf.Draw (driver: Application.Driver, screen: new (0, ++i, tf.Text.Length, 1), normalColor: top.GetAttributeForRole (VisualRole.Normal), hotColor: top.GetAttributeForRole (VisualRole.Normal)); i++; top.Move (0, ++i); top.AddStr ("From now on we are using Surrogate pairs with combining diacritics"); diff --git a/Examples/UICatalog/Scenarios/ConfigurationEditor.cs b/Examples/UICatalog/Scenarios/ConfigurationEditor.cs index a5beca9e9..36011a78c 100644 --- a/Examples/UICatalog/Scenarios/ConfigurationEditor.cs +++ b/Examples/UICatalog/Scenarios/ConfigurationEditor.cs @@ -75,7 +75,7 @@ public class ConfigurationEditor : Scenario void ConfigurationManagerOnApplied (object? sender, ConfigurationManagerEventArgs e) { - Application.Top?.SetNeedsDraw (); + Application.Current?.SetNeedsDraw (); } } public void Save () diff --git a/Examples/UICatalog/Scenarios/ContextMenus.cs b/Examples/UICatalog/Scenarios/ContextMenus.cs index 141392f29..bb3a7b19e 100644 --- a/Examples/UICatalog/Scenarios/ContextMenus.cs +++ b/Examples/UICatalog/Scenarios/ContextMenus.cs @@ -1,5 +1,7 @@ -using System.Globalization; +#nullable enable +using System.Globalization; using JetBrains.Annotations; +// ReSharper disable AccessToDisposedClosure namespace UICatalog.Scenarios; @@ -7,70 +9,33 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Menus")] public class ContextMenus : Scenario { - [CanBeNull] - private PopoverMenu _winContextMenu; - private TextField _tfTopLeft, _tfTopRight, _tfMiddle, _tfBottomLeft, _tfBottomRight; - private readonly List _cultureInfos = Application.SupportedCultures; + private PopoverMenu? _winContextMenu; + private TextField? _tfTopLeft, _tfTopRight, _tfMiddle, _tfBottomLeft, _tfBottomRight; + private readonly List? _cultureInfos = Application.SupportedCultures; private readonly Key _winContextMenuKey = Key.Space.WithCtrl; + private Window? _appWindow; + public override void Main () { // Init Application.Init (); // Setup - Create a top-level application window and configure it. - Window appWindow = new () + _appWindow = new () { Title = GetQuitKeyAndName (), Arrangement = ViewArrangement.Fixed, SchemeName = "Toplevel" }; - var text = "Context Menu"; - var width = 20; - - CreateWinContextMenu (); - - var label = new Label - { - X = Pos.Center (), Y = 1, Text = $"Press '{_winContextMenuKey}' to open the Window context menu." - }; - appWindow.Add (label); - - label = new () - { - X = Pos.Center (), - Y = Pos.Bottom (label), - Text = $"Press '{PopoverMenu.DefaultKey}' to open the TextField context menu." - }; - appWindow.Add (label); - - _tfTopLeft = new () { Id = "_tfTopLeft", Width = width, Text = text }; - appWindow.Add (_tfTopLeft); - - _tfTopRight = new () { Id = "_tfTopRight", X = Pos.AnchorEnd (width), Width = width, Text = text }; - appWindow.Add (_tfTopRight); - - _tfMiddle = new () { Id = "_tfMiddle", X = Pos.Center (), Y = Pos.Center (), Width = width, Text = text }; - appWindow.Add (_tfMiddle); - - _tfBottomLeft = new () { Id = "_tfBottomLeft", Y = Pos.AnchorEnd (1), Width = width, Text = text }; - appWindow.Add (_tfBottomLeft); - - _tfBottomRight = new () { Id = "_tfBottomRight", X = Pos.AnchorEnd (width), Y = Pos.AnchorEnd (1), Width = width, Text = text }; - appWindow.Add (_tfBottomRight); - - appWindow.KeyDown += OnAppWindowOnKeyDown; - appWindow.MouseClick += OnAppWindowOnMouseClick; - - CultureInfo originalCulture = Thread.CurrentThread.CurrentUICulture; - appWindow.Closed += (s, e) => { Thread.CurrentThread.CurrentUICulture = originalCulture; }; + _appWindow.Initialized += AppWindowOnInitialized; // Run - Start the application. - Application.Run (appWindow); - appWindow.Dispose (); - appWindow.KeyDown -= OnAppWindowOnKeyDown; - appWindow.MouseClick -= OnAppWindowOnMouseClick; + Application.Run (_appWindow); + _appWindow.Dispose (); + _appWindow.KeyDown -= OnAppWindowOnKeyDown; + _appWindow.MouseClick -= OnAppWindowOnMouseClick; _winContextMenu?.Dispose (); // Shutdown - Calling Application.Shutdown is required. @@ -78,7 +43,51 @@ public class ContextMenus : Scenario return; - void OnAppWindowOnMouseClick (object s, MouseEventArgs e) + void AppWindowOnInitialized (object? sender, EventArgs e) + { + + var text = "Context Menu"; + var width = 20; + + CreateWinContextMenu (); + + var label = new Label + { + X = Pos.Center (), Y = 1, Text = $"Press '{_winContextMenuKey}' to open the Window context menu." + }; + _appWindow.Add (label); + + label = new () + { + X = Pos.Center (), + Y = Pos.Bottom (label), + Text = $"Press '{PopoverMenu.DefaultKey}' to open the TextField context menu." + }; + _appWindow.Add (label); + + _tfTopLeft = new () { Id = "_tfTopLeft", Width = width, Text = text }; + _appWindow.Add (_tfTopLeft); + + _tfTopRight = new () { Id = "_tfTopRight", X = Pos.AnchorEnd (width), Width = width, Text = text }; + _appWindow.Add (_tfTopRight); + + _tfMiddle = new () { Id = "_tfMiddle", X = Pos.Center (), Y = Pos.Center (), Width = width, Text = text }; + _appWindow.Add (_tfMiddle); + + _tfBottomLeft = new () { Id = "_tfBottomLeft", Y = Pos.AnchorEnd (1), Width = width, Text = text }; + _appWindow.Add (_tfBottomLeft); + + _tfBottomRight = new () { Id = "_tfBottomRight", X = Pos.AnchorEnd (width), Y = Pos.AnchorEnd (1), Width = width, Text = text }; + _appWindow.Add (_tfBottomRight); + + _appWindow.KeyDown += OnAppWindowOnKeyDown; + _appWindow.MouseClick += OnAppWindowOnMouseClick; + + CultureInfo originalCulture = Thread.CurrentThread.CurrentUICulture; + _appWindow.Closed += (s, e) => { Thread.CurrentThread.CurrentUICulture = originalCulture; }; + } + + void OnAppWindowOnMouseClick (object? s, MouseEventArgs e) { if (e.Flags == MouseFlags.Button3Clicked) { @@ -88,7 +97,7 @@ public class ContextMenus : Scenario } } - void OnAppWindowOnKeyDown (object s, Key e) + void OnAppWindowOnKeyDown (object? s, Key e) { if (e == _winContextMenuKey) { @@ -101,12 +110,6 @@ public class ContextMenus : Scenario private void CreateWinContextMenu () { - if (_winContextMenu is { }) - { - _winContextMenu.Dispose (); - _winContextMenu = null; - } - _winContextMenu = new ( [ new MenuItemv2 @@ -171,6 +174,7 @@ public class ContextMenus : Scenario { Key = _winContextMenuKey }; + Application.Popover?.Register (_winContextMenu); } private Menuv2 GetSupportedCultureMenu () @@ -178,7 +182,7 @@ public class ContextMenus : Scenario List supportedCultures = []; int index = -1; - foreach (CultureInfo c in _cultureInfos) + foreach (CultureInfo c in _cultureInfos!) { MenuItemv2 culture = new (); diff --git a/Examples/UICatalog/Scenarios/CsvEditor.cs b/Examples/UICatalog/Scenarios/CsvEditor.cs index 609cb4e06..aca10fdac 100644 --- a/Examples/UICatalog/Scenarios/CsvEditor.cs +++ b/Examples/UICatalog/Scenarios/CsvEditor.cs @@ -502,7 +502,7 @@ public class CsvEditor : Scenario // Only set the current filename if we successfully loaded the entire file _currentFile = filename; _selectedCellTextField.SuperView.Enabled = true; - Application.Top.Title = $"{GetName ()} - {Path.GetFileName (_currentFile)}"; + Application.Current.Title = $"{GetName ()} - {Path.GetFileName (_currentFile)}"; } catch (Exception ex) { diff --git a/Examples/UICatalog/Scenarios/Images.cs b/Examples/UICatalog/Scenarios/Images.cs index 488f595a7..a9854092d 100644 --- a/Examples/UICatalog/Scenarios/Images.cs +++ b/Examples/UICatalog/Scenarios/Images.cs @@ -151,7 +151,7 @@ public class Images : Scenario _win.Add (_tabView); // Start trying to detect sixel support - var sixelSupportDetector = new SixelSupportDetector (); + var sixelSupportDetector = new SixelSupportDetector (Application.Driver); sixelSupportDetector.Detect (UpdateSixelSupportState); Application.Run (_win); diff --git a/Examples/UICatalog/Scenarios/Mazing.cs b/Examples/UICatalog/Scenarios/Mazing.cs index 01bbc41a4..935c72ae5 100644 --- a/Examples/UICatalog/Scenarios/Mazing.cs +++ b/Examples/UICatalog/Scenarios/Mazing.cs @@ -171,7 +171,7 @@ public class Mazing : Scenario if (_m.PlayerHp <= 0) { _message = "You died!"; - Application.Top!.SetNeedsDraw (); // trigger redraw + Application.Current!.SetNeedsDraw (); // trigger redraw _dead = true; return; // Stop further action if dead @@ -190,7 +190,7 @@ public class Mazing : Scenario _message = string.Empty; } - Application.Top!.SetNeedsDraw (); // trigger redraw + Application.Current!.SetNeedsDraw (); // trigger redraw } // Optional win condition: @@ -200,7 +200,7 @@ public class Mazing : Scenario _m = new (); // Generate a new maze _m.PlayerHp = hp; GenerateNpcs (); - Application.Top!.SetNeedsDraw (); // trigger redraw + Application.Current!.SetNeedsDraw (); // trigger redraw } } } diff --git a/Examples/UICatalog/Scenarios/Menus.cs b/Examples/UICatalog/Scenarios/Menus.cs index 70f67f6e2..755844715 100644 --- a/Examples/UICatalog/Scenarios/Menus.cs +++ b/Examples/UICatalog/Scenarios/Menus.cs @@ -121,7 +121,7 @@ public class Menus : Scenario Command.Cancel, ctx => { - if (Application.Popover?.GetActivePopover () as PopoverMenu is { Visible: true } visiblePopover) + if (App?.Popover?.GetActivePopover () as PopoverMenu is { Visible: true } visiblePopover) { visiblePopover.Visible = false; } diff --git a/Examples/UICatalog/Scenarios/Navigation.cs b/Examples/UICatalog/Scenarios/Navigation.cs index 7ce1d3317..94e7dad8e 100644 --- a/Examples/UICatalog/Scenarios/Navigation.cs +++ b/Examples/UICatalog/Scenarios/Navigation.cs @@ -219,7 +219,7 @@ public class Navigation : Scenario progressBar.Fraction += 0.01f; - Application.Invoke (() => { }); + Application.Invoke ((_) => { }); } void ColorPicker_ColorChanged (object sender, ResultEventArgs e) diff --git a/Examples/UICatalog/Scenarios/Notepad.cs b/Examples/UICatalog/Scenarios/Notepad.cs index 996702298..595604a7b 100644 --- a/Examples/UICatalog/Scenarios/Notepad.cs +++ b/Examples/UICatalog/Scenarios/Notepad.cs @@ -294,7 +294,7 @@ public class Notepad : Scenario // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused // and the context menu is disposed when it is closed. - Application.Popover?.Register (contextMenu); + tv.App!.Popover?.Register (contextMenu); contextMenu?.MakeVisible (e.MouseEvent.ScreenPosition); e.MouseEvent.Handled = true; diff --git a/Examples/UICatalog/Scenarios/Progress.cs b/Examples/UICatalog/Scenarios/Progress.cs index 4696c160c..0e3af66c4 100644 --- a/Examples/UICatalog/Scenarios/Progress.cs +++ b/Examples/UICatalog/Scenarios/Progress.cs @@ -43,7 +43,7 @@ public class Progress : Scenario { // Note the check for Mainloop being valid. System.Timers can run after they are Disposed. // This code must be defensive for that. - Application.Invoke (() => systemTimerDemo.Pulse ()); + Application.Invoke ((_) => systemTimerDemo.Pulse ()); }, null, 0, diff --git a/Examples/UICatalog/Scenarios/RegionScenario.cs b/Examples/UICatalog/Scenarios/RegionScenario.cs index dea5d5f0a..495d90d6a 100644 --- a/Examples/UICatalog/Scenarios/RegionScenario.cs +++ b/Examples/UICatalog/Scenarios/RegionScenario.cs @@ -24,31 +24,31 @@ public class RegionScenario : Scenario { Application.Init (); - Window app = new () + Window appWindow = new () { Title = GetQuitKeyAndName (), TabStop = TabBehavior.TabGroup }; - app.Padding!.Thickness = new (1); + appWindow.Padding!.Thickness = new (1); var tools = new ToolsView { Title = "Tools", X = Pos.AnchorEnd (), Y = 2 }; - tools.CurrentAttribute = app.GetAttributeForRole (VisualRole.HotNormal); + tools.CurrentAttribute = appWindow.GetAttributeForRole (VisualRole.HotNormal); tools.SetStyle += b => { _drawStyle = b; - app.SetNeedsDraw (); + appWindow.SetNeedsDraw (); }; tools.RegionOpChanged += (s, e) => { _regionOp = e; }; //tools.AddLayer += () => canvas.AddLayer (); - app.Add (tools); + appWindow.Add (tools); // Add drag handling to window - app.MouseEvent += (s, e) => + appWindow.MouseEvent += (s, e) => { if (e.Flags.HasFlag (MouseFlags.Button1Pressed)) { @@ -62,7 +62,7 @@ public class RegionScenario : Scenario // Drag if (_isDragging && _dragStart.HasValue) { - app.SetNeedsDraw (); + appWindow.SetNeedsDraw (); } } } @@ -77,31 +77,31 @@ public class RegionScenario : Scenario _dragStart = null; } - app.SetNeedsDraw (); + appWindow.SetNeedsDraw (); } }; // Draw the regions - app.DrawingContent += (s, e) => + appWindow.DrawingContent += (s, e) => { // Draw all regions with single line style //_region.FillRectangles (_attribute.Value, _fillRune); switch (_drawStyle) { case RegionDrawStyles.FillOnly: - _region.FillRectangles (tools.CurrentAttribute!.Value, _previewFillRune); + _region.FillRectangles (appWindow.App?.Driver, tools.CurrentAttribute!.Value, _previewFillRune); break; case RegionDrawStyles.InnerBoundaries: - _region.DrawBoundaries (app.LineCanvas, LineStyle.Single, tools.CurrentAttribute); - _region.FillRectangles (tools.CurrentAttribute!.Value, (Rune)' '); + _region.DrawBoundaries (appWindow.LineCanvas, LineStyle.Single, tools.CurrentAttribute); + _region.FillRectangles (appWindow.App?.Driver, tools.CurrentAttribute!.Value, (Rune)' '); break; case RegionDrawStyles.OuterBoundary: - _region.DrawOuterBoundary (app.LineCanvas, LineStyle.Single, tools.CurrentAttribute); - _region.FillRectangles (tools.CurrentAttribute!.Value, (Rune)' '); + _region.DrawOuterBoundary (appWindow.LineCanvas, LineStyle.Single, tools.CurrentAttribute); + _region.FillRectangles (appWindow.App?.Driver, tools.CurrentAttribute!.Value, (Rune)' '); break; } @@ -109,14 +109,14 @@ public class RegionScenario : Scenario // If currently dragging, draw preview rectangle if (_isDragging && _dragStart.HasValue) { - Point currentMousePos = Application.GetLastMousePosition ()!.Value; + Point currentMousePos = appWindow.App!.Mouse.LastMousePosition!.Value; Rectangle previewRect = GetRectFromPoints (_dragStart.Value, currentMousePos); var previewRegion = new Region (previewRect); - previewRegion.FillRectangles (tools.CurrentAttribute!.Value, (Rune)' '); + previewRegion.FillRectangles (appWindow.App.Driver, tools.CurrentAttribute!.Value, (Rune)' '); previewRegion.DrawBoundaries ( - app.LineCanvas, + appWindow.LineCanvas, LineStyle.Dashed, new ( tools.CurrentAttribute!.Value.Foreground.GetBrighterColor (), @@ -124,10 +124,10 @@ public class RegionScenario : Scenario } }; - Application.Run (app); + Application.Run (appWindow); // Clean up - app.Dispose (); + appWindow.Dispose (); Application.Shutdown (); } diff --git a/Examples/UICatalog/Scenarios/Shortcuts.cs b/Examples/UICatalog/Scenarios/Shortcuts.cs index 712b69acf..331ed0337 100644 --- a/Examples/UICatalog/Scenarios/Shortcuts.cs +++ b/Examples/UICatalog/Scenarios/Shortcuts.cs @@ -28,7 +28,7 @@ public class Shortcuts : Scenario private void App_Loaded (object? sender, EventArgs e) { Application.QuitKey = Key.F4.WithCtrl; - Application.Top!.Title = GetQuitKeyAndName (); + Application.Current!.Title = GetQuitKeyAndName (); ObservableCollection eventSource = new (); @@ -46,14 +46,14 @@ public class Shortcuts : Scenario eventLog.Width = Dim.Func ( _ => Math.Min ( - Application.Top.Viewport.Width / 2, + Application.Current.Viewport.Width / 2, eventLog?.MaxLength + eventLog!.GetAdornmentsThickness ().Horizontal ?? 0)); eventLog.Width = Dim.Func ( _ => Math.Min ( eventLog.SuperView!.Viewport.Width / 2, eventLog?.MaxLength + eventLog!.GetAdornmentsThickness ().Horizontal ?? 0)); - Application.Top.Add (eventLog); + Application.Current.Add (eventLog); var alignKeysShortcut = new Shortcut { @@ -86,7 +86,7 @@ public class Shortcuts : Scenario }; - Application.Top.Add (alignKeysShortcut); + Application.Current.Add (alignKeysShortcut); var commandFirstShortcut = new Shortcut { @@ -115,7 +115,7 @@ public class Shortcuts : Scenario $"{commandFirstShortcut.Id}.CommandView.CheckedStateChanging: {cb.Text}"); eventLog.MoveDown (); - IEnumerable toAlign = Application.Top.SubViews.OfType (); + IEnumerable toAlign = Application.Current.SubViews.OfType (); IEnumerable enumerable = toAlign as View [] ?? toAlign.ToArray (); foreach (View view in enumerable) @@ -134,7 +134,7 @@ public class Shortcuts : Scenario } }; - Application.Top.Add (commandFirstShortcut); + Application.Current.Add (commandFirstShortcut); var canFocusShortcut = new Shortcut { @@ -159,7 +159,7 @@ public class Shortcuts : Scenario SetCanFocus (e.Result == CheckState.Checked); } }; - Application.Top.Add (canFocusShortcut); + Application.Current.Add (canFocusShortcut); var appShortcut = new Shortcut { @@ -173,7 +173,7 @@ public class Shortcuts : Scenario BindKeyToApplication = true }; - Application.Top.Add (appShortcut); + Application.Current.Add (appShortcut); var buttonShortcut = new Shortcut { @@ -193,7 +193,7 @@ public class Shortcuts : Scenario var button = (Button)buttonShortcut.CommandView; buttonShortcut.Accepting += Button_Clicked; - Application.Top.Add (buttonShortcut); + Application.Current.Add (buttonShortcut); var optionSelectorShortcut = new Shortcut { @@ -221,7 +221,7 @@ public class Shortcuts : Scenario } }; - Application.Top.Add (optionSelectorShortcut); + Application.Current.Add (optionSelectorShortcut); var sliderShortcut = new Shortcut { @@ -248,7 +248,7 @@ public class Shortcuts : Scenario eventLog.MoveDown (); }; - Application.Top.Add (sliderShortcut); + Application.Current.Add (sliderShortcut); ListView listView = new ListView () { @@ -270,7 +270,7 @@ public class Shortcuts : Scenario Key = Key.F5.WithCtrl, }; - Application.Top.Add (listViewShortcut); + Application.Current.Add (listViewShortcut); var noCommandShortcut = new Shortcut { @@ -282,7 +282,7 @@ public class Shortcuts : Scenario Key = Key.D0 }; - Application.Top.Add (noCommandShortcut); + Application.Current.Add (noCommandShortcut); var noKeyShortcut = new Shortcut { @@ -295,7 +295,7 @@ public class Shortcuts : Scenario HelpText = "Keyless" }; - Application.Top.Add (noKeyShortcut); + Application.Current.Add (noKeyShortcut); var noHelpShortcut = new Shortcut { @@ -308,7 +308,7 @@ public class Shortcuts : Scenario HelpText = "" }; - Application.Top.Add (noHelpShortcut); + Application.Current.Add (noHelpShortcut); noHelpShortcut.SetFocus (); var framedShortcut = new Shortcut @@ -340,7 +340,7 @@ public class Shortcuts : Scenario } framedShortcut.SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Toplevel); - Application.Top.Add (framedShortcut); + Application.Current.Add (framedShortcut); // Horizontal var progressShortcut = new Shortcut @@ -387,7 +387,7 @@ public class Shortcuts : Scenario }; timer.Start (); - Application.Top.Add (progressShortcut); + Application.Current.Add (progressShortcut); var textField = new TextField { @@ -408,7 +408,7 @@ public class Shortcuts : Scenario }; textField.CanFocus = true; - Application.Top.Add (textFieldShortcut); + Application.Current.Add (textFieldShortcut); var bgColorShortcut = new Shortcut { @@ -450,19 +450,19 @@ public class Shortcuts : Scenario eventSource.Add ($"ColorChanged: {o.GetType ().Name} - {args.Result}"); eventLog.MoveDown (); - Application.Top.SetScheme ( - new (Application.Top.GetScheme ()) + Application.Current.SetScheme ( + new (Application.Current.GetScheme ()) { Normal = new ( - Application.Top!.GetAttributeForRole (VisualRole.Normal).Foreground, + Application.Current!.GetAttributeForRole (VisualRole.Normal).Foreground, args.Result, - Application.Top!.GetAttributeForRole (VisualRole.Normal).Style) + Application.Current!.GetAttributeForRole (VisualRole.Normal).Style) }); } }; bgColorShortcut.CommandView = bgColor; - Application.Top.Add (bgColorShortcut); + Application.Current.Add (bgColorShortcut); var appQuitShortcut = new Shortcut { @@ -476,9 +476,9 @@ public class Shortcuts : Scenario }; appQuitShortcut.Accepting += (o, args) => { Application.RequestStop (); }; - Application.Top.Add (appQuitShortcut); + Application.Current.Add (appQuitShortcut); - foreach (Shortcut shortcut in Application.Top.SubViews.OfType ()) + foreach (Shortcut shortcut in Application.Current.SubViews.OfType ()) { shortcut.Selecting += (o, args) => { @@ -529,7 +529,7 @@ public class Shortcuts : Scenario void SetCanFocus (bool canFocus) { - foreach (Shortcut peer in Application.Top!.SubViews.OfType ()) + foreach (Shortcut peer in Application.Current!.SubViews.OfType ()) { if (peer.CanFocus) { @@ -542,7 +542,7 @@ public class Shortcuts : Scenario { var max = 0; - IEnumerable toAlign = Application.Top!.SubViews.OfType ().Where(s => !s.Y.Has(out _)).Cast(); + IEnumerable toAlign = Application.Current!.SubViews.OfType ().Where(s => !s.Y.Has(out _)).Cast(); IEnumerable enumerable = toAlign as Shortcut [] ?? toAlign.ToArray (); if (align) diff --git a/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs b/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs index 62c35ac40..571597f8c 100644 --- a/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs +++ b/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs @@ -179,9 +179,9 @@ public class SingleBackgroundWorker : Scenario var builderUI = new StagingUIController (_startStaging, e.Result as ObservableCollection); - Toplevel top = Application.Top; + Toplevel top = Application.Current; top.Visible = false; - Application.Top.Visible = false; + Application.Current.Visible = false; builderUI.Load (); builderUI.Dispose (); top.Visible = true; diff --git a/Examples/UICatalog/Scenarios/TableEditor.cs b/Examples/UICatalog/Scenarios/TableEditor.cs index 84d7644b9..57b1f9a60 100644 --- a/Examples/UICatalog/Scenarios/TableEditor.cs +++ b/Examples/UICatalog/Scenarios/TableEditor.cs @@ -1363,7 +1363,7 @@ public class TableEditor : Scenario // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused // and the context menu is disposed when it is closed. - Application.Popover?.Register (contextMenu); + e.View?.App!.Popover?.Register (contextMenu); contextMenu?.MakeVisible (new (e.ScreenPosition.X + 1, e.ScreenPosition.Y + 1)); } diff --git a/Examples/UICatalog/Scenarios/Themes.cs b/Examples/UICatalog/Scenarios/Themes.cs index 6e4d676ea..d515b5ee0 100644 --- a/Examples/UICatalog/Scenarios/Themes.cs +++ b/Examples/UICatalog/Scenarios/Themes.cs @@ -129,7 +129,7 @@ public sealed class Themes : Scenario { if (_view is { }) { - Application.Top!.SchemeName = args.NewValue; + Application.Current!.SchemeName = args.NewValue; if (_view.HasScheme) { @@ -160,11 +160,11 @@ public sealed class Themes : Scenario TabStop = TabBehavior.TabStop }; - allViewsView.FocusedChanged += (s, args) => + allViewsView.FocusedChanged += (s, a) => { allViewsView.Title = - $"All Views - Focused: {args.NewFocused.Title}"; - viewPropertiesEditor.ViewToEdit = args.NewFocused.SubViews.ElementAt(0); + $"All Views - Focused: {a.NewFocused?.Title}"; + viewPropertiesEditor.ViewToEdit = a.NewFocused?.SubViews.ElementAt(0); }; appWindow.Add (allViewsView); diff --git a/Examples/UICatalog/Scenarios/TreeUseCases.cs b/Examples/UICatalog/Scenarios/TreeUseCases.cs index 4603b9e54..d9ee48d15 100644 --- a/Examples/UICatalog/Scenarios/TreeUseCases.cs +++ b/Examples/UICatalog/Scenarios/TreeUseCases.cs @@ -77,7 +77,7 @@ public class TreeUseCases : Scenario if (_currentTree != null) { - Application.Top.Remove (_currentTree); + Application.Current.Remove (_currentTree); _currentTree.Dispose (); } @@ -97,7 +97,7 @@ public class TreeUseCases : Scenario tree.TreeBuilder = new GameObjectTreeBuilder (); } - Application.Top.Add (tree); + Application.Current.Add (tree); tree.AddObject (army1); @@ -117,13 +117,13 @@ public class TreeUseCases : Scenario if (_currentTree != null) { - Application.Top.Remove (_currentTree); + Application.Current.Remove (_currentTree); _currentTree.Dispose (); } var tree = new TreeView { X = 0, Y = 1, Width = Dim.Fill(), Height = Dim.Fill (1) }; - Application.Top.Add (tree); + Application.Current.Add (tree); tree.AddObject (myHouse); @@ -134,13 +134,13 @@ public class TreeUseCases : Scenario { if (_currentTree != null) { - Application.Top.Remove (_currentTree); + Application.Current.Remove (_currentTree); _currentTree.Dispose (); } var tree = new TreeView { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1) }; - Application.Top.Add (tree); + Application.Current.Add (tree); var root1 = new TreeNode ("Root1"); root1.Children.Add (new TreeNode ("Child1.1")); diff --git a/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs b/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs index 34540d5cc..eeb4f8b78 100644 --- a/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs +++ b/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs @@ -418,7 +418,7 @@ public class TreeViewFileSystem : Scenario // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused // and the context menu is disposed when it is closed. - Application.Popover?.Register (contextMenu); + _detailsFrame.App?.Popover?.Register (contextMenu); Application.Invoke (() => contextMenu?.MakeVisible (screenPoint)); } diff --git a/Examples/UICatalog/Scenarios/ViewExperiments.cs b/Examples/UICatalog/Scenarios/ViewExperiments.cs index b7abf8588..c36fce3e7 100644 --- a/Examples/UICatalog/Scenarios/ViewExperiments.cs +++ b/Examples/UICatalog/Scenarios/ViewExperiments.cs @@ -75,15 +75,15 @@ public class ViewExperiments : Scenario Y = Pos.Center (), Title = $"_Close", }; - //popoverButton.Accepting += (sender, e) => Application.Popover!.Visible = false; + //popoverButton.Accepting += (sender, e) => App?.Popover!.Visible = false; popoverView.Add (popoverButton); button.Accepting += ButtonAccepting; void ButtonAccepting (object sender, CommandEventArgs e) { - //Application.Popover = popoverView; - //Application.Popover!.Visible = true; + //App?.Popover = popoverView; + //App?.Popover!.Visible = true; } testFrame.MouseClick += TestFrameOnMouseClick; @@ -94,8 +94,8 @@ public class ViewExperiments : Scenario { popoverView.X = e.ScreenPosition.X; popoverView.Y = e.ScreenPosition.Y; - //Application.Popover = popoverView; - //Application.Popover!.Visible = true; + //App?.Popover = popoverView; + //App?.Popover!.Visible = true; } } diff --git a/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs b/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs index fcf57343a..afe039f1c 100644 --- a/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs +++ b/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs @@ -69,7 +69,7 @@ public class WindowsAndFrameViews : Scenario // add it to our list listWin.Add (win); - // create 3 more Windows in a loop, adding them Application.Top + // create 3 more Windows in a loop, adding them Application.Current // Each with a // button // sub Window with diff --git a/Examples/UICatalog/UICatalog.cs b/Examples/UICatalog/UICatalog.cs index 6f7a37139..446057259 100644 --- a/Examples/UICatalog/UICatalog.cs +++ b/Examples/UICatalog/UICatalog.cs @@ -242,7 +242,7 @@ public class UICatalog /// /// Shows the UI Catalog selection UI. When the user selects a Scenario to run, the UI Catalog main app UI is - /// killed and the Scenario is run as though it were Application.Top. When the Scenario exits, this function exits. + /// killed and the Scenario is run as though it were Application.Current. When the Scenario exits, this function exits. /// /// private static Scenario RunUICatalogTopLevel () @@ -269,7 +269,7 @@ public class UICatalog [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] private static readonly FileSystemWatcher _homeDirWatcher = new (); - private static void StartConfigFileWatcher () + private static void StartConfigWatcher () { // Set up a file system watcher for `./.tui/` _currentDirWatcher.NotifyFilter = NotifyFilters.LastWrite; @@ -317,10 +317,19 @@ public class UICatalog //_homeDirWatcher.Created += ConfigFileChanged; _homeDirWatcher.EnableRaisingEvents = true; + + ThemeManager.ThemeChanged += ThemeManagerOnThemeChanged; } - private static void StopConfigFileWatcher () + private static void ThemeManagerOnThemeChanged (object? sender, EventArgs e) { + CM.Apply (); + } + + private static void StopConfigWatcher () + { + ThemeManager.ThemeChanged += ThemeManagerOnThemeChanged; + _currentDirWatcher.EnableRaisingEvents = false; _currentDirWatcher.Changed -= ConfigFileChanged; _currentDirWatcher.Created -= ConfigFileChanged; @@ -332,7 +341,7 @@ public class UICatalog private static void ConfigFileChanged (object sender, FileSystemEventArgs e) { - if (Application.Top == null) + if (Application.Current == null) { return; } @@ -398,7 +407,7 @@ public class UICatalog if (!Options.DontEnableConfigurationManagement) { ConfigurationManager.Enable (ConfigLocations.All); - StartConfigFileWatcher (); + StartConfigWatcher (); } while (RunUICatalogTopLevel () is { } scenario) @@ -440,7 +449,7 @@ public class UICatalog #endif } - StopConfigFileWatcher (); + StopConfigWatcher (); VerifyObjectsWereDisposed (); } diff --git a/Examples/UICatalog/UICatalog.csproj b/Examples/UICatalog/UICatalog.csproj index 0b529c499..88e916f9c 100644 --- a/Examples/UICatalog/UICatalog.csproj +++ b/Examples/UICatalog/UICatalog.csproj @@ -49,4 +49,5 @@ + \ No newline at end of file diff --git a/Examples/UICatalog/UICatalogTop.cs b/Examples/UICatalog/UICatalogTop.cs index afec138a8..88a5e4144 100644 --- a/Examples/UICatalog/UICatalogTop.cs +++ b/Examples/UICatalog/UICatalogTop.cs @@ -211,6 +211,7 @@ public class UICatalogTop : Toplevel return; } ThemeManager.Theme = ThemeManager.GetThemeNames () [(int)args.Value]; + }; var menuItem = new MenuItemv2 @@ -691,7 +692,7 @@ public class UICatalogTop : Toplevel _disableMouseCb!.CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked; _force16ColorsShortcutCb!.CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked; - Application.Top?.SetNeedsDraw (); + Application.Current?.SetNeedsDraw (); } private void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a) { ConfigApplied (); } diff --git a/NULLABLE_VIEWS_REMAINING.md b/NULLABLE_VIEWS_REMAINING.md new file mode 100644 index 000000000..220e6933f --- /dev/null +++ b/NULLABLE_VIEWS_REMAINING.md @@ -0,0 +1,163 @@ +# View Subclasses Still With `#nullable disable` + +This document lists all View-related files in the `/Views` directory that still have `#nullable disable` set. + +**Total**: 121 files + +## Breakdown by Subdirectory + +### Autocomplete (8 files) +- Autocomplete/AppendAutocomplete.cs +- Autocomplete/AutocompleteBase.cs +- Autocomplete/AutocompleteContext.cs +- Autocomplete/AutocompleteFilepathContext.cs +- Autocomplete/IAutocomplete.cs +- Autocomplete/ISuggestionGenerator.cs +- Autocomplete/SingleWordSuggestionGenerator.cs +- Autocomplete/Suggestion.cs + +### CollectionNavigation (7 files) +- CollectionNavigation/CollectionNavigator.cs +- CollectionNavigation/CollectionNavigatorBase.cs +- CollectionNavigation/DefaultCollectionNavigatorMatcher.cs +- CollectionNavigation/ICollectionNavigator.cs +- CollectionNavigation/ICollectionNavigatorMatcher.cs +- CollectionNavigation/IListCollectionNavigator.cs +- CollectionNavigation/TableCollectionNavigator.cs + +### Color/ColorPicker (13 files) +- Color/BBar.cs +- Color/ColorBar.cs +- Color/ColorModelStrategy.cs +- Color/ColorPicker.16.cs +- Color/ColorPicker.Prompt.cs +- Color/ColorPicker.Style.cs +- Color/ColorPicker.cs +- Color/GBar.cs +- Color/HueBar.cs +- Color/IColorBar.cs +- Color/LightnessBar.cs +- Color/RBar.cs +- Color/SaturationBar.cs +- Color/ValueBar.cs + +### FileDialogs (10 files) +- FileDialogs/AllowedType.cs +- FileDialogs/DefaultFileOperations.cs +- FileDialogs/FileDialogCollectionNavigator.cs +- FileDialogs/FileDialogHistory.cs +- FileDialogs/FileDialogState.cs +- FileDialogs/FileDialogStyle.cs +- FileDialogs/FileDialogTableSource.cs +- FileDialogs/FilesSelectedEventArgs.cs +- FileDialogs/OpenDialog.cs +- FileDialogs/OpenMode.cs +- FileDialogs/SaveDialog.cs + +### GraphView (9 files) +- GraphView/Axis.cs +- GraphView/BarSeriesBar.cs +- GraphView/GraphCellToRender.cs +- GraphView/GraphView.cs +- GraphView/IAnnotation.cs +- GraphView/LegendAnnotation.cs +- GraphView/LineF.cs +- GraphView/PathAnnotation.cs +- GraphView/TextAnnotation.cs + +### Menu (3 files) +- Menu/MenuBarv2.cs +- Menu/Menuv2.cs +- Menu/PopoverMenu.cs + +### Menuv1 (4 files) +- Menuv1/MenuClosingEventArgs.cs +- Menuv1/MenuItemCheckStyle.cs +- Menuv1/MenuOpenedEventArgs.cs +- Menuv1/MenuOpeningEventArgs.cs + +### ScrollBar (2 files) +- ScrollBar/ScrollBar.cs +- ScrollBar/ScrollSlider.cs + +### Selectors (2 files) +- Selectors/FlagSelector.cs +- Selectors/SelectorStyles.cs + +### Slider (9 files) +- Slider/Slider.cs +- Slider/SliderAttributes.cs +- Slider/SliderConfiguration.cs +- Slider/SliderEventArgs.cs +- Slider/SliderOption.cs +- Slider/SliderOptionEventArgs.cs +- Slider/SliderStyle.cs +- Slider/SliderType.cs + +### SpinnerView (2 files) +- SpinnerView/SpinnerStyle.cs +- SpinnerView/SpinnerView.cs + +### TabView (4 files) +- TabView/Tab.cs +- TabView/TabChangedEventArgs.cs +- TabView/TabMouseEventArgs.cs +- TabView/TabStyle.cs + +### TableView (18 files) +- TableView/CellActivatedEventArgs.cs +- TableView/CellColorGetterArgs.cs +- TableView/CellToggledEventArgs.cs +- TableView/CheckBoxTableSourceWrapper.cs +- TableView/CheckBoxTableSourceWrapperByIndex.cs +- TableView/CheckBoxTableSourceWrapperByObject.cs +- TableView/ColumnStyle.cs +- TableView/DataTableSource.cs +- TableView/EnumerableTableSource.cs +- TableView/IEnumerableTableSource.cs +- TableView/ITableSource.cs +- TableView/ListColumnStyle.cs +- TableView/ListTableSource.cs +- TableView/RowColorGetterArgs.cs +- TableView/SelectedCellChangedEventArgs.cs +- TableView/TableSelection.cs +- TableView/TableStyle.cs +- TableView/TableView.cs +- TableView/TreeTableSource.cs + +### TextInput (11 files) +- TextInput/ContentsChangedEventArgs.cs +- TextInput/DateField.cs +- TextInput/HistoryTextItemEventArgs.cs +- TextInput/ITextValidateProvider.cs +- TextInput/NetMaskedTextProvider.cs +- TextInput/TextEditingLineStatus.cs +- TextInput/TextField.cs +- TextInput/TextRegexProvider.cs +- TextInput/TextValidateField.cs +- TextInput/TimeField.cs + +### TreeView (14 files) +- TreeView/AspectGetterDelegate.cs +- TreeView/Branch.cs +- TreeView/DelegateTreeBuilder.cs +- TreeView/DrawTreeViewLineEventArgs.cs +- TreeView/ITreeBuilder.cs +- TreeView/ITreeViewFilter.cs +- TreeView/ObjectActivatedEventArgs.cs +- TreeView/SelectionChangedEventArgs.cs +- TreeView/TreeBuilder.cs +- TreeView/TreeNode.cs +- TreeView/TreeNodeBuilder.cs +- TreeView/TreeStyle.cs +- TreeView/TreeView.cs +- TreeView/TreeViewTextFilter.cs + +### Wizard (3 files) +- Wizard/Wizard.cs +- Wizard/WizardEventArgs.cs +- Wizard/WizardStep.cs + +## Summary + +These 121 View-related files still have `#nullable disable` as they require additional work to be fully nullable-compliant. All other files in the Terminal.Gui library (outside of the Views directory) have been updated to support nullable reference types. diff --git a/PR_DESCRIPTION_UPDATED.md b/PR_DESCRIPTION_UPDATED.md new file mode 100644 index 000000000..8a653ba68 --- /dev/null +++ b/PR_DESCRIPTION_UPDATED.md @@ -0,0 +1,322 @@ +# Fixes #4329 - Major Architectural Improvements: API Rename, Nullable Types, and Application Decoupling + +## Overview + +This PR delivers **three major architectural improvements** to Terminal.Gui v2: + +1. **API Terminology Modernization** - Renamed confusing `Application.Top`/`TopLevels` to intuitive `Application.Current`/`Session Stack` +2. **Nullable Reference Types** - Enabled nullable for 143 non-View library files +3. **Application Decoupling** - Introduced `View.App` property to decouple View hierarchy from static Application class + +**Impact**: 561 files changed, 7,033 insertions(+), 2,736 deletions(-) across library, tests, and examples. + +--- + +## Part 1: API Terminology Modernization (Breaking Change) + +### Changes + +- **`Application.Top` → `Application.Current`** (684 occurrences across codebase) +- **`Application.TopLevels` → `Application.SessionStack`** (31 occurrences) +- Updated `IApplication` interface, `ApplicationImpl`, all tests, examples, and documentation + +### Rationale + +The old naming was ambiguous and inconsistent with .NET patterns: +- `Top` didn't clearly indicate "currently active/running view" +- `TopLevels` exposed implementation detail (it's a stack!) and didn't match `SessionToken` terminology + +New naming follows established patterns: +- `Current` matches `Thread.CurrentThread`, `HttpContext.Current`, `Synchronization Context.Current` +- `SessionStack` clearly describes both content (sessions) and structure (stack), aligning with `SessionToken` + +### Impact Statistics + +| Category | Files Changed | Occurrences Updated | +|----------|---------------|---------------------| +| Terminal.Gui library | 41 | 715 | +| Unit tests | 43 | 631 | +| Integration tests | 3 | 25 | +| Examples | 15 | 15 | +| Documentation | 3 | 14 | +| **Total** | **91** | **~800** | + +###Breaking Changes + +**All references must be updated:** +```csharp +// OLD (v1/early v2) +Application.Top?.SetNeedsDraw(); +foreach (var tl in Application.TopLevels) { } + +// NEW (v2 current) +Application.Current?.SetNeedsDraw(); +foreach (var tl in Application.SessionStack) { } +``` + +--- + +## Part 2: Nullable Reference Types Enabled + +### Changes + +**Phase 1** - Project Configuration (commit 439e161): +- Added `enable` to `Terminal.Gui.csproj` (project-wide default) +- Removed redundant `#nullable enable` from 37 files +- Added `#nullable disable` to 170 files not yet compliant + +**Phase 2** - Non-View Compliance (commit 06bd50d): +- **Removed `#nullable disable` from ALL 143 non-View library files** +- Build successful with 0 errors +- All core infrastructure now fully nullable-aware + +**Phase 3** - Cleanup (commits 97d9c7d, 49d4fb2): +- Fixed duplicate `#nullable` directives in 37 files +- All files now have clean, single nullable directive + +### Impact Statistics + +| Directory | Files Nullable-Enabled | +|-----------|------------------------| +| App/ | 25 ✅ | +| Configuration/ | 24 ✅ | +| ViewBase/ | 30 ✅ | +| Drivers/ | 25 ✅ | +| Drawing/ | 18 ✅ | +| FileServices/ | 7 ✅ | +| Input/ | 6 ✅ | +| Text/ | 5 ✅ | +| Resources/ | 3 ✅ | +| **Views/** | **121 ⏸️ (documented in NULLABLE_VIEWS_REMAINING.md)** | +| **Total Enabled** | **143 files** | + +### Remaining Work + +See [NULLABLE_VIEWS_REMAINING.md](./NULLABLE_VIEWS_REMAINING.md) for the 121 View subclass files still with `#nullable disable`. These require careful migration due to complex view hierarchies and will be addressed in a follow-up PR. + +--- + +## Part 3: Application Decoupling (MASSIVE Change) + +### Problem + +Prior to this PR, Views were tightly coupled to the **static** `Application` class: +- Direct static calls: `Application.Current`, `Application.Driver`, `Application.MainLoop` +- Made Views untestable in isolation +- Violated dependency inversion principle +- Prevented Views from working with different IApplication implementations + +### Solution: `View.App` Property + +Introduced `View.App` property that provides IApplication instance: + +```csharp +// Terminal.Gui/ViewBase/View.cs +public IApplication? App +{ + get => GetApp(); + internal set => _app = value; +} + +private IApplication? GetApp() +{ + // Walk up hierarchy to find IApplication + if (_app is { }) return _app; + if (SuperView is { }) return SuperView.App; + return Application.Instance; // Fallback to global +} +``` + +### Migration Pattern + +**Before** (tightly coupled): +```csharp +// Direct static dependency +Application.Driver.Move(x, y); +if (Application.Current == this) { } +Application.MainLoop.Invoke(() => { }); +``` + +**After** (decoupled via View.App): +```csharp +// Use injected IApplication instance +App?.Driver.Move(x, y); +if (App?.Current == this) { } +App?.MainLoop.Invoke(() => { }); +``` + +### Impact Statistics + +- **90 files changed** in decoupling commit (899fd76) +- **987 insertions, 728 deletions** +- Affects ViewBase, Views, Adornments, Input handling, Drawing + +### Benefits + +✅ **Testability**: Views can now be tested with mock IApplication +✅ **Flexibility**: Views work with any IApplication implementation +✅ **Cleaner Architecture**: Follows dependency injection pattern +✅ **Future-proof**: Enables multi-application scenarios +✅ **Maintainability**: Clearer dependencies, easier to refactor + +### Known Remaining Coupling + +After decoupling work, only **1 direct Application dependency** remains in ViewBase: +- `Border.Arrangement.cs`: Uses `Application.ArrangeKey` for hotkey binding + +Additional investigation areas for future work: +1. Some Views still reference Application for convenience (non-critical) +2. Test infrastructure may have residual static dependencies +3. Example applications use Application.Run (expected pattern) + +--- + +## Part 4: Test Infrastructure Improvements + +### New Test File: `ApplicationImplBeginEndTests.cs` + +Added **16 comprehensive tests** validating fragile Begin/End state management: + +**Critical Test Coverage:** +- `End_ThrowsArgumentException_WhenNotBalanced` - Ensures proper Begin/End pairing +- `End_RestoresCurrentToPreviousToplevel` - Validates Current property management +- `MultipleBeginEnd_MaintainsStackIntegrity` - Tests nested sessions (5 levels deep) + +**Additional Coverage:** +- Argument validation (null checks) +- SessionStack push/pop operations +- Current property state transitions +- Unique ID generation for toplevels +- SessionToken management +- ResetState cleanup behavior +- Toplevel activation/deactivation events + +### Test Quality Improvements + +All new tests follow best practices: +- Work directly with ApplicationImpl instances (no global Application pollution) +- Use try-finally blocks ensuring Shutdown() always called +- Properly dispose toplevels before Shutdown (satisfies DEBUG_IDISPOSABLE assertions) +- No redundant ResetState calls (Shutdown calls it internally) + +**Result**: All 16 new tests + all existing tests passing ✅ + +--- + +## Additional Changes + +### Merged from v2_develop + +- RunState → SessionToken terminology (precedent for this rename) +- Application.TopLevels visibility changed to public (made this rename more important) +- Legacy MainLoop infrastructure removed +- Driver architecture modernization +- Test infrastructure improvements + +### Documentation + +- Created 5 comprehensive terminology proposal documents in `docfx/docs/`: + - `terminology-index.md` - Navigation guide + - `terminology-proposal.md` - Complete analysis + - `terminology-proposal-summary.md` - Quick reference + - `terminology-diagrams.md` - 11 Mermaid diagrams + - `terminology-before-after.md` - Side-by-side examples +- Updated `navigation.md`, `config.md`, `migratingfromv1.md` +- Created `NULLABLE_VIEWS_REMAINING.md` - Tracks remaining nullable work + +--- + +## Testing + +- ✅ **Build**: Successful with 0 errors +- ✅ **Unit Tests**: All 16 new tests + all existing tests passing +- ✅ **Integration Tests**: Updated and passing +- ✅ **Examples**: UICatalog, ReactiveExample, CommunityToolkitExample all updated and functional +- ✅ **Documentation**: Builds successfully + +--- + +## Breaking Changes Summary + +### API Changes (Requires Code Updates) + +1. **`Application.Top` → `Application.Current`** + - All usages must be updated + - Affects any code accessing the currently running toplevel + +2. **`Application.TopLevels` → `Application.SessionStack`** + - All usages must be updated + - Affects code iterating over running sessions + +### Non-Breaking Changes + +- Nullable reference types: Improved type safety, no runtime changes +- View.App property: Additive, existing Application. * calls still work (for now) + +--- + +## Migration Guide + +### For Terminology Changes + +```bash +# Find and replace in your codebase +Application.Top → Application.Current +Application.TopLevels → Application.SessionStack +``` + +### For View.App Usage (Recommended, Not Required) + +When writing new View code or refactoring existing Views: + +```csharp +// Prefer (future-proof, testable) +App?.Driver.AddRune(rune); +if (App?.Current == this) { } + +// Over (works but tightly coupled) +Application.Driver.AddRune(rune); +if (Application.Current == this) { } +``` + +--- + +## Future Work + +### Nullable Types +- Enable nullable for remaining 121 View files +- Document nullable patterns for View subclass authors + +### Application Decoupling +- Remove last `Application.ArrangeKey` reference from Border +- Consider making View.App property public for advanced scenarios +- Add documentation on using View.App for testable Views + +### Tests +- Expand ApplicationImpl test coverage based on new patterns discovered +- Add tests for View.App hierarchy traversal + +--- + +## Pull Request Checklist + +- [x] I've named my PR in the form of "Fixes #issue. Terse description." +- [x] My code follows the style guidelines of Terminal.Gui +- [x] My code follows the Terminal.Gui library design guidelines +- [x] I ran `dotnet test` before commit +- [x] I have made corresponding changes to the API documentation +- [x] My changes generate no new warnings +- [x] I have checked my code and corrected any poor grammar or misspellings +- [x] I conducted basic QA to assure all features are working + +--- + +## Related Issues + +- Fixes #4329 - Rename/Clarify Application.Toplevels/Top Terminology +- Related to #2491 - Toplevel refactoring +- Fixes #4333 (duplicate/related issue) + +--- + +**Note**: This is a large, multi-faceted PR that delivers significant architectural improvements. The changes are well-tested and maintain backward compatibility except for the intentional breaking API rename. The work positions Terminal.Gui v2 for better testability, maintainability, and future enhancements. diff --git a/Terminal.Gui/App/Application.Current.cs b/Terminal.Gui/App/Application.Current.cs new file mode 100644 index 000000000..e94d074e7 --- /dev/null +++ b/Terminal.Gui/App/Application.Current.cs @@ -0,0 +1,18 @@ +using System.Collections.Concurrent; + +namespace Terminal.Gui.App; + +public static partial class Application // Current handling +{ + /// + [Obsolete ("The legacy static Application object is going away.")] public static ConcurrentStack SessionStack => ApplicationImpl.Instance.SessionStack; + + /// The that is currently active. + /// The current toplevel. + [Obsolete ("The legacy static Application object is going away.")] + public static Toplevel? Current + { + get => ApplicationImpl.Instance.Current; + internal set => ApplicationImpl.Instance.Current = value; + } +} diff --git a/Terminal.Gui/App/Application.Driver.cs b/Terminal.Gui/App/Application.Driver.cs index c08fb879f..741134d39 100644 --- a/Terminal.Gui/App/Application.Driver.cs +++ b/Terminal.Gui/App/Application.Driver.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics.CodeAnalysis; @@ -7,6 +6,7 @@ namespace Terminal.Gui.App; public static partial class Application // Driver abstractions { /// + [Obsolete ("The legacy static Application object is going away.")] public static IDriver? Driver { get => ApplicationImpl.Instance.Driver; @@ -15,6 +15,7 @@ public static partial class Application // Driver abstractions /// [ConfigurationProperty (Scope = typeof (SettingsScope))] + [Obsolete ("The legacy static Application object is going away.")] public static bool Force16Colors { get => ApplicationImpl.Instance.Force16Colors; @@ -23,6 +24,7 @@ public static partial class Application // Driver abstractions /// [ConfigurationProperty (Scope = typeof (SettingsScope))] + [Obsolete ("The legacy static Application object is going away.")] public static string ForceDriver { get => ApplicationImpl.Instance.ForceDriver; @@ -30,11 +32,13 @@ public static partial class Application // Driver abstractions } /// + [Obsolete ("The legacy static Application object is going away.")] public static List Sixel => ApplicationImpl.Instance.Sixel; /// Gets a list of types and type names that are available. /// [RequiresUnreferencedCode ("AOT")] + [Obsolete ("The legacy static Application object is going away.")] public static (List, List) GetDriverTypes () { // use reflection to get the list of drivers @@ -59,4 +63,4 @@ public static partial class Application // Driver abstractions return (driverTypes, driverTypeNames); } -} \ No newline at end of file +} diff --git a/Terminal.Gui/App/Application.Keyboard.cs b/Terminal.Gui/App/Application.Keyboard.cs index 1fa176a7d..9dbdde854 100644 --- a/Terminal.Gui/App/Application.Keyboard.cs +++ b/Terminal.Gui/App/Application.Keyboard.cs @@ -1,10 +1,9 @@ -#nullable enable - -namespace Terminal.Gui.App; +namespace Terminal.Gui.App; public static partial class Application // Keyboard handling { /// + [Obsolete ("The legacy static Application object is going away.")] public static IKeyboard Keyboard { get => ApplicationImpl.Instance.Keyboard; @@ -13,14 +12,9 @@ public static partial class Application // Keyboard handling } /// + [Obsolete ("The legacy static Application object is going away.")] public static bool RaiseKeyDownEvent (Key key) => ApplicationImpl.Instance.Keyboard.RaiseKeyDownEvent (key); - /// - public static bool? InvokeCommandsBoundToKey (Key key) => ApplicationImpl.Instance.Keyboard.InvokeCommandsBoundToKey (key); - - /// - public static bool? InvokeCommand (Command command, Key key, KeyBinding binding) => ApplicationImpl.Instance.Keyboard.InvokeCommand (command, key, binding); - /// /// Raised when the user presses a key. /// @@ -33,15 +27,14 @@ public static partial class Application // Keyboard handling /// and events. /// Fired after and before . /// + [Obsolete ("The legacy static Application object is going away.")] public static event EventHandler? KeyDown { add => ApplicationImpl.Instance.Keyboard.KeyDown += value; remove => ApplicationImpl.Instance.Keyboard.KeyDown -= value; } - /// - public static bool RaiseKeyUpEvent (Key key) => ApplicationImpl.Instance.Keyboard.RaiseKeyUpEvent (key); - /// + [Obsolete ("The legacy static Application object is going away.")] public static KeyBindings KeyBindings => ApplicationImpl.Instance.Keyboard.KeyBindings; } diff --git a/Terminal.Gui/App/Application.Lifecycle.cs b/Terminal.Gui/App/Application.Lifecycle.cs index 051d6b5c8..802ba7a53 100644 --- a/Terminal.Gui/App/Application.Lifecycle.cs +++ b/Terminal.Gui/App/Application.Lifecycle.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; @@ -11,68 +10,41 @@ namespace Terminal.Gui.App; public static partial class Application // Lifecycle (Init/Shutdown) { + /// + /// Creates a new instance. + /// + /// + /// The recommended pattern is for developers to call Application.Create() and then use the returned + /// instance for all subsequent application operations. + /// + /// A new instance. + public static IApplication Create () { return new ApplicationImpl (); } - /// Initializes a new instance of a Terminal.Gui Application. must be called when the application is closing. - /// Call this method once per instance (or after has been called). - /// - /// This function loads the right for the platform, Creates a . and - /// assigns it to - /// - /// - /// must be called when the application is closing (typically after - /// has returned) to ensure resources are cleaned up and - /// terminal settings - /// restored. - /// - /// - /// The function combines - /// and - /// into a single - /// call. An application can use without explicitly calling - /// . - /// - /// - /// The to use. If neither or - /// are specified the default driver for the platform will be used. - /// - /// - /// The short name (e.g. "dotnet", "windows", "unix", or "fake") of the - /// to use. If neither or are - /// specified the default driver for the platform will be used. - /// + /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public static void Init (IDriver? driver = null, string? driverName = null) + [Obsolete ("The legacy static Application object is going away.")] + public static void Init (string? driverName = null) { - ApplicationImpl.Instance.Init (driver, driverName ?? ForceDriver); + ApplicationImpl.Instance.Init (driverName ?? ForceDriver); } /// /// Gets or sets the main thread ID for the application. /// + [Obsolete ("The legacy static Application object is going away.")] public static int? MainThreadId { get => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId; set => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId = value; } - /// Shutdown an application initialized with . - /// - /// Shutdown must be called for every call to or - /// to ensure all resources are cleaned - /// up (Disposed) - /// and terminal settings are restored. - /// + /// + [Obsolete ("The legacy static Application object is going away.")] public static void Shutdown () => ApplicationImpl.Instance.Shutdown (); - /// - /// Gets whether the application has been initialized with and not yet shutdown with . - /// - /// - /// - /// The event is raised after the and methods have been called. - /// - /// + /// + [Obsolete ("The legacy static Application object is going away.")] public static bool Initialized { get => ApplicationImpl.Instance.Initialized; @@ -80,6 +52,7 @@ public static partial class Application // Lifecycle (Init/Shutdown) } /// + [Obsolete ("The legacy static Application object is going away.")] public static event EventHandler>? InitializedChanged { add => ApplicationImpl.Instance.InitializedChanged += value; @@ -91,5 +64,6 @@ public static partial class Application // Lifecycle (Init/Shutdown) // this in a function like this ensures we don't make mistakes in // guaranteeing that the state of this singleton is deterministic when Init // starts running and after Shutdown returns. + [Obsolete ("The legacy static Application object is going away.")] internal static void ResetState (bool ignoreDisposed = false) => ApplicationImpl.Instance?.ResetState (ignoreDisposed); } diff --git a/Terminal.Gui/App/Application.Mouse.cs b/Terminal.Gui/App/Application.Mouse.cs index 659e07b1c..5e6ec118f 100644 --- a/Terminal.Gui/App/Application.Mouse.cs +++ b/Terminal.Gui/App/Application.Mouse.cs @@ -1,17 +1,12 @@ -#nullable enable using System.ComponentModel; namespace Terminal.Gui.App; public static partial class Application // Mouse handling { - /// - /// Gets the most recent position of the mouse. - /// - public static Point? GetLastMousePosition () { return Mouse.GetLastMousePosition (); } - /// Disable or enable the mouse. The mouse is enabled by default. [ConfigurationProperty (Scope = typeof (SettingsScope))] + [Obsolete ("The legacy static Application object is going away.")] public static bool IsMouseDisabled { get => Mouse.IsMouseDisabled; @@ -26,12 +21,8 @@ public static partial class Application // Mouse handling /// This property provides access to mouse-related functionality in a way that supports /// parallel test execution by avoiding static state. /// - /// - /// New code should use Application.Mouse instead of the static properties and methods - /// for better testability. Legacy static properties like and - /// are retained for backward compatibility. - /// /// + [Obsolete ("The legacy static Application object is going away.")] public static IMouse Mouse => ApplicationImpl.Instance.Mouse; #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved @@ -54,6 +45,7 @@ public static partial class Application // Mouse handling /// Use this even to handle mouse events at the application level, before View-specific handling. /// /// + [Obsolete ("The legacy static Application object is going away.")] public static event EventHandler? MouseEvent { add => Mouse.MouseEvent += value; @@ -65,11 +57,13 @@ public static partial class Application // Mouse handling /// INTERNAL: Holds the non- views that are currently under the /// mouse. /// + [Obsolete ("The legacy static Application object is going away.")] internal static List CachedViewsUnderMouse => Mouse.CachedViewsUnderMouse; /// /// INTERNAL API: Holds the last mouse position. /// + [Obsolete ("The legacy static Application object is going away.")] internal static Point? LastMousePosition { get => Mouse.LastMousePosition; @@ -81,6 +75,7 @@ public static partial class Application // Mouse handling /// /// The position of the mouse. /// The most recent result from GetViewsUnderLocation(). + [Obsolete ("The legacy static Application object is going away.")] internal static void RaiseMouseEnterLeaveEvents (Point screenPosition, List currentViewsUnderMouse) { Mouse.RaiseMouseEnterLeaveEvents (screenPosition, currentViewsUnderMouse); @@ -92,6 +87,7 @@ public static partial class Application // Mouse handling /// /// This method can be used to simulate a mouse event, e.g. in unit tests. /// The mouse event with coordinates relative to the screen. + [Obsolete ("The legacy static Application object is going away.")] internal static void RaiseMouseEvent (MouseEventArgs mouseEvent) { Mouse.RaiseMouseEvent (mouseEvent); diff --git a/Terminal.Gui/App/Application.Navigation.cs b/Terminal.Gui/App/Application.Navigation.cs index b822a6027..b1053ae42 100644 --- a/Terminal.Gui/App/Application.Navigation.cs +++ b/Terminal.Gui/App/Application.Navigation.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.App; @@ -7,6 +6,7 @@ public static partial class Application // Navigation stuff /// /// Gets the instance for the current . /// + [Obsolete ("The legacy static Application object is going away.")] public static ApplicationNavigation? Navigation { get => ApplicationImpl.Instance.Navigation; @@ -15,7 +15,7 @@ public static partial class Application // Navigation stuff /// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key. [ConfigurationProperty (Scope = typeof (SettingsScope))] - public static Key NextTabGroupKey + [Obsolete ("The legacy static Application object is going away.")]public static Key NextTabGroupKey { get => ApplicationImpl.Instance.Keyboard.NextTabGroupKey; set => ApplicationImpl.Instance.Keyboard.NextTabGroupKey = value; @@ -41,6 +41,7 @@ public static partial class Application // Navigation stuff /// and events. /// Fired after . /// + [Obsolete ("The legacy static Application object is going away.")] public static event EventHandler? KeyUp { add => ApplicationImpl.Instance.Keyboard.KeyUp += value; diff --git a/Terminal.Gui/App/Application.Popover.cs b/Terminal.Gui/App/Application.Popover.cs index 31522f80f..40eba8d4d 100644 --- a/Terminal.Gui/App/Application.Popover.cs +++ b/Terminal.Gui/App/Application.Popover.cs @@ -1,13 +1,13 @@ -#nullable enable namespace Terminal.Gui.App; public static partial class Application // Popover handling { /// Gets the Application manager. + [Obsolete ("The legacy static Application object is going away.")] public static ApplicationPopover? Popover { get => ApplicationImpl.Instance.Popover; internal set => ApplicationImpl.Instance.Popover = value; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/App/Application.Run.cs b/Terminal.Gui/App/Application.Run.cs index 56206a997..d218c370d 100644 --- a/Terminal.Gui/App/Application.Run.cs +++ b/Terminal.Gui/App/Application.Run.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui.App; @@ -22,41 +21,57 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E } /// + [Obsolete ("The legacy static Application object is going away.")] public static SessionToken Begin (Toplevel toplevel) => ApplicationImpl.Instance.Begin (toplevel); /// + [Obsolete ("The legacy static Application object is going away.")] public static bool PositionCursor () => ApplicationImpl.Instance.PositionCursor (); /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public static Toplevel Run (Func? errorHandler = null, string? driver = null) => ApplicationImpl.Instance.Run (errorHandler, driver); + [Obsolete ("The legacy static Application object is going away.")] + public static Toplevel Run (Func? errorHandler = null, string? driverName = null) => ApplicationImpl.Instance.Run (errorHandler, driverName); /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public static TView Run (Func? errorHandler = null, string? driver = null) - where TView : Toplevel, new() => ApplicationImpl.Instance.Run (errorHandler, driver); + [Obsolete ("The legacy static Application object is going away.")] + public static TView Run (Func? errorHandler = null, string? driverName = null) + where TView : Toplevel, new() => ApplicationImpl.Instance.Run (errorHandler, driverName); /// + [Obsolete ("The legacy static Application object is going away.")] public static void Run (Toplevel view, Func? errorHandler = null) => ApplicationImpl.Instance.Run (view, errorHandler); /// + [Obsolete ("The legacy static Application object is going away.")] public static object? AddTimeout (TimeSpan time, Func callback) => ApplicationImpl.Instance.AddTimeout (time, callback); /// + [Obsolete ("The legacy static Application object is going away.")] public static bool RemoveTimeout (object token) => ApplicationImpl.Instance.RemoveTimeout (token); /// /// + [Obsolete ("The legacy static Application object is going away.")] public static ITimedEvents? TimedEvents => ApplicationImpl.Instance?.TimedEvents; - /// + + /// + [Obsolete ("The legacy static Application object is going away.")] + public static void Invoke (Action action) => ApplicationImpl.Instance.Invoke (action); + + /// + [Obsolete ("The legacy static Application object is going away.")] public static void Invoke (Action action) => ApplicationImpl.Instance.Invoke (action); /// + [Obsolete ("The legacy static Application object is going away.")] public static void LayoutAndDraw (bool forceRedraw = false) => ApplicationImpl.Instance.LayoutAndDraw (forceRedraw); /// + [Obsolete ("The legacy static Application object is going away.")] public static bool StopAfterFirstIteration { get => ApplicationImpl.Instance.StopAfterFirstIteration; @@ -64,15 +79,15 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E } /// + [Obsolete ("The legacy static Application object is going away.")] public static void RequestStop (Toplevel? top = null) => ApplicationImpl.Instance.RequestStop (top); /// + [Obsolete ("The legacy static Application object is going away.")] public static void End (SessionToken sessionToken) => ApplicationImpl.Instance.End (sessionToken); - /// - internal static void RaiseIteration () => ApplicationImpl.Instance.RaiseIteration (); - /// + [Obsolete ("The legacy static Application object is going away.")] public static event EventHandler? Iteration { add => ApplicationImpl.Instance.Iteration += value; @@ -80,6 +95,7 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E } /// + [Obsolete ("The legacy static Application object is going away.")] public static event EventHandler? SessionBegun { add => ApplicationImpl.Instance.SessionBegun += value; @@ -87,6 +103,7 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E } /// + [Obsolete ("The legacy static Application object is going away.")] public static event EventHandler? SessionEnded { add => ApplicationImpl.Instance.SessionEnded += value; diff --git a/Terminal.Gui/App/Application.Screen.cs b/Terminal.Gui/App/Application.Screen.cs index 2cc223cdb..195c33ea6 100644 --- a/Terminal.Gui/App/Application.Screen.cs +++ b/Terminal.Gui/App/Application.Screen.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.App; @@ -6,6 +5,7 @@ public static partial class Application // Screen related stuff; intended to hid { /// + [Obsolete ("The legacy static Application object is going away.")] public static Rectangle Screen { get => ApplicationImpl.Instance.Screen; @@ -13,6 +13,7 @@ public static partial class Application // Screen related stuff; intended to hid } /// + [Obsolete ("The legacy static Application object is going away.")] public static event EventHandler>? ScreenChanged { add => ApplicationImpl.Instance.ScreenChanged += value; @@ -21,6 +22,7 @@ public static partial class Application // Screen related stuff; intended to hid /// + [Obsolete ("The legacy static Application object is going away.")] internal static bool ClearScreenNextIteration { get => ApplicationImpl.Instance.ClearScreenNextIteration; diff --git a/Terminal.Gui/App/Application.Toplevel.cs b/Terminal.Gui/App/Application.Toplevel.cs deleted file mode 100644 index e7c05e437..000000000 --- a/Terminal.Gui/App/Application.Toplevel.cs +++ /dev/null @@ -1,18 +0,0 @@ -#nullable enable -using System.Collections.Concurrent; - -namespace Terminal.Gui.App; - -public static partial class Application // Toplevel handling -{ - /// - public static ConcurrentStack TopLevels => ApplicationImpl.Instance.TopLevels; - - /// The that is currently active. - /// The top. - public static Toplevel? Top - { - get => ApplicationImpl.Instance.Top; - internal set => ApplicationImpl.Instance.Top = value; - } -} diff --git a/Terminal.Gui/App/Application.cs b/Terminal.Gui/App/Application.cs index bd3ccf7a3..7213e16be 100644 --- a/Terminal.Gui/App/Application.cs +++ b/Terminal.Gui/App/Application.cs @@ -1,8 +1,7 @@ -#nullable enable - // We use global using directives to simplify the code and avoid repetitive namespace declarations. // Put them here so they are available throughout the application. // Do not put them in AssemblyInfo.cs as it will break GitVersion's /updateassemblyinfo + global using Attribute = Terminal.Gui.Drawing.Attribute; global using Color = Terminal.Gui.Drawing.Color; global using CM = Terminal.Gui.Configuration.ConfigurationManager; @@ -16,7 +15,6 @@ global using Terminal.Gui.Drawing; global using Terminal.Gui.Text; global using Terminal.Gui.Resources; global using Terminal.Gui.FileServices; -using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Resources; @@ -40,95 +38,31 @@ namespace Terminal.Gui.App; public static partial class Application { /// - /// Maximum number of iterations of the main loop (and hence draws) - /// to allow to occur per second. Defaults to > which is a 40ms sleep - /// after iteration (factoring in how long iteration took to run). - /// Note that not every iteration draws (see ). - /// Only affects v2 drivers. + /// Maximum number of iterations of the main loop (and hence draws) + /// to allow to occur per second. Defaults to > which is a 40ms sleep + /// after iteration (factoring in how long iteration took to run). + /// + /// Note that not every iteration draws (see ). + /// Only affects v2 drivers. + /// /// public static ushort MaximumIterationsPerSecond = DefaultMaximumIterationsPerSecond; /// - /// Default value for + /// Default value for /// public const ushort DefaultMaximumIterationsPerSecond = 25; - /// - /// Gets a string representation of the Application as rendered by . - /// - /// A string representation of the Application - public new static string ToString () - { - IDriver? driver = Driver; - - if (driver is null) - { - return string.Empty; - } - - return ToString (driver); - } - - /// - /// Gets a string representation of the Application rendered by the provided . - /// - /// The driver to use to render the contents. - /// A string representation of the Application - public static string ToString (IDriver? driver) - { - if (driver is null) - { - return string.Empty; - } - - var sb = new StringBuilder (); - - Cell [,] contents = driver?.Contents!; - - for (var r = 0; r < driver!.Rows; r++) - { - for (var c = 0; c < driver.Cols; c++) - { - Rune rune = contents [r, c].Rune; - - if (rune.DecodeSurrogatePair (out char []? sp)) - { - sb.Append (sp); - } - else - { - sb.Append ((char)rune.Value); - } - - if (rune.GetColumns () > 1) - { - c++; - } - - // See Issue #2616 - //foreach (var combMark in contents [r, c].CombiningMarks) { - // sb.Append ((char)combMark.Value); - //} - } - - sb.AppendLine (); - } - - return sb.ToString (); - } - /// Gets all cultures supported by the application without the invariant language. public static List? SupportedCultures { get; private set; } = GetSupportedCultures (); - internal static List GetAvailableCulturesFromEmbeddedResources () { ResourceManager rm = new (typeof (Strings)); CultureInfo [] cultures = CultureInfo.GetCultures (CultureTypes.AllCultures); - return cultures.Where ( - cultureInfo => + return cultures.Where (cultureInfo => !cultureInfo.Equals (CultureInfo.InvariantCulture) && rm.GetResourceSet (cultureInfo, true, false) is { } ) @@ -152,8 +86,7 @@ public static partial class Application if (cultures.Length > 1 && Directory.Exists (Path.Combine (assemblyLocation, "pt-PT"))) { // Return all culture for which satellite folder found with culture code. - return cultures.Where ( - cultureInfo => + return cultures.Where (cultureInfo => Directory.Exists (Path.Combine (assemblyLocation, cultureInfo.Name)) && File.Exists (Path.Combine (assemblyLocation, cultureInfo.Name, resourceFilename)) ) diff --git a/Terminal.Gui/App/ApplicationImpl.Driver.cs b/Terminal.Gui/App/ApplicationImpl.Driver.cs index 36679b2b0..837024b83 100644 --- a/Terminal.Gui/App/ApplicationImpl.Driver.cs +++ b/Terminal.Gui/App/ApplicationImpl.Driver.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Concurrent; namespace Terminal.Gui.App; @@ -80,7 +79,7 @@ public partial class ApplicationImpl Logging.Trace ($"Created Subcomponents: {Coordinator}"); - Coordinator.StartInputTaskAsync ().Wait (); + Coordinator.StartInputTaskAsync (this).Wait (); if (Driver == null) { diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs index 0a7795833..73465bf8a 100644 --- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs +++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -15,7 +14,7 @@ public partial class ApplicationImpl /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public void Init (IDriver? driver = null, string? driverName = null) + public void Init (string? driverName = null) { if (Initialized) { @@ -34,11 +33,11 @@ public partial class ApplicationImpl _driverName = ForceDriver; } - Debug.Assert (Navigation is null); - Navigation = new (); + // Debug.Assert (Navigation is null); + // Navigation = new (); - Debug.Assert (Popover is null); - Popover = new (); + //Debug.Assert (Popover is null); + //Popover = new (); // Preserve existing keyboard settings if they exist bool hasExistingKeyboard = _keyboard is { }; @@ -50,7 +49,7 @@ public partial class ApplicationImpl Key existingPrevTabGroupKey = _keyboard?.PrevTabGroupKey ?? Key.F6.WithShift; // Reset keyboard to ensure fresh state with default bindings - _keyboard = new KeyboardImpl { Application = this }; + _keyboard = new KeyboardImpl { App = this }; // Restore previously set keys if they existed and were different from defaults if (hasExistingKeyboard) @@ -114,9 +113,6 @@ public partial class ApplicationImpl // Clear the event to prevent memory leaks InitializedChanged = null; - - // Create a new lazy instance for potential future Init - _lazyInstance = new (() => new ApplicationImpl ()); } #if DEBUG @@ -156,8 +152,11 @@ public partial class ApplicationImpl // Init created. Apps that do any threading will need to code defensively for this. // e.g. see Issue #537 + // === 0. Stop all timers === + TimedEvents?.StopAll (); + // === 1. Stop all running toplevels === - foreach (Toplevel? t in TopLevels) + foreach (Toplevel? t in SessionStack) { t!.Running = false; } @@ -170,29 +169,30 @@ public partial class ApplicationImpl popover.Visible = false; } + // Any popovers added to Popover have their lifetime controlled by Popover Popover?.Dispose (); Popover = null; // === 3. Clean up toplevels === - TopLevels.Clear (); + SessionStack.Clear (); #if DEBUG_IDISPOSABLE - // Don't dispose the Top. It's up to caller dispose it - if (View.EnableDebugIDisposableAsserts && !ignoreDisposed && Top is { }) + // Don't dispose the Current. It's up to caller dispose it + if (View.EnableDebugIDisposableAsserts && !ignoreDisposed && Current is { }) { - Debug.Assert (Top.WasDisposed, $"Title = {Top.Title}, Id = {Top.Id}"); + Debug.Assert (Current.WasDisposed, $"Title = {Current.Title}, Id = {Current.Id}"); // If End wasn't called _CachedSessionTokenToplevel may be null if (CachedSessionTokenToplevel is { }) { Debug.Assert (CachedSessionTokenToplevel.WasDisposed); - Debug.Assert (CachedSessionTokenToplevel == Top); + Debug.Assert (CachedSessionTokenToplevel == Current); } } #endif - Top = null; + Current = null; CachedSessionTokenToplevel = null; // === 4. Clean up driver === @@ -222,7 +222,7 @@ public partial class ApplicationImpl // === 7. Clear navigation and screen state === ScreenChanged = null; - Navigation = null; + //Navigation = null; // === 8. Reset initialization state === Initialized = false; diff --git a/Terminal.Gui/App/ApplicationImpl.Run.cs b/Terminal.Gui/App/ApplicationImpl.Run.cs index fd0564e05..4e9f59645 100644 --- a/Terminal.Gui/App/ApplicationImpl.Run.cs +++ b/Terminal.Gui/App/ApplicationImpl.Run.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -37,28 +36,28 @@ public partial class ApplicationImpl var rs = new SessionToken (toplevel); #if DEBUG_IDISPOSABLE - if (View.EnableDebugIDisposableAsserts && Top is { } && toplevel != Top && !TopLevels.Contains (Top)) + if (View.EnableDebugIDisposableAsserts && Current is { } && toplevel != Current && !SessionStack.Contains (Current)) { - // This assertion confirm if the Top was already disposed - Debug.Assert (Top.WasDisposed); - Debug.Assert (Top == CachedSessionTokenToplevel); + // This assertion confirm if the Current was already disposed + Debug.Assert (Current.WasDisposed); + Debug.Assert (Current == CachedSessionTokenToplevel); } #endif - lock (TopLevels) + lock (SessionStack) { - if (Top is { } && toplevel != Top && !TopLevels.Contains (Top)) + if (Current is { } && toplevel != Current && !SessionStack.Contains (Current)) { - // If Top was already disposed and isn't on the Toplevels Stack, + // If Current was already disposed and isn't on the Toplevels Stack, // clean it up here if is the same as _CachedSessionTokenToplevel - if (Top == CachedSessionTokenToplevel) + if (Current == CachedSessionTokenToplevel) { - Top = null; + Current = null; } else { // Probably this will never hit - throw new ObjectDisposedException (Top.GetType ().FullName); + throw new ObjectDisposedException (Current.GetType ().FullName); } } @@ -67,56 +66,58 @@ public partial class ApplicationImpl if (string.IsNullOrEmpty (toplevel.Id)) { var count = 1; - var id = (TopLevels.Count + count).ToString (); + var id = (SessionStack.Count + count).ToString (); - while (TopLevels.Count > 0 && TopLevels.FirstOrDefault (x => x.Id == id) is { }) + while (SessionStack.Count > 0 && SessionStack.FirstOrDefault (x => x.Id == id) is { }) { count++; - id = (TopLevels.Count + count).ToString (); + id = (SessionStack.Count + count).ToString (); } - toplevel.Id = (TopLevels.Count + count).ToString (); + toplevel.Id = (SessionStack.Count + count).ToString (); - TopLevels.Push (toplevel); + SessionStack.Push (toplevel); } else { - Toplevel? dup = TopLevels.FirstOrDefault (x => x.Id == toplevel.Id); + Toplevel? dup = SessionStack.FirstOrDefault (x => x.Id == toplevel.Id); if (dup is null) { - TopLevels.Push (toplevel); + SessionStack.Push (toplevel); } } } - if (Top is null) + if (Current is null) { - Top = toplevel; + toplevel.App = this; + Current = toplevel; } - if ((Top?.Modal == false && toplevel.Modal) - || (Top?.Modal == false && !toplevel.Modal) - || (Top?.Modal == true && toplevel.Modal)) + if ((Current?.Modal == false && toplevel.Modal) + || (Current?.Modal == false && !toplevel.Modal) + || (Current?.Modal == true && toplevel.Modal)) { if (toplevel.Visible) { - if (Top is { HasFocus: true }) + if (Current is { HasFocus: true }) { - Top.HasFocus = false; + Current.HasFocus = false; } - // Force leave events for any entered views in the old Top - if (Mouse.GetLastMousePosition () is { }) + // Force leave events for any entered views in the old Current + if (Mouse.LastMousePosition is { }) { - Mouse.RaiseMouseEnterLeaveEvents (Mouse.GetLastMousePosition ()!.Value, new ()); + Mouse.RaiseMouseEnterLeaveEvents (Mouse.LastMousePosition!.Value, new ()); } - Top?.OnDeactivate (toplevel); - Toplevel previousTop = Top!; + Current?.OnDeactivate (toplevel); + Toplevel previousTop = Current!; - Top = toplevel; - Top.OnActivate (previousTop); + Current = toplevel; + Current.App = this; + Current.OnActivate (previousTop); } } @@ -135,7 +136,7 @@ public partial class ApplicationImpl toplevel.OnLoaded (); - Instance.LayoutAndDraw (true); + LayoutAndDraw (true); if (PositionCursor ()) { @@ -156,18 +157,18 @@ public partial class ApplicationImpl /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public Toplevel Run (Func? errorHandler = null, string? driver = null) { return Run (errorHandler, driver); } + public Toplevel Run (Func? errorHandler = null, string? driverName = null) { return Run (errorHandler, driverName); } /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public TView Run (Func? errorHandler = null, string? driver = null) + public TView Run (Func? errorHandler = null, string? driverName = null) where TView : Toplevel, new () { if (!Initialized) { // Init() has NOT been called. Auto-initialize as per interface contract. - Init (null, driver); + Init (driverName); } TView top = new (); @@ -193,15 +194,15 @@ public partial class ApplicationImpl throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view"); } - Top = view; + Current = view; - SessionToken rs = Application.Begin (view); + SessionToken rs = Begin (view); - Top.Running = true; + Current.Running = true; var firstIteration = true; - while (TopLevels.TryPeek (out Toplevel? found) && found == view && view.Running) + while (SessionStack.TryPeek (out Toplevel? found) && found == view && view.Running) { if (Coordinator is null) { @@ -220,7 +221,7 @@ public partial class ApplicationImpl } Logging.Information ("Run - Calling End"); - Application.End (rs); + End (rs); } /// @@ -233,11 +234,11 @@ public partial class ApplicationImpl ApplicationPopover.HideWithQuitCommand (visiblePopover); } - sessionToken.Toplevel.OnUnloaded (); + sessionToken.Toplevel?.OnUnloaded (); // End the Session // First, take it off the Toplevel Stack - if (TopLevels.TryPop (out Toplevel? topOfStack)) + if (SessionStack.TryPop (out Toplevel? topOfStack)) { if (topOfStack != sessionToken.Toplevel) { @@ -250,10 +251,11 @@ public partial class ApplicationImpl // Notify that it is closing sessionToken.Toplevel?.OnClosed (sessionToken.Toplevel); - if (TopLevels.TryPeek (out Toplevel? newTop)) + if (SessionStack.TryPeek (out Toplevel? newTop)) { - Top = newTop; - Top?.SetNeedsDraw (); + newTop.App = this; + Current = newTop; + Current?.SetNeedsDraw (); } if (sessionToken.Toplevel is { HasFocus: true }) @@ -261,9 +263,9 @@ public partial class ApplicationImpl sessionToken.Toplevel.HasFocus = false; } - if (Top is { HasFocus: false }) + if (Current is { HasFocus: false }) { - Top.SetFocus (); + Current.SetFocus (); } CachedSessionTokenToplevel = sessionToken.Toplevel; @@ -283,9 +285,9 @@ public partial class ApplicationImpl /// public void RequestStop (Toplevel? top) { - Logging.Trace ($"Top: '{(top is { } ? top : "null")}'"); + Logging.Trace ($"Current: '{(top is { } ? top : "null")}'"); - top ??= Top; + top ??= Current; if (top == null) { @@ -322,12 +324,12 @@ public partial class ApplicationImpl public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); } /// - public void Invoke (Action action) + public void Invoke (Action? action) { // If we are already on the main UI thread - if (Top is { Running: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId) + if (Current is { Running: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId) { - action (); + action?.Invoke (this); return; } @@ -336,7 +338,30 @@ public partial class ApplicationImpl TimeSpan.Zero, () => { - action (); + action?.Invoke (this); + + return false; + } + ); + } + + + /// + public void Invoke (Action action) + { + // If we are already on the main UI thread + if (Current is { Running: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId) + { + action?.Invoke (); + + return; + } + + _timedEvents.Add ( + TimeSpan.Zero, + () => + { + action?.Invoke (); return false; } diff --git a/Terminal.Gui/App/ApplicationImpl.Screen.cs b/Terminal.Gui/App/ApplicationImpl.Screen.cs index 3c2f32cb6..b5a930794 100644 --- a/Terminal.Gui/App/ApplicationImpl.Screen.cs +++ b/Terminal.Gui/App/ApplicationImpl.Screen.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.App; @@ -45,6 +44,11 @@ public partial class ApplicationImpl /// public bool PositionCursor () { + if (Driver is null) + { + return false; + } + // Find the most focused view and position the cursor there. View? mostFocused = Navigation?.GetFocused (); @@ -66,7 +70,7 @@ public partial class ApplicationImpl Rectangle mostFocusedViewport = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = Point.Empty }); Rectangle superViewViewport = - mostFocused.SuperView?.ViewportToScreen (mostFocused.SuperView.Viewport with { Location = Point.Empty }) ?? Driver!.Screen; + mostFocused.SuperView?.ViewportToScreen (mostFocused.SuperView.Viewport with { Location = Point.Empty }) ?? Driver.Screen; if (!superViewViewport.IntersectsWith (mostFocusedViewport)) { @@ -133,7 +137,7 @@ public partial class ApplicationImpl ScreenChanged?.Invoke (this, new (screen)); - foreach (Toplevel t in TopLevels) + foreach (Toplevel t in SessionStack) { t.OnSizeChanging (new (screen.Size)); t.SetNeedsLayout (); @@ -147,7 +151,7 @@ public partial class ApplicationImpl /// public void LayoutAndDraw (bool forceRedraw = false) { - List tops = [.. TopLevels]; + List tops = [.. SessionStack]; if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover) { @@ -169,9 +173,12 @@ public partial class ApplicationImpl Driver?.ClearContents (); } - View.SetClipToScreen (); - View.Draw (tops, neededLayout || forceRedraw); - View.SetClipToScreen (); - Driver?.Refresh (); + if (Driver is { }) + { + Driver.Clip = new (Screen); + View.Draw (tops, neededLayout || forceRedraw); + Driver.Clip = new (Screen); + Driver?.Refresh (); + } } } diff --git a/Terminal.Gui/App/ApplicationImpl.cs b/Terminal.Gui/App/ApplicationImpl.cs index d3b0277ba..bcedf0b14 100644 --- a/Terminal.Gui/App/ApplicationImpl.cs +++ b/Terminal.Gui/App/ApplicationImpl.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Concurrent; namespace Terminal.Gui.App; @@ -10,9 +9,9 @@ namespace Terminal.Gui.App; public partial class ApplicationImpl : IApplication { /// - /// Creates a new instance of the Application backend. + /// INTERNAL: Creates a new instance of the Application backend. /// - public ApplicationImpl () { } + internal ApplicationImpl () { } /// /// INTERNAL: Creates a new instance of the Application backend. @@ -22,22 +21,22 @@ public partial class ApplicationImpl : IApplication #region Singleton - // Private static readonly Lazy instance of Application - private static Lazy _lazyInstance = new (() => new ApplicationImpl ()); - /// - /// Change the singleton implementation, should not be called except before application - /// startup. This method lets you provide alternative implementations of core static gateway - /// methods of . + /// Configures the singleton instance of to use the specified backend implementation. /// - /// - public static void ChangeInstance (IApplication? newApplication) { _lazyInstance = new (newApplication!); } + /// + public static void SetInstance (IApplication? app) + { + _instance = app; + } + + // Private static readonly Lazy instance of Application + private static IApplication? _instance; /// /// Gets the currently configured backend implementation of gateway methods. - /// Change to your own implementation by using (before init). /// - public static IApplication Instance => _lazyInstance.Value; + public static IApplication Instance => _instance ??= new ApplicationImpl (); #endregion Singleton @@ -57,7 +56,7 @@ public partial class ApplicationImpl : IApplication { if (_mouse is null) { - _mouse = new MouseImpl { Application = this }; + _mouse = new MouseImpl { App = this }; } return _mouse; @@ -66,7 +65,6 @@ public partial class ApplicationImpl : IApplication } private IKeyboard? _keyboard; - private bool _stopAfterFirstIteration; /// /// Handles keyboard input and key bindings at the Application level @@ -77,7 +75,7 @@ public partial class ApplicationImpl : IApplication { if (_keyboard is null) { - _keyboard = new KeyboardImpl { Application = this }; + _keyboard = new KeyboardImpl { App = this }; } return _keyboard; @@ -89,22 +87,67 @@ public partial class ApplicationImpl : IApplication #region View Management - /// - public ApplicationPopover? Popover { get; set; } + private ApplicationPopover? _popover; /// - public ApplicationNavigation? Navigation { get; set; } + public ApplicationPopover? Popover + { + get + { + if (_popover is null) + { + _popover = new () { App = this }; + } + + return _popover; + } + set => _popover = value; + } + + private ApplicationNavigation? _navigation; /// - public Toplevel? Top { get; set; } + public ApplicationNavigation? Navigation + { + get + { + if (_navigation is null) + { + _navigation = new () { App = this }; + } - // BUGBUG: Technically, this is not the full lst of TopLevels. There be dragons here, e.g. see how Toplevel.Id is used. What + return _navigation; + } + set => _navigation = value ?? throw new ArgumentNullException (nameof (value)); + } + + private Toplevel? _current; /// - public ConcurrentStack TopLevels { get; } = new (); + public Toplevel? Current + { + get => _current; + set + { + _current = value; + + if (_current is { }) + { + _current.App = this; + } + } + } + + // BUGBUG: Technically, this is not the full lst of sessions. There be dragons here, e.g. see how Toplevel.Id is used. What + + /// + public ConcurrentStack SessionStack { get; } = new (); /// public Toplevel? CachedSessionTokenToplevel { get; set; } #endregion View Management + + /// + public new string ToString () => Driver?.ToString () ?? string.Empty; } diff --git a/Terminal.Gui/App/ApplicationNavigation.cs b/Terminal.Gui/App/ApplicationNavigation.cs index 4a64b037b..d0ef40d9c 100644 --- a/Terminal.Gui/App/ApplicationNavigation.cs +++ b/Terminal.Gui/App/ApplicationNavigation.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics; @@ -17,6 +16,11 @@ public class ApplicationNavigation // TODO: Move navigation key bindings here from AddApplicationKeyBindings } + /// + /// The instance used by this instance. + /// + public IApplication? App { get; set; } + private View? _focused; /// @@ -105,10 +109,10 @@ public class ApplicationNavigation /// public bool AdvanceFocus (NavigationDirection direction, TabBehavior? behavior) { - if (Application.Popover?.GetActivePopover () as View is { Visible: true } visiblePopover) + if (App?.Popover?.GetActivePopover () as View is { Visible: true } visiblePopover) { return visiblePopover.AdvanceFocus (direction, behavior); } - return Application.Top is { } && Application.Top.AdvanceFocus (direction, behavior); + return App?.Current is { } && App.Current.AdvanceFocus (direction, behavior); } } diff --git a/Terminal.Gui/App/ApplicationPopover.cs b/Terminal.Gui/App/ApplicationPopover.cs index 69430dbab..72f9957df 100644 --- a/Terminal.Gui/App/ApplicationPopover.cs +++ b/Terminal.Gui/App/ApplicationPopover.cs @@ -1,12 +1,11 @@ -#nullable enable using System.Diagnostics; namespace Terminal.Gui.App; /// -/// Helper class for support of views for . Held by -/// +/// Helper class for support of views for . Held by +/// /// public sealed class ApplicationPopover : IDisposable { @@ -15,6 +14,11 @@ public sealed class ApplicationPopover : IDisposable /// public ApplicationPopover () { } + /// + /// The instance used by this instance. + /// + public IApplication? App { get; set; } + private readonly List _popovers = []; /// @@ -35,10 +39,15 @@ public sealed class ApplicationPopover : IDisposable /// , after it has been registered. public IPopover? Register (IPopover? popover) { - if (popover is { } && !_popovers.Contains (popover)) + if (popover is { } && !IsRegistered (popover)) { - // When created, set IPopover.Toplevel to the current Application.Top - popover.Toplevel ??= Application.Top; + // When created, set IPopover.Toplevel to the current Application.Current + popover.Current ??= App?.Current; + + if (popover is View popoverView) + { + popoverView.App = App; + } _popovers.Add (popover); } @@ -46,6 +55,13 @@ public sealed class ApplicationPopover : IDisposable return popover; } + /// + /// Indicates whether a popover has been registered or not. + /// + /// + /// + public bool IsRegistered (IPopover? popover) => popover is { } && _popovers.Contains (popover); + /// /// De-registers with the application. Use this to remove the popover and it's /// keyboard bindings from the application. @@ -59,7 +75,7 @@ public sealed class ApplicationPopover : IDisposable /// public bool DeRegister (IPopover? popover) { - if (popover is null || !_popovers.Contains (popover)) + if (popover is null || !IsRegistered (popover)) { return false; } @@ -100,9 +116,14 @@ public sealed class ApplicationPopover : IDisposable /// public void Show (IPopover? popover) { + if (!IsRegistered (popover)) + { + throw new InvalidOperationException (@"Popovers must be registered before being shown."); + } // If there's an existing popover, hide it. if (_activePopover is View popoverView) { + popoverView.App = App; popoverView.Visible = false; _activePopover = null; } @@ -120,9 +141,6 @@ public sealed class ApplicationPopover : IDisposable throw new InvalidOperationException ("Popovers must have a key binding for Command.Quit."); } - - Register (popover); - if (!newPopover.IsInitialized) { newPopover.BeginInit (); @@ -148,7 +166,7 @@ public sealed class ApplicationPopover : IDisposable { _activePopover = null; popoverView.Visible = false; - Application.Top?.SetNeedsDraw (); + popoverView.App?.Current?.SetNeedsDraw (); } } @@ -177,7 +195,7 @@ public sealed class ApplicationPopover : IDisposable internal bool DispatchKeyDown (Key key) { // Do active first - Active gets all key down events. - var activePopover = GetActivePopover () as View; + View? activePopover = GetActivePopover () as View; if (activePopover is { Visible: true }) { @@ -197,13 +215,14 @@ public sealed class ApplicationPopover : IDisposable { if (popover == activePopover || popover is not View popoverView - || (popover.Toplevel is { } && popover.Toplevel != Application.Top)) + || (popover.Current is { } && popover.Current != App?.Current)) { continue; } // hotKeyHandled = popoverView.InvokeCommandsBoundToHotKey (key); //Logging.Debug ($"Inactive - Calling NewKeyDownEvent ({key}) on {popoverView.Title}"); + popoverView.App ??= App; hotKeyHandled = popoverView.NewKeyDownEvent (key); if (hotKeyHandled is true) diff --git a/Terminal.Gui/App/CWP/CWPEventHelper.cs b/Terminal.Gui/App/CWP/CWPEventHelper.cs index d85d184d5..4840a358c 100644 --- a/Terminal.Gui/App/CWP/CWPEventHelper.cs +++ b/Terminal.Gui/App/CWP/CWPEventHelper.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.App; using System; @@ -53,4 +52,4 @@ public static class CWPEventHelper eventHandler.Invoke (null, args); return args.Handled; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/App/CWP/CWPPropertyHelper.cs b/Terminal.Gui/App/CWP/CWPPropertyHelper.cs index 2ea94ce97..09bcd7fa0 100644 --- a/Terminal.Gui/App/CWP/CWPPropertyHelper.cs +++ b/Terminal.Gui/App/CWP/CWPPropertyHelper.cs @@ -1,7 +1,5 @@ namespace Terminal.Gui.App; -#nullable enable - /// /// Provides helper methods for executing property change workflows in the Cancellable Work Pattern (CWP). /// diff --git a/Terminal.Gui/App/CWP/CWPWorkflowHelper.cs b/Terminal.Gui/App/CWP/CWPWorkflowHelper.cs index 401f17fb8..4c0328589 100644 --- a/Terminal.Gui/App/CWP/CWPWorkflowHelper.cs +++ b/Terminal.Gui/App/CWP/CWPWorkflowHelper.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.App; using System; @@ -126,4 +125,4 @@ public static class CWPWorkflowHelper } return args.Result!; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/App/CWP/CancelEventArgs.cs b/Terminal.Gui/App/CWP/CancelEventArgs.cs index 7378b722a..422fa5323 100644 --- a/Terminal.Gui/App/CWP/CancelEventArgs.cs +++ b/Terminal.Gui/App/CWP/CancelEventArgs.cs @@ -1,4 +1,3 @@ -#nullable enable using System.ComponentModel; namespace Terminal.Gui.App; diff --git a/Terminal.Gui/App/CWP/EventArgs.cs b/Terminal.Gui/App/CWP/EventArgs.cs index fe7644264..edd1cc450 100644 --- a/Terminal.Gui/App/CWP/EventArgs.cs +++ b/Terminal.Gui/App/CWP/EventArgs.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.App; #pragma warning disable CS1711 diff --git a/Terminal.Gui/App/CWP/ResultEventArgs.cs b/Terminal.Gui/App/CWP/ResultEventArgs.cs index d75627c3b..e8a3f67c0 100644 --- a/Terminal.Gui/App/CWP/ResultEventArgs.cs +++ b/Terminal.Gui/App/CWP/ResultEventArgs.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.App; using System; @@ -42,4 +41,4 @@ public class ResultEventArgs Result = result; } } -#pragma warning restore CS1711 \ No newline at end of file +#pragma warning restore CS1711 diff --git a/Terminal.Gui/App/CWP/ValueChangedEventArgs.cs b/Terminal.Gui/App/CWP/ValueChangedEventArgs.cs index d04c42825..880c59245 100644 --- a/Terminal.Gui/App/CWP/ValueChangedEventArgs.cs +++ b/Terminal.Gui/App/CWP/ValueChangedEventArgs.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.App; /// diff --git a/Terminal.Gui/App/CWP/ValueChangingEventArgs.cs b/Terminal.Gui/App/CWP/ValueChangingEventArgs.cs index fed087b8c..15d4688b2 100644 --- a/Terminal.Gui/App/CWP/ValueChangingEventArgs.cs +++ b/Terminal.Gui/App/CWP/ValueChangingEventArgs.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.App; /// @@ -41,4 +40,4 @@ public class ValueChangingEventArgs CurrentValue = currentValue; NewValue = newValue; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/App/Clipboard/Clipboard.cs b/Terminal.Gui/App/Clipboard/Clipboard.cs index f8cf39892..0db013351 100644 --- a/Terminal.Gui/App/Clipboard/Clipboard.cs +++ b/Terminal.Gui/App/Clipboard/Clipboard.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.App; /// Provides cut, copy, and paste support for the OS clipboard. @@ -31,7 +30,7 @@ public static class Clipboard if (IsSupported) { // throw new InvalidOperationException ($"{Application.Driver?.GetType ().Name}.GetClipboardData returned null instead of string.Empty"); - string? clipData = Application.Driver?.Clipboard?.GetClipboardData () ?? string.Empty; + string clipData = Application.Driver?.Clipboard?.GetClipboardData () ?? string.Empty; _contents = clipData; } @@ -66,4 +65,4 @@ public static class Clipboard /// Returns true if the environmental dependencies are in place to interact with the OS clipboard. /// public static bool IsSupported => Application.Driver?.Clipboard?.IsSupported ?? false; -} \ No newline at end of file +} diff --git a/Terminal.Gui/App/Clipboard/ClipboardBase.cs b/Terminal.Gui/App/Clipboard/ClipboardBase.cs index 97cfec61e..46a1f26bf 100644 --- a/Terminal.Gui/App/Clipboard/ClipboardBase.cs +++ b/Terminal.Gui/App/Clipboard/ClipboardBase.cs @@ -1,3 +1,4 @@ +#nullable disable using System.Diagnostics; namespace Terminal.Gui.App; diff --git a/Terminal.Gui/App/Clipboard/ClipboardProcessRunner.cs b/Terminal.Gui/App/Clipboard/ClipboardProcessRunner.cs index 214b5337d..70c471d74 100644 --- a/Terminal.Gui/App/Clipboard/ClipboardProcessRunner.cs +++ b/Terminal.Gui/App/Clipboard/ClipboardProcessRunner.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics; namespace Terminal.Gui.App; @@ -45,7 +44,6 @@ internal static class ClipboardProcessRunner CreateNoWindow = true }; - TaskCompletionSource eventHandled = new (); process.Start (); if (!string.IsNullOrEmpty (input)) diff --git a/Terminal.Gui/App/IApplication.cs b/Terminal.Gui/App/IApplication.cs index 64620f09e..c02563170 100644 --- a/Terminal.Gui/App/IApplication.cs +++ b/Terminal.Gui/App/IApplication.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui.App; @@ -41,14 +40,9 @@ public interface IApplication #region Initialization and Shutdown /// Initializes a new instance of Application. - /// - /// The to use. If neither or - /// are specified the default driver for the platform will be used. - /// /// /// The short name (e.g. "dotnet", "windows", "unix", or "fake") of the - /// to use. If neither or are - /// specified the default driver for the platform will be used. + /// to use. If not specified the default driver for the platform will be used. /// /// /// Call this method once per instance (or after has been called). @@ -61,14 +55,14 @@ public interface IApplication /// has returned) to ensure resources are cleaned up and terminal settings restored. /// /// - /// The function combines and + /// The function combines and /// into a single call. An application can use - /// without explicitly calling . + /// without explicitly calling . /// /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public void Init (IDriver? driver = null, string? driverName = null); + public void Init (string? driverName = null); /// /// This event is raised after the and methods have been called. @@ -137,7 +131,7 @@ public interface IApplication /// stopped, will be called. /// /// Handler for any unhandled exceptions (resumes when returns true, rethrows when null). - /// + /// /// The driver name. If not specified the default driver for the platform will be used. Must be /// if has already been called. /// @@ -154,7 +148,7 @@ public interface IApplication /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public Toplevel Run (Func? errorHandler = null, string? driver = null); + public Toplevel Run (Func? errorHandler = null, string? driverName = null); /// /// Runs a new Session creating a -derived object of type @@ -163,7 +157,7 @@ public interface IApplication /// /// The type of to create and run. /// Handler for any unhandled exceptions (resumes when returns true, rethrows when null). - /// + /// /// The driver name. If not specified the default driver for the platform will be used. Must be /// if has already been called. /// @@ -206,7 +200,7 @@ public interface IApplication /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public TView Run (Func? errorHandler = null, string? driver = null) + public TView Run (Func? errorHandler = null, string? driverName = null) where TView : Toplevel, new (); /// @@ -266,6 +260,17 @@ public interface IApplication /// public event EventHandler? Iteration; + /// Runs on the main UI loop thread. + /// The action to be invoked on the main processing thread. + /// + /// + /// If called from the main thread, the action is executed immediately. Otherwise, it is queued via + /// with and will be executed on the next main loop + /// iteration. + /// + /// + void Invoke (Action? action); + /// Runs on the main UI loop thread. /// The action to be invoked on the main processing thread. /// @@ -296,14 +301,14 @@ public interface IApplication /// /// This will cause to return. /// - /// This is equivalent to calling with as the parameter. + /// This is equivalent to calling with as the parameter. /// /// void RequestStop (); /// Requests that the currently running Session stop. The Session will stop after the current iteration completes. /// - /// The to stop. If , stops the currently running . + /// The to stop. If , stops the currently running . /// /// /// This will cause to return. @@ -351,22 +356,22 @@ public interface IApplication #region Toplevel Management - /// Gets or sets the current Toplevel. + /// Gets or sets the currently active Toplevel. /// /// /// This is set by and cleared by . /// /// - Toplevel? Top { get; set; } + Toplevel? Current { get; set; } - /// Gets the stack of all Toplevels. + /// Gets the stack of all active Toplevel sessions. /// /// /// Toplevels are added to this stack by and removed by /// . /// /// - ConcurrentStack TopLevels { get; } + ConcurrentStack SessionStack { get; } /// /// Caches the Toplevel associated with the current Session. @@ -428,7 +433,7 @@ public interface IApplication /// /// /// This is typically set to when a View's changes and that view - /// has no SuperView (e.g. when is moved or resized). + /// has no SuperView (e.g. when is moved or resized). /// /// /// Automatically reset to after processes it. @@ -509,12 +514,15 @@ public interface IApplication /// returns , the timeout will stop and be removed. /// /// - /// A token that can be used to stop the timeout by calling . + /// Call with the returned value to stop the timeout. /// /// /// /// When the time specified passes, the callback will be invoked on the main UI thread. /// + /// + /// calls StopAll on to remove all timeouts. + /// /// object AddTimeout (TimeSpan time, Func callback); @@ -539,4 +547,10 @@ public interface IApplication ITimedEvents? TimedEvents { get; } #endregion Timeouts + + /// + /// Gets a string representation of the Application as rendered by . + /// + /// A string representation of the Application + public string ToString (); } diff --git a/Terminal.Gui/App/IPopover.cs b/Terminal.Gui/App/IPopover.cs index 7e86ffe77..87218d07a 100644 --- a/Terminal.Gui/App/IPopover.cs +++ b/Terminal.Gui/App/IPopover.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.App; @@ -51,12 +50,12 @@ namespace Terminal.Gui.App; public interface IPopover { /// - /// Gets or sets the that this Popover is associated with. If null, it is not associated with + /// Gets or sets the that this Popover is associated with. If null, it is not associated with /// any Toplevel and will receive all keyboard - /// events from the . If set, it will only receive keyboard events the Toplevel would normally + /// events from the . If set, it will only receive keyboard events the Toplevel would normally /// receive. - /// When is called, the is set to the current - /// if not already set. + /// When is called, the is set to the current + /// if not already set. /// - Toplevel? Toplevel { get; set; } + Toplevel? Current { get; set; } } diff --git a/Terminal.Gui/App/Keyboard/IKeyboard.cs b/Terminal.Gui/App/Keyboard/IKeyboard.cs index d0fbd023e..404217308 100644 --- a/Terminal.Gui/App/Keyboard/IKeyboard.cs +++ b/Terminal.Gui/App/Keyboard/IKeyboard.cs @@ -1,10 +1,9 @@ -#nullable enable namespace Terminal.Gui.App; /// /// Defines a contract for managing keyboard input and key bindings at the Application level. /// -/// This interface decouples keyboard handling state from the static class, +/// This interface decouples keyboard handling state from the static class, /// enabling parallelizable unit tests and better testability. /// /// @@ -14,7 +13,7 @@ public interface IKeyboard /// Sets the application instance that this keyboard handler is associated with. /// This provides access to application state without coupling to static Application class. /// - IApplication? Application { get; set; } + IApplication? App { get; set; } /// /// Called when the user presses a key (by the ). Raises the cancelable diff --git a/Terminal.Gui/App/Keyboard/KeyboardImpl.cs b/Terminal.Gui/App/Keyboard/KeyboardImpl.cs index 0741f4c53..e1be672e5 100644 --- a/Terminal.Gui/App/Keyboard/KeyboardImpl.cs +++ b/Terminal.Gui/App/Keyboard/KeyboardImpl.cs @@ -1,12 +1,9 @@ -#nullable enable -using System.Diagnostics; - namespace Terminal.Gui.App; /// /// INTERNAL: Implements to manage keyboard input and key bindings at the Application level. /// -/// This implementation decouples keyboard handling state from the static class, +/// This implementation decouples keyboard handling state from the static class, /// enabling parallelizable unit tests and better testability. /// /// @@ -28,7 +25,7 @@ internal class KeyboardImpl : IKeyboard private readonly Dictionary _commandImplementations = new (); /// - public IApplication? Application { get; set; } + public IApplication? App { get; set; } /// public KeyBindings KeyBindings { get; internal set; } = new (null); @@ -136,16 +133,16 @@ internal class KeyboardImpl : IKeyboard return true; } - if (Application?.Popover?.DispatchKeyDown (key) is true) + if (App?.Popover?.DispatchKeyDown (key) is true) { return true; } - if (Application?.Top is null) + if (App?.Current is null) { - if (Application?.TopLevels is { }) + if (App?.SessionStack is { }) { - foreach (Toplevel topLevel in Application.TopLevels.ToList ()) + foreach (Toplevel topLevel in App.SessionStack.ToList ()) { if (topLevel.NewKeyDownEvent (key)) { @@ -161,7 +158,7 @@ internal class KeyboardImpl : IKeyboard } else { - if (Application.Top.NewKeyDownEvent (key)) + if (App.Current.NewKeyDownEvent (key)) { return true; } @@ -179,7 +176,7 @@ internal class KeyboardImpl : IKeyboard /// public bool RaiseKeyUpEvent (Key key) { - if (Application?.Initialized != true) + if (App?.Initialized != true) { return true; } @@ -194,9 +191,9 @@ internal class KeyboardImpl : IKeyboard // TODO: Add Popover support - if (Application?.TopLevels is { }) + if (App?.SessionStack is { }) { - foreach (Toplevel topLevel in Application.TopLevels.ToList ()) + foreach (Toplevel topLevel in App.SessionStack.ToList ()) { if (topLevel.NewKeyUpEvent (key)) { @@ -294,7 +291,7 @@ internal class KeyboardImpl : IKeyboard Command.Quit, () => { - Application?.RequestStop (); + App?.RequestStop (); return true; } @@ -303,32 +300,32 @@ internal class KeyboardImpl : IKeyboard Command.Suspend, () => { - Application?.Driver?.Suspend (); + App?.Driver?.Suspend (); return true; } ); AddCommand ( Command.NextTabStop, - () => Application?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop)); + () => App?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop)); AddCommand ( Command.PreviousTabStop, - () => Application?.Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop)); + () => App?.Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop)); AddCommand ( Command.NextTabGroup, - () => Application?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup)); + () => App?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup)); AddCommand ( Command.PreviousTabGroup, - () => Application?.Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup)); + () => App?.Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup)); AddCommand ( Command.Refresh, () => { - Application?.LayoutAndDraw (true); + App?.LayoutAndDraw (true); return true; } @@ -338,7 +335,7 @@ internal class KeyboardImpl : IKeyboard Command.Arrange, () => { - View? viewToArrange = Application?.Navigation?.GetFocused (); + View? viewToArrange = App?.Navigation?.GetFocused (); // Go up the superview hierarchy and find the first that is not ViewArrangement.Fixed while (viewToArrange is { SuperView: { }, Arrangement: ViewArrangement.Fixed }) diff --git a/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs b/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs index 69a16bd49..df03c3187 100644 --- a/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs +++ b/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs @@ -1,8 +1,5 @@ -#nullable enable -using System; using System.Collections.Concurrent; using System.Diagnostics; -using Terminal.Gui.Drivers; namespace Terminal.Gui.App; @@ -30,6 +27,9 @@ public class ApplicationMainLoop : IApplicationMainLoop + public IApplication? App { get; private set; } + /// public ITimedEvents TimedEvents { @@ -82,7 +82,7 @@ public class ApplicationMainLoop : IApplicationMainLoop - /// Handles raising events and setting required draw status etc when changes + /// Handles raising events and setting required draw status etc when changes /// public IToplevelTransitionManager ToplevelTransitionManager = new ToplevelTransitionManager (); @@ -94,14 +94,17 @@ public class ApplicationMainLoop : IApplicationMainLoop /// /// + /// public void Initialize ( ITimedEvents timedEvents, ConcurrentQueue inputBuffer, IInputProcessor inputProcessor, IOutput consoleOutput, - IComponentFactory componentFactory + IComponentFactory componentFactory, + IApplication? app ) { + App = app; InputQueue = inputBuffer; Output = consoleOutput; InputProcessor = inputProcessor; @@ -116,10 +119,10 @@ public class ApplicationMainLoop : IApplicationMainLoop public void Iteration () { - Application.RaiseIteration (); + App?.RaiseIteration (); DateTime dt = DateTime.Now; - int timeAllowed = 1000 / Math.Max(1,(int)Application.MaximumIterationsPerSecond); + int timeAllowed = 1000 / Math.Max (1, (int)Application.MaximumIterationsPerSecond); IterationImpl (); @@ -139,14 +142,14 @@ public class ApplicationMainLoop : IApplicationMainLoop : IApplicationMainLoop : IApplicationMainLoop : IApplicationMainLoopType of raw input events processed by the loop, e.g. for cross-platform .NET driver public interface IApplicationMainLoop : IDisposable where TInputRecord : struct { + /// + /// The Application this loop is associated with. + /// + public IApplication? App { get; } + /// /// Gets the implementation that manages user-defined timeouts and periodic events. /// @@ -73,6 +77,7 @@ public interface IApplicationMainLoop : IDisposable where TInputRe /// The factory for creating driver-specific components. Used here to create the /// that tracks terminal size changes. /// + /// /// /// /// This method is called by during application startup @@ -98,7 +103,8 @@ public interface IApplicationMainLoop : IDisposable where TInputRe ConcurrentQueue inputQueue, IInputProcessor inputProcessor, IOutput output, - IComponentFactory componentFactory + IComponentFactory componentFactory, + IApplication? app ); /// diff --git a/Terminal.Gui/App/MainLoop/IMainLoopCoordinator.cs b/Terminal.Gui/App/MainLoop/IMainLoopCoordinator.cs index e08b2a742..c5321a2ea 100644 --- a/Terminal.Gui/App/MainLoop/IMainLoopCoordinator.cs +++ b/Terminal.Gui/App/MainLoop/IMainLoopCoordinator.cs @@ -16,6 +16,7 @@ public interface IMainLoopCoordinator /// /// Initializes all required subcomponents and starts the input thread. /// + /// /// /// This method: /// @@ -25,7 +26,7 @@ public interface IMainLoopCoordinator /// /// /// A task that completes when initialization is done - public Task StartInputTaskAsync (); + public Task StartInputTaskAsync (IApplication? app); /// /// Stops the input thread and performs cleanup. diff --git a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs index 45fd2a7d3..58b54c94e 100644 --- a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs +++ b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs @@ -46,24 +46,25 @@ internal class MainLoopCoordinator : IMainLoopCoordinator where TI private readonly ITimedEvents _timedEvents; private readonly SemaphoreSlim _startupSemaphore = new (0, 1); - private IInput _input; - private Task _inputTask; - private IOutput _output; - private DriverImpl _driver; + private IInput? _input; + private Task? _inputTask; + private IOutput? _output; + private DriverImpl? _driver; private bool _stopCalled; /// /// Starts the input loop thread in separate task (returning immediately). /// - public async Task StartInputTaskAsync () + /// The instance that is running the input loop. + public async Task StartInputTaskAsync (IApplication? app) { Logging.Trace ("Booting... ()"); - _inputTask = Task.Run (RunInput); + _inputTask = Task.Run (() => RunInput (app)); // Main loop is now booted on same thread as rest of users application - BootMainLoop (); + BootMainLoop (app); // Wait asynchronously for the semaphore or task failure. Task waitForSemaphore = _startupSemaphore.WaitAsync (); @@ -107,13 +108,13 @@ internal class MainLoopCoordinator : IMainLoopCoordinator where TI _stopCalled = true; _runCancellationTokenSource.Cancel (); - _output.Dispose (); + _output?.Dispose (); // Wait for input infinite loop to exit - _inputTask.Wait (); + _inputTask?.Wait (); } - private void BootMainLoop () + private void BootMainLoop (IApplication? app) { //Logging.Trace ($"_inputProcessor: {_inputProcessor}, _output: {_output}, _componentFactory: {_componentFactory}"); @@ -121,13 +122,13 @@ internal class MainLoopCoordinator : IMainLoopCoordinator where TI { // Instance must be constructed on the thread in which it is used. _output = _componentFactory.CreateOutput (); - _loop.Initialize (_timedEvents, _inputQueue, _inputProcessor, _output, _componentFactory); + _loop.Initialize (_timedEvents, _inputQueue, _inputProcessor, _output, _componentFactory, app); - BuildDriverIfPossible (); + BuildDriverIfPossible (app); } } - private void BuildDriverIfPossible () + private void BuildDriverIfPossible (IApplication? app) { if (_input != null && _output != null) @@ -139,7 +140,7 @@ internal class MainLoopCoordinator : IMainLoopCoordinator where TI _loop.AnsiRequestScheduler, _loop.SizeMonitor); - Application.Driver = _driver; + app!.Driver = _driver; _startupSemaphore.Release (); Logging.Trace ($"Driver: _input: {_input}, _output: {_output}"); @@ -149,7 +150,8 @@ internal class MainLoopCoordinator : IMainLoopCoordinator where TI /// /// INTERNAL: Runs the IInput read loop on a new thread called the "Input Thread". /// - private void RunInput () + /// + private void RunInput (IApplication? app) { try { @@ -165,7 +167,7 @@ internal class MainLoopCoordinator : IMainLoopCoordinator where TI impl.InputImpl = _input; } - BuildDriverIfPossible (); + BuildDriverIfPossible (app); } try diff --git a/Terminal.Gui/App/MainLoop/MainLoopSyncContext.cs b/Terminal.Gui/App/MainLoop/MainLoopSyncContext.cs index f67f5ec81..b9375ccca 100644 --- a/Terminal.Gui/App/MainLoop/MainLoopSyncContext.cs +++ b/Terminal.Gui/App/MainLoop/MainLoopSyncContext.cs @@ -1,42 +1,43 @@ +#nullable disable namespace Terminal.Gui.App; -/// -/// provides the sync context set while executing code in Terminal.Gui, to let -/// users use async/await on their code -/// -internal sealed class MainLoopSyncContext : SynchronizationContext -{ - public override SynchronizationContext CreateCopy () { return new MainLoopSyncContext (); } +///// +///// provides the sync context set while executing code in Terminal.Gui, to let +///// users use async/await on their code +///// +//internal sealed class MainLoopSyncContext : SynchronizationContext +//{ +// public override SynchronizationContext CreateCopy () { return new MainLoopSyncContext (); } - public override void Post (SendOrPostCallback d, object state) - { - // Queue the task using the modern architecture - ApplicationImpl.Instance.Invoke (() => { d (state); }); - } +// public override void Post (SendOrPostCallback d, object state) +// { +// // Queue the task using the modern architecture +// ApplicationImpl.Instance.Invoke (() => { d (state); }); +// } - //_mainLoop.Driver.Wakeup (); - public override void Send (SendOrPostCallback d, object state) - { - if (Thread.CurrentThread.ManagedThreadId == Application.MainThreadId) - { - d (state); - } - else - { - var wasExecuted = false; +// //_mainLoop.Driver.Wakeup (); +// public override void Send (SendOrPostCallback d, object state) +// { +// if (Thread.CurrentThread.ManagedThreadId == Application.MainThreadId) +// { +// d (state); +// } +// else +// { +// var wasExecuted = false; - Application.Invoke ( - () => - { - d (state); - wasExecuted = true; - } - ); +// ApplicationImpl.Instance.Invoke ( +// () => +// { +// d (state); +// wasExecuted = true; +// } +// ); - while (!wasExecuted) - { - Thread.Sleep (15); - } - } - } -} +// while (!wasExecuted) +// { +// Thread.Sleep (15); +// } +// } +// } +//} diff --git a/Terminal.Gui/App/Mouse/IMouse.cs b/Terminal.Gui/App/Mouse/IMouse.cs index a4bc2c2e8..ed01dbf1b 100644 --- a/Terminal.Gui/App/Mouse/IMouse.cs +++ b/Terminal.Gui/App/Mouse/IMouse.cs @@ -1,4 +1,3 @@ -#nullable enable using System.ComponentModel; namespace Terminal.Gui.App; @@ -6,7 +5,7 @@ namespace Terminal.Gui.App; /// /// Defines a contract for mouse event handling and state management in a Terminal.Gui application. /// -/// This interface allows for decoupling of mouse-related functionality from the static class, +/// This interface allows for decoupling of mouse-related functionality from the static class, /// enabling better testability and parallel test execution. /// /// @@ -16,18 +15,13 @@ public interface IMouse : IMouseGrabHandler /// Sets the application instance that this mouse handler is associated with. /// This provides access to application state without coupling to static Application class. /// - IApplication? Application { get; set; } + IApplication? App { get; set; } /// /// Gets or sets the last known position of the mouse. /// Point? LastMousePosition { get; set; } - /// - /// Gets the most recent position of the mouse. - /// - Point? GetLastMousePosition (); - /// /// Gets or sets whether the mouse is disabled. The mouse is enabled by default. /// diff --git a/Terminal.Gui/App/Mouse/IMouseGrabHandler.cs b/Terminal.Gui/App/Mouse/IMouseGrabHandler.cs index 06fd0e626..31bdebeaf 100644 --- a/Terminal.Gui/App/Mouse/IMouseGrabHandler.cs +++ b/Terminal.Gui/App/Mouse/IMouseGrabHandler.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.App; /// diff --git a/Terminal.Gui/App/Mouse/MouseGrabHandler.cs b/Terminal.Gui/App/Mouse/MouseGrabHandler.cs index 3fe7ab689..175d371ed 100644 --- a/Terminal.Gui/App/Mouse/MouseGrabHandler.cs +++ b/Terminal.Gui/App/Mouse/MouseGrabHandler.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.App; /// diff --git a/Terminal.Gui/App/Mouse/MouseImpl.cs b/Terminal.Gui/App/Mouse/MouseImpl.cs index 78d662dde..29116db0a 100644 --- a/Terminal.Gui/App/Mouse/MouseImpl.cs +++ b/Terminal.Gui/App/Mouse/MouseImpl.cs @@ -1,13 +1,11 @@ -#nullable enable using System.ComponentModel; -using System.Diagnostics; namespace Terminal.Gui.App; /// /// INTERNAL: Implements to manage mouse event handling and state. /// -/// This class holds all mouse-related state that was previously in the static class, +/// This class holds all mouse-related state that was previously in the static class, /// enabling better testability and parallel test execution. /// /// @@ -19,14 +17,11 @@ internal class MouseImpl : IMouse public MouseImpl () { } /// - public IApplication? Application { get; set; } + public IApplication? App { get; set; } /// public Point? LastMousePosition { get; set; } - /// - public Point? GetLastMousePosition () { return LastMousePosition; } - /// public bool IsMouseDisabled { get; set; } @@ -57,7 +52,7 @@ internal class MouseImpl : IMouse public void RaiseMouseEvent (MouseEventArgs mouseEvent) { //Debug.Assert (App.Application.MainThreadId == Thread.CurrentThread.ManagedThreadId); - if (Application?.Initialized is true) + if (App?.Initialized is true) { // LastMousePosition is only set if the application is initialized. LastMousePosition = mouseEvent.ScreenPosition; @@ -72,9 +67,9 @@ internal class MouseImpl : IMouse //Debug.Assert (mouseEvent.Position == mouseEvent.ScreenPosition); mouseEvent.Position = mouseEvent.ScreenPosition; - List currentViewsUnderMouse = View.GetViewsUnderLocation (mouseEvent.ScreenPosition, ViewportSettingsFlags.TransparentMouse); + List? currentViewsUnderMouse = App?.Current?.GetViewsUnderLocation (mouseEvent.ScreenPosition, ViewportSettingsFlags.TransparentMouse); - View? deepestViewUnderMouse = currentViewsUnderMouse.LastOrDefault (); + View? deepestViewUnderMouse = currentViewsUnderMouse?.LastOrDefault (); if (deepestViewUnderMouse is { }) { @@ -96,7 +91,7 @@ internal class MouseImpl : IMouse // Dismiss the Popover if the user presses mouse outside of it if (mouseEvent.IsPressed - && Application?.Popover?.GetActivePopover () as View is { Visible: true } visiblePopover + && App?.Popover?.GetActivePopover () as View is { Visible: true } visiblePopover && View.IsInHierarchy (visiblePopover, deepestViewUnderMouse, includeAdornments: true) is false) { ApplicationPopover.HideWithQuitCommand (visiblePopover); @@ -119,9 +114,9 @@ internal class MouseImpl : IMouse return; } - // if the mouse is outside the Application.Top or Application.Popover hierarchy, we don't want to + // if the mouse is outside the Application.Current or Popover hierarchy, we don't want to // send the mouse event to the deepest view under the mouse. - if (!View.IsInHierarchy (Application?.Top, deepestViewUnderMouse, true) && !View.IsInHierarchy (Application?.Popover?.GetActivePopover () as View, deepestViewUnderMouse, true)) + if (!View.IsInHierarchy (App?.Current, deepestViewUnderMouse, true) && !View.IsInHierarchy (App?.Popover?.GetActivePopover () as View, deepestViewUnderMouse, true)) { return; } @@ -161,7 +156,10 @@ internal class MouseImpl : IMouse return; } - RaiseMouseEnterLeaveEvents (viewMouseEvent.ScreenPosition, currentViewsUnderMouse); + if (currentViewsUnderMouse is { }) + { + RaiseMouseEnterLeaveEvents (viewMouseEvent.ScreenPosition, currentViewsUnderMouse); + } while (deepestViewUnderMouse.NewMouseEvent (viewMouseEvent) is not true && MouseGrabView is not { }) { diff --git a/Terminal.Gui/App/PopoverBaseImpl.cs b/Terminal.Gui/App/PopoverBaseImpl.cs index 1e9aacb71..41258401f 100644 --- a/Terminal.Gui/App/PopoverBaseImpl.cs +++ b/Terminal.Gui/App/PopoverBaseImpl.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.App; @@ -74,8 +73,18 @@ public abstract class PopoverBaseImpl : View, IPopover } } + private Toplevel? _current; + /// - public Toplevel? Toplevel { get; set; } + public Toplevel? Current + { + get => _current; + set + { + _current = value; + App ??= _current?.App; + } + } /// /// Called when the property is changing. @@ -100,14 +109,17 @@ public abstract class PopoverBaseImpl : View, IPopover { // Whenever visible is changing to true, we need to resize; // it's our only chance because we don't get laid out until we're visible - Layout (Application.Screen.Size); + if (App is { }) + { + Layout (App.Screen.Size); + } } else { // Whenever visible is changing to false, we need to reset the focus - if (ApplicationNavigation.IsInHierarchy (this, Application.Navigation?.GetFocused ())) + if (ApplicationNavigation.IsInHierarchy (this, App?.Navigation?.GetFocused ())) { - Application.Navigation?.SetFocused (Application.Top?.MostFocused); + App?.Navigation?.SetFocused (App?.Current?.MostFocused); } } diff --git a/Terminal.Gui/App/SessionToken.cs b/Terminal.Gui/App/SessionToken.cs index d6466ed3f..6505727bb 100644 --- a/Terminal.Gui/App/SessionToken.cs +++ b/Terminal.Gui/App/SessionToken.cs @@ -10,7 +10,7 @@ public class SessionToken : IDisposable public SessionToken (Toplevel view) { Toplevel = view; } /// The belonging to this . - public Toplevel Toplevel { get; internal set; } + public Toplevel? Toplevel { get; internal set; } /// Releases all resource used by the object. /// Call when you are finished using the . diff --git a/Terminal.Gui/App/Timeout/ITimedEvents.cs b/Terminal.Gui/App/Timeout/ITimedEvents.cs index aa9499520..376f71f51 100644 --- a/Terminal.Gui/App/Timeout/ITimedEvents.cs +++ b/Terminal.Gui/App/Timeout/ITimedEvents.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.App; /// @@ -49,4 +48,14 @@ public interface ITimedEvents /// for each timeout that is not actively executing. /// SortedList Timeouts { get; } + + /// + /// Gets the timeout for the specified event. + /// + /// The token of the event. + /// The for the event, or if the event is not found. + TimeSpan? GetTimeout (object token); + + /// Stops and removes all timed events. + void StopAll (); } diff --git a/Terminal.Gui/App/Timeout/LogarithmicTimeout.cs b/Terminal.Gui/App/Timeout/LogarithmicTimeout.cs index eacf09607..25690eb24 100644 --- a/Terminal.Gui/App/Timeout/LogarithmicTimeout.cs +++ b/Terminal.Gui/App/Timeout/LogarithmicTimeout.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.App; /// Implements a logarithmic increasing timeout. diff --git a/Terminal.Gui/App/Timeout/SmoothAcceleratingTimeout.cs b/Terminal.Gui/App/Timeout/SmoothAcceleratingTimeout.cs index 962ce4b19..7a11dcddc 100644 --- a/Terminal.Gui/App/Timeout/SmoothAcceleratingTimeout.cs +++ b/Terminal.Gui/App/Timeout/SmoothAcceleratingTimeout.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.App; /// diff --git a/Terminal.Gui/App/Timeout/TimedEvents.cs b/Terminal.Gui/App/Timeout/TimedEvents.cs index da1dcc5c0..09e008b51 100644 --- a/Terminal.Gui/App/Timeout/TimedEvents.cs +++ b/Terminal.Gui/App/Timeout/TimedEvents.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics; namespace Terminal.Gui.App; @@ -145,6 +144,22 @@ public class TimedEvents : ITimedEvents return false; } + /// + public TimeSpan? GetTimeout (object token) + { + lock (_timeoutsLockToken) + { + int idx = _timeouts.IndexOfValue ((token as Timeout)!); + + if (idx == -1) + { + return null; + } + + return _timeouts.Values [idx].Span; + } + } + private void AddTimeout (TimeSpan time, Timeout timeout) { lock (_timeoutsLockToken) @@ -202,7 +217,7 @@ public class TimedEvents : ITimedEvents { if (k < now) { - if (timeout.Callback ()) + if (timeout.Callback! ()) { AddTimeout (timeout.Span, timeout); } @@ -216,4 +231,13 @@ public class TimedEvents : ITimedEvents } } } + + /// + public void StopAll () + { + lock (_timeoutsLockToken) + { + _timeouts.Clear (); + } + } } diff --git a/Terminal.Gui/App/Timeout/Timeout.cs b/Terminal.Gui/App/Timeout/Timeout.cs index c3054869f..226dc9e2f 100644 --- a/Terminal.Gui/App/Timeout/Timeout.cs +++ b/Terminal.Gui/App/Timeout/Timeout.cs @@ -20,7 +20,7 @@ public class Timeout /// rescheduled and invoked again after the same interval. /// If the callback returns , the timeout will be removed and not invoked again. /// - public Func Callback { get; set; } + public Func? Callback { get; set; } /// /// Gets or sets the time interval to wait before invoking the . diff --git a/Terminal.Gui/App/Toplevel/IToplevelTransitionManager.cs b/Terminal.Gui/App/Toplevel/IToplevelTransitionManager.cs index 7b69f8c9b..26c44129b 100644 --- a/Terminal.Gui/App/Toplevel/IToplevelTransitionManager.cs +++ b/Terminal.Gui/App/Toplevel/IToplevelTransitionManager.cs @@ -7,14 +7,16 @@ public interface IToplevelTransitionManager { /// - /// Raises the event on the current top level + /// Raises the event on tahe current top level /// if it has not been raised before now. /// - void RaiseReadyEventIfNeeded (); + /// + void RaiseReadyEventIfNeeded (IApplication? app); /// /// Handles any state change needed when the application top changes e.g. /// setting redraw flags /// - void HandleTopMaybeChanging (); + /// + void HandleTopMaybeChanging (IApplication? app); } diff --git a/Terminal.Gui/App/Toplevel/ToplevelTransitionManager.cs b/Terminal.Gui/App/Toplevel/ToplevelTransitionManager.cs index 282dde040..10166b450 100644 --- a/Terminal.Gui/App/Toplevel/ToplevelTransitionManager.cs +++ b/Terminal.Gui/App/Toplevel/ToplevelTransitionManager.cs @@ -1,6 +1,3 @@ -#nullable enable -using Terminal.Gui.Drivers; - namespace Terminal.Gui.App; /// @@ -12,10 +9,11 @@ public class ToplevelTransitionManager : IToplevelTransitionManager private View? _lastTop; + /// /// - public void RaiseReadyEventIfNeeded () + public void RaiseReadyEventIfNeeded (IApplication? app) { - Toplevel? top = Application.Top; + Toplevel? top = app?.Current; if (top != null && !_readiedTopLevels.Contains (top)) { @@ -27,16 +25,17 @@ public class ToplevelTransitionManager : IToplevelTransitionManager } } + /// /// - public void HandleTopMaybeChanging () + public void HandleTopMaybeChanging (IApplication? app) { - Toplevel? newTop = Application.Top; + Toplevel? newTop = app?.Current; if (_lastTop != null && _lastTop != newTop && newTop != null) { newTop.SetNeedsDraw (); } - _lastTop = Application.Top; + _lastTop = app?.Current; } } diff --git a/Terminal.Gui/Configuration/AppSettingsScope.cs b/Terminal.Gui/Configuration/AppSettingsScope.cs index 35594cacb..66c6af7f0 100644 --- a/Terminal.Gui/Configuration/AppSettingsScope.cs +++ b/Terminal.Gui/Configuration/AppSettingsScope.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Terminal.Gui.Configuration; diff --git a/Terminal.Gui/Configuration/AttributeJsonConverter.cs b/Terminal.Gui/Configuration/AttributeJsonConverter.cs index 34ee281c5..2a00b556f 100644 --- a/Terminal.Gui/Configuration/AttributeJsonConverter.cs +++ b/Terminal.Gui/Configuration/AttributeJsonConverter.cs @@ -9,7 +9,7 @@ namespace Terminal.Gui.Configuration; internal class AttributeJsonConverter : JsonConverter { - private static AttributeJsonConverter _instance; + private static AttributeJsonConverter? _instance; /// public static AttributeJsonConverter Instance @@ -63,7 +63,7 @@ internal class AttributeJsonConverter : JsonConverter throw new JsonException ($"{propertyName}: Unexpected token when parsing Attribute: {reader.TokenType}."); } - propertyName = reader.GetString (); + propertyName = reader.GetString ()!; reader.Read (); var property = $"\"{reader.GetString ()}\""; diff --git a/Terminal.Gui/Configuration/ColorJsonConverter.cs b/Terminal.Gui/Configuration/ColorJsonConverter.cs index 70d6ca7e7..8959a9a10 100644 --- a/Terminal.Gui/Configuration/ColorJsonConverter.cs +++ b/Terminal.Gui/Configuration/ColorJsonConverter.cs @@ -15,7 +15,7 @@ namespace Terminal.Gui.Configuration; /// internal class ColorJsonConverter : JsonConverter { - private static ColorJsonConverter _instance; + private static ColorJsonConverter? _instance; /// Singleton public static ColorJsonConverter Instance diff --git a/Terminal.Gui/Configuration/ConcurrentDictionaryJsonConverter.cs b/Terminal.Gui/Configuration/ConcurrentDictionaryJsonConverter.cs index a33f9181a..a5d186184 100644 --- a/Terminal.Gui/Configuration/ConcurrentDictionaryJsonConverter.cs +++ b/Terminal.Gui/Configuration/ConcurrentDictionaryJsonConverter.cs @@ -1,3 +1,4 @@ +#nullable disable using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Text.Json; diff --git a/Terminal.Gui/Configuration/ConfigLocations.cs b/Terminal.Gui/Configuration/ConfigLocations.cs index 8f348fa8c..c41aacde9 100644 --- a/Terminal.Gui/Configuration/ConfigLocations.cs +++ b/Terminal.Gui/Configuration/ConfigLocations.cs @@ -1,5 +1,4 @@ -#nullable enable -namespace Terminal.Gui.Configuration; +namespace Terminal.Gui.Configuration; /// /// Describes the location of the configuration settings. The constants can be combined (bitwise) to specify multiple diff --git a/Terminal.Gui/Configuration/ConfigProperty.cs b/Terminal.Gui/Configuration/ConfigProperty.cs index 0442a3b6f..0a5d7dc8f 100644 --- a/Terminal.Gui/Configuration/ConfigProperty.cs +++ b/Terminal.Gui/Configuration/ConfigProperty.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; diff --git a/Terminal.Gui/Configuration/ConfigurationManager.cs b/Terminal.Gui/Configuration/ConfigurationManager.cs index 6f0364ad5..b8d3cdf5d 100644 --- a/Terminal.Gui/Configuration/ConfigurationManager.cs +++ b/Terminal.Gui/Configuration/ConfigurationManager.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Collections.Frozen; +using System.Collections.Frozen; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; diff --git a/Terminal.Gui/Configuration/ConfigurationManagerEventArgs.cs b/Terminal.Gui/Configuration/ConfigurationManagerEventArgs.cs index 24a325ab7..ea03daff1 100644 --- a/Terminal.Gui/Configuration/ConfigurationManagerEventArgs.cs +++ b/Terminal.Gui/Configuration/ConfigurationManagerEventArgs.cs @@ -1,6 +1,4 @@ -#nullable enable - -namespace Terminal.Gui.Configuration; +namespace Terminal.Gui.Configuration; /// Event arguments for the events. public class ConfigurationManagerEventArgs : EventArgs diff --git a/Terminal.Gui/Configuration/ConfigurationManagerNotEnabledException.cs b/Terminal.Gui/Configuration/ConfigurationManagerNotEnabledException.cs index 45206c876..f9fc6b62e 100644 --- a/Terminal.Gui/Configuration/ConfigurationManagerNotEnabledException.cs +++ b/Terminal.Gui/Configuration/ConfigurationManagerNotEnabledException.cs @@ -1,5 +1,4 @@ -#nullable enable -namespace Terminal.Gui.Configuration; +namespace Terminal.Gui.Configuration; /// /// The exception that is thrown when a API is called but the configuration manager is not enabled. diff --git a/Terminal.Gui/Configuration/ConfigurationPropertyAttribute.cs b/Terminal.Gui/Configuration/ConfigurationPropertyAttribute.cs index 2f1218aa9..3911e9f88 100644 --- a/Terminal.Gui/Configuration/ConfigurationPropertyAttribute.cs +++ b/Terminal.Gui/Configuration/ConfigurationPropertyAttribute.cs @@ -1,6 +1,4 @@ -#nullable enable - -namespace Terminal.Gui.Configuration; +namespace Terminal.Gui.Configuration; /// An attribute indicating a property is managed by . /// diff --git a/Terminal.Gui/Configuration/DeepCloner.cs b/Terminal.Gui/Configuration/DeepCloner.cs index 0d918625c..3a6caec52 100644 --- a/Terminal.Gui/Configuration/DeepCloner.cs +++ b/Terminal.Gui/Configuration/DeepCloner.cs @@ -1,4 +1,4 @@ -#nullable enable + using System.Collections; using System.Collections.Concurrent; diff --git a/Terminal.Gui/Configuration/DictionaryJsonConverter.cs b/Terminal.Gui/Configuration/DictionaryJsonConverter.cs index bfd940d33..2cdbbfd48 100644 --- a/Terminal.Gui/Configuration/DictionaryJsonConverter.cs +++ b/Terminal.Gui/Configuration/DictionaryJsonConverter.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +#nullable disable +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/Terminal.Gui/Configuration/KeyCodeJsonConverter.cs b/Terminal.Gui/Configuration/KeyCodeJsonConverter.cs index 04d8d9765..d69ea9479 100644 --- a/Terminal.Gui/Configuration/KeyCodeJsonConverter.cs +++ b/Terminal.Gui/Configuration/KeyCodeJsonConverter.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +#nullable disable +using System.Text.Json; using System.Text.Json.Serialization; namespace Terminal.Gui.Configuration; diff --git a/Terminal.Gui/Configuration/KeyJsonConverter.cs b/Terminal.Gui/Configuration/KeyJsonConverter.cs index 01413c432..1e723c8b7 100644 --- a/Terminal.Gui/Configuration/KeyJsonConverter.cs +++ b/Terminal.Gui/Configuration/KeyJsonConverter.cs @@ -9,7 +9,7 @@ public class KeyJsonConverter : JsonConverter /// public override Key Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return Key.TryParse (reader.GetString (), out Key key) ? key : Key.Empty; + return Key.TryParse (reader.GetString ()!, out Key key) ? key : Key.Empty; } /// diff --git a/Terminal.Gui/Configuration/RuneJsonConverter.cs b/Terminal.Gui/Configuration/RuneJsonConverter.cs index 8fc7f9f7b..48ec57518 100644 --- a/Terminal.Gui/Configuration/RuneJsonConverter.cs +++ b/Terminal.Gui/Configuration/RuneJsonConverter.cs @@ -26,7 +26,7 @@ internal class RuneJsonConverter : JsonConverter { case JsonTokenType.String: { - string value = reader.GetString (); + string? value = reader.GetString (); int first = RuneExtensions.MaxUnicodeCodePoint + 1; int second = RuneExtensions.MaxUnicodeCodePoint + 1; diff --git a/Terminal.Gui/Configuration/SchemeJsonConverter.cs b/Terminal.Gui/Configuration/SchemeJsonConverter.cs index cabeefacf..fe363dc54 100644 --- a/Terminal.Gui/Configuration/SchemeJsonConverter.cs +++ b/Terminal.Gui/Configuration/SchemeJsonConverter.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/Terminal.Gui/Configuration/SchemeManager.cs b/Terminal.Gui/Configuration/SchemeManager.cs index 4fa1fd809..0023a0824 100644 --- a/Terminal.Gui/Configuration/SchemeManager.cs +++ b/Terminal.Gui/Configuration/SchemeManager.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Collections.Immutable; +using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; diff --git a/Terminal.Gui/Configuration/Scope.cs b/Terminal.Gui/Configuration/Scope.cs index 88d637264..a3fe4f069 100644 --- a/Terminal.Gui/Configuration/Scope.cs +++ b/Terminal.Gui/Configuration/Scope.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; diff --git a/Terminal.Gui/Configuration/ScopeJsonConverter.cs b/Terminal.Gui/Configuration/ScopeJsonConverter.cs index 034e904ae..94ed04e8f 100644 --- a/Terminal.Gui/Configuration/ScopeJsonConverter.cs +++ b/Terminal.Gui/Configuration/ScopeJsonConverter.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/Terminal.Gui/Configuration/SettingsScope.cs b/Terminal.Gui/Configuration/SettingsScope.cs index 5feeb1131..de9dbdeb8 100644 --- a/Terminal.Gui/Configuration/SettingsScope.cs +++ b/Terminal.Gui/Configuration/SettingsScope.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Terminal.Gui.Configuration; diff --git a/Terminal.Gui/Configuration/SourcesManager.cs b/Terminal.Gui/Configuration/SourcesManager.cs index 729086541..541d452a2 100644 --- a/Terminal.Gui/Configuration/SourcesManager.cs +++ b/Terminal.Gui/Configuration/SourcesManager.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Diagnostics; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json; diff --git a/Terminal.Gui/Configuration/ThemeManager.cs b/Terminal.Gui/Configuration/ThemeManager.cs index b184ba9ba..a234ac869 100644 --- a/Terminal.Gui/Configuration/ThemeManager.cs +++ b/Terminal.Gui/Configuration/ThemeManager.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; diff --git a/Terminal.Gui/Configuration/ThemeScope.cs b/Terminal.Gui/Configuration/ThemeScope.cs index 541cb80f6..29bef03d8 100644 --- a/Terminal.Gui/Configuration/ThemeScope.cs +++ b/Terminal.Gui/Configuration/ThemeScope.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Terminal.Gui.Configuration; diff --git a/Terminal.Gui/Drawing/Attribute.cs b/Terminal.Gui/Drawing/Attribute.cs index bc7005c41..0b794f20c 100644 --- a/Terminal.Gui/Drawing/Attribute.cs +++ b/Terminal.Gui/Drawing/Attribute.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Numerics; +using System.Numerics; using System.Text.Json.Serialization; namespace Terminal.Gui.Drawing; diff --git a/Terminal.Gui/Drawing/Cell.cs b/Terminal.Gui/Drawing/Cell.cs index e72a7837e..2fa98bef1 100644 --- a/Terminal.Gui/Drawing/Cell.cs +++ b/Terminal.Gui/Drawing/Cell.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.Drawing; diff --git a/Terminal.Gui/Drawing/Color/AnsiColorCode.cs b/Terminal.Gui/Drawing/Color/AnsiColorCode.cs index 56bc857a8..28d1fafb7 100644 --- a/Terminal.Gui/Drawing/Color/AnsiColorCode.cs +++ b/Terminal.Gui/Drawing/Color/AnsiColorCode.cs @@ -1,3 +1,4 @@ +// ReSharper disable InconsistentNaming namespace Terminal.Gui.Drawing; /// diff --git a/Terminal.Gui/Drawing/Color/AnsiColorNameResolver.cs b/Terminal.Gui/Drawing/Color/AnsiColorNameResolver.cs index aae4a6da5..5a0ebc827 100644 --- a/Terminal.Gui/Drawing/Color/AnsiColorNameResolver.cs +++ b/Terminal.Gui/Drawing/Color/AnsiColorNameResolver.cs @@ -1,5 +1,3 @@ -#nullable enable - using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; diff --git a/Terminal.Gui/Drawing/Color/Color.ColorExtensions.cs b/Terminal.Gui/Drawing/Color/Color.ColorExtensions.cs index 84e5f089a..a0b23b545 100644 --- a/Terminal.Gui/Drawing/Color/Color.ColorExtensions.cs +++ b/Terminal.Gui/Drawing/Color/Color.ColorExtensions.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Frozen; using ColorHelper; diff --git a/Terminal.Gui/Drawing/Color/Color.ColorParseException.cs b/Terminal.Gui/Drawing/Color/Color.ColorParseException.cs index 97595db04..ac1da5d5f 100644 --- a/Terminal.Gui/Drawing/Color/Color.ColorParseException.cs +++ b/Terminal.Gui/Drawing/Color/Color.ColorParseException.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui.Drawing; diff --git a/Terminal.Gui/Drawing/Color/Color.Formatting.cs b/Terminal.Gui/Drawing/Color/Color.Formatting.cs index 15e0dccb9..89082867b 100644 --- a/Terminal.Gui/Drawing/Color/Color.Formatting.cs +++ b/Terminal.Gui/Drawing/Color/Color.Formatting.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Runtime.CompilerServices; diff --git a/Terminal.Gui/Drawing/Color/Color.Operators.cs b/Terminal.Gui/Drawing/Color/Color.Operators.cs index 831f32bab..b8d33bd4a 100644 --- a/Terminal.Gui/Drawing/Color/Color.Operators.cs +++ b/Terminal.Gui/Drawing/Color/Color.Operators.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics.Contracts; using System.Numerics; diff --git a/Terminal.Gui/Drawing/Color/Color.cs b/Terminal.Gui/Drawing/Color/Color.cs index 995249c13..e83a05a73 100644 --- a/Terminal.Gui/Drawing/Color/Color.cs +++ b/Terminal.Gui/Drawing/Color/Color.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Frozen; using System.Globalization; using System.Numerics; diff --git a/Terminal.Gui/Drawing/Color/ColorModel.cs b/Terminal.Gui/Drawing/Color/ColorModel.cs index 6af865a9c..158c03236 100644 --- a/Terminal.Gui/Drawing/Color/ColorModel.cs +++ b/Terminal.Gui/Drawing/Color/ColorModel.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace Terminal.Gui.Drawing; /// diff --git a/Terminal.Gui/Drawing/Color/ColorStrings.cs b/Terminal.Gui/Drawing/Color/ColorStrings.cs index 705ea13e4..8f70c6a9e 100644 --- a/Terminal.Gui/Drawing/Color/ColorStrings.cs +++ b/Terminal.Gui/Drawing/Color/ColorStrings.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Globalization; namespace Terminal.Gui.Drawing; diff --git a/Terminal.Gui/Drawing/Color/IColorNameResolver.cs b/Terminal.Gui/Drawing/Color/IColorNameResolver.cs index 36881adb3..14e44718f 100644 --- a/Terminal.Gui/Drawing/Color/IColorNameResolver.cs +++ b/Terminal.Gui/Drawing/Color/IColorNameResolver.cs @@ -1,5 +1,3 @@ -#nullable enable - using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui.Drawing; diff --git a/Terminal.Gui/Drawing/Color/ICustomColorFormatter.cs b/Terminal.Gui/Drawing/Color/ICustomColorFormatter.cs index 425a02441..3bcd14919 100644 --- a/Terminal.Gui/Drawing/Color/ICustomColorFormatter.cs +++ b/Terminal.Gui/Drawing/Color/ICustomColorFormatter.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Drawing; /// An interface to support custom formatting and parsing of values. diff --git a/Terminal.Gui/Drawing/Color/MultiStandardColorNameResolver.cs b/Terminal.Gui/Drawing/Color/MultiStandardColorNameResolver.cs index d409950eb..36ed50910 100644 --- a/Terminal.Gui/Drawing/Color/MultiStandardColorNameResolver.cs +++ b/Terminal.Gui/Drawing/Color/MultiStandardColorNameResolver.cs @@ -1,5 +1,3 @@ -#nullable enable - using System.Collections.Frozen; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; diff --git a/Terminal.Gui/Drawing/Color/StandardColors.cs b/Terminal.Gui/Drawing/Color/StandardColors.cs index 05adfdb30..7e5b83831 100644 --- a/Terminal.Gui/Drawing/Color/StandardColors.cs +++ b/Terminal.Gui/Drawing/Color/StandardColors.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Frozen; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; diff --git a/Terminal.Gui/Drawing/Color/StandardColorsNameResolver.cs b/Terminal.Gui/Drawing/Color/StandardColorsNameResolver.cs index daae80b9e..0c8a4257a 100644 --- a/Terminal.Gui/Drawing/Color/StandardColorsNameResolver.cs +++ b/Terminal.Gui/Drawing/Color/StandardColorsNameResolver.cs @@ -1,5 +1,3 @@ -#nullable enable - using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui.Drawing; @@ -17,4 +15,4 @@ public class StandardColorsNameResolver : IColorNameResolver /// public bool TryNameColor (Color color, [NotNullWhen (true)] out string? name) => StandardColors.TryNameColor (color, out name); -} \ No newline at end of file +} diff --git a/Terminal.Gui/Drawing/Glyphs.cs b/Terminal.Gui/Drawing/Glyphs.cs index 28eb3a546..71336009d 100644 --- a/Terminal.Gui/Drawing/Glyphs.cs +++ b/Terminal.Gui/Drawing/Glyphs.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.Drawing; /// Defines the standard set of glyphs used to draw checkboxes, lines, borders, etc... diff --git a/Terminal.Gui/Drawing/LineCanvas/IntersectionDefinition.cs b/Terminal.Gui/Drawing/LineCanvas/IntersectionDefinition.cs index a03f8bca8..9f2c77df7 100644 --- a/Terminal.Gui/Drawing/LineCanvas/IntersectionDefinition.cs +++ b/Terminal.Gui/Drawing/LineCanvas/IntersectionDefinition.cs @@ -17,4 +17,4 @@ internal class IntersectionDefinition /// Defines how position relates to . internal IntersectionType Type { get; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Drawing/LineCanvas/IntersectionRuneType.cs b/Terminal.Gui/Drawing/LineCanvas/IntersectionRuneType.cs index b32310fa7..c3a0043f7 100644 --- a/Terminal.Gui/Drawing/LineCanvas/IntersectionRuneType.cs +++ b/Terminal.Gui/Drawing/LineCanvas/IntersectionRuneType.cs @@ -16,4 +16,4 @@ internal enum IntersectionRuneType Cross, HLine, VLine -} \ No newline at end of file +} diff --git a/Terminal.Gui/Drawing/LineCanvas/IntersectionType.cs b/Terminal.Gui/Drawing/LineCanvas/IntersectionType.cs index 87a051e55..e0cecf815 100644 --- a/Terminal.Gui/Drawing/LineCanvas/IntersectionType.cs +++ b/Terminal.Gui/Drawing/LineCanvas/IntersectionType.cs @@ -25,4 +25,4 @@ internal enum IntersectionType /// A line exists at this point who has 0 length Dot -} \ No newline at end of file +} diff --git a/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs b/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs index 637b61861..c426b6896 100644 --- a/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs +++ b/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Runtime.InteropServices; @@ -181,7 +180,7 @@ public class LineCanvas : IDisposable } // Safe as long as the list is not modified while the span is in use. ReadOnlySpan intersects = CollectionsMarshal.AsSpan(intersectionsBufferList); - Cell? cell = GetCellForIntersects (Application.Driver, intersects); + Cell? cell = GetCellForIntersects (intersects); // TODO: Can we skip the whole nested looping if _exclusionRegion is null? if (cell is { } && _exclusionRegion?.Contains (x, y) is null or false) { @@ -223,7 +222,7 @@ public class LineCanvas : IDisposable .Where (i => i is not null) .ToArray (); - Rune? rune = GetRuneForIntersects (Application.Driver, intersects); + Rune? rune = GetRuneForIntersects (intersects); if (rune is { } && _exclusionRegion?.Contains (x, y) is null or false) { @@ -402,7 +401,7 @@ public class LineCanvas : IDisposable // TODO: Add other resolvers }; - private Cell? GetCellForIntersects (IDriver? driver, ReadOnlySpan intersects) + private Cell? GetCellForIntersects (ReadOnlySpan intersects) { if (intersects.IsEmpty) { @@ -410,7 +409,7 @@ public class LineCanvas : IDisposable } var cell = new Cell (); - Rune? rune = GetRuneForIntersects (driver, intersects); + Rune? rune = GetRuneForIntersects (intersects); if (rune.HasValue) { @@ -422,7 +421,7 @@ public class LineCanvas : IDisposable return cell; } - private Rune? GetRuneForIntersects (IDriver? driver, ReadOnlySpan intersects) + private Rune? GetRuneForIntersects (ReadOnlySpan intersects) { if (intersects.IsEmpty) { @@ -432,7 +431,7 @@ public class LineCanvas : IDisposable IntersectionRuneType runeType = GetRuneTypeForIntersects (intersects); if (_runeResolvers.TryGetValue (runeType, out IntersectionRuneResolver? resolver)) { - return resolver.GetRuneForIntersects (driver, intersects); + return resolver.GetRuneForIntersects (intersects); } // TODO: Remove these once we have all of the below ported to IntersectionRuneResolvers @@ -769,7 +768,7 @@ public class LineCanvas : IDisposable internal Rune _thickV; protected IntersectionRuneResolver () { SetGlyphs (); } - public Rune? GetRuneForIntersects (IDriver? driver, ReadOnlySpan intersects) + public Rune? GetRuneForIntersects (ReadOnlySpan intersects) { // Note that there aren't any glyphs for intersections of double lines with heavy lines diff --git a/Terminal.Gui/Drawing/LineCanvas/LineStyle.cs b/Terminal.Gui/Drawing/LineCanvas/LineStyle.cs index 9c8f234fe..9b93708db 100644 --- a/Terminal.Gui/Drawing/LineCanvas/LineStyle.cs +++ b/Terminal.Gui/Drawing/LineCanvas/LineStyle.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Text.Json.Serialization; namespace Terminal.Gui.Drawing; diff --git a/Terminal.Gui/Drawing/LineCanvas/StraightLine.cs b/Terminal.Gui/Drawing/LineCanvas/StraightLine.cs index 5e64c0b7a..b4f86665d 100644 --- a/Terminal.Gui/Drawing/LineCanvas/StraightLine.cs +++ b/Terminal.Gui/Drawing/LineCanvas/StraightLine.cs @@ -1,6 +1,5 @@  namespace Terminal.Gui.Drawing; -#nullable enable // TODO: Add events that notify when StraightLine changes to enable dynamic layout /// A line between two points on a horizontal or vertical and a given style/color. diff --git a/Terminal.Gui/Drawing/LineCanvas/StraightLineExtensions.cs b/Terminal.Gui/Drawing/LineCanvas/StraightLineExtensions.cs index 1cff548ae..f8089952f 100644 --- a/Terminal.Gui/Drawing/LineCanvas/StraightLineExtensions.cs +++ b/Terminal.Gui/Drawing/LineCanvas/StraightLineExtensions.cs @@ -1,4 +1,4 @@ - + namespace Terminal.Gui.Drawing; /// Extension methods for (including collections). @@ -187,7 +187,7 @@ public static class StraightLineExtensions { if (length == 0) { - throw new ArgumentException ("0 length lines are not supported", nameof (length)); + throw new ArgumentException (@"0 length lines are not supported", nameof (length)); } int sub = length > 0 ? 1 : -1; @@ -220,7 +220,7 @@ public static class StraightLineExtensions { if (length == 0) { - throw new ArgumentException ("0 length lines are not supported", nameof (length)); + throw new ArgumentException (@"0 length lines are not supported", nameof (length)); } int sub = length > 0 ? 1 : -1; diff --git a/Terminal.Gui/Drawing/Quant/PopularityPaletteWithThreshold.cs b/Terminal.Gui/Drawing/Quant/PopularityPaletteWithThreshold.cs index 0e7f53596..c98050941 100644 --- a/Terminal.Gui/Drawing/Quant/PopularityPaletteWithThreshold.cs +++ b/Terminal.Gui/Drawing/Quant/PopularityPaletteWithThreshold.cs @@ -1,8 +1,9 @@ - +namespace Terminal.Gui.Drawing; + /// /// Simple fast palette building algorithm which uses the frequency that a color is seen -/// to determine whether it will appear in the final palette. Includes a threshold where -/// by colors will be considered 'the same'. This reduces the chance of under represented +/// to determine whether it will appear in the final palette. Includes a threshold whereby +/// colors will be considered 'the same'. This reduces the chance of underrepresented /// colors being missed completely. /// public class PopularityPaletteWithThreshold : IPaletteBuilder @@ -22,11 +23,11 @@ public class PopularityPaletteWithThreshold : IPaletteBuilder } /// - public List BuildPalette (List colors, int maxColors) + public List BuildPalette (List? colors, int maxColors) { - if (colors == null || colors.Count == 0 || maxColors <= 0) + if (colors is null || colors.Count == 0 || maxColors <= 0) { - return new (); + return []; } // Step 1: Build the histogram of colors (count occurrences) @@ -34,14 +35,10 @@ public class PopularityPaletteWithThreshold : IPaletteBuilder foreach (Color color in colors) { - if (colorHistogram.ContainsKey (color)) + if (!colorHistogram.TryAdd (color, 1)) { colorHistogram [color]++; } - else - { - colorHistogram [color] = 1; - } } // If we already have fewer or equal colors than the limit, no need to merge diff --git a/Terminal.Gui/Drawing/Region.cs b/Terminal.Gui/Drawing/Region.cs index f3abb8e07..597801a6a 100644 --- a/Terminal.Gui/Drawing/Region.cs +++ b/Terminal.Gui/Drawing/Region.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.Drawing; @@ -51,10 +50,10 @@ public class Region private readonly List _rectangles = []; // Add a single reusable list for temp operations - private readonly List _tempRectangles = new(); + private readonly List _tempRectangles = new (); // Object used for synchronization - private readonly object _syncLock = new object(); + private readonly object _syncLock = new object (); /// /// Initializes a new instance of the class. @@ -121,12 +120,12 @@ public class Region { lock (_syncLock) { - CombineInternal(region, operation); + CombineInternal (region, operation); } } // Private method to implement the combine logic within a lock - private void CombineInternal(Region? region, RegionOp operation) + private void CombineInternal (Region? region, RegionOp operation) { if (region is null || region._rectangles.Count == 0) { @@ -189,36 +188,36 @@ public class Region case RegionOp.Union: // Avoid collection initialization with spread operator - _tempRectangles.Clear(); - _tempRectangles.AddRange(_rectangles); + _tempRectangles.Clear (); + _tempRectangles.AddRange (_rectangles); if (region != null) { // Get the region's rectangles safely lock (region._syncLock) { - _tempRectangles.AddRange(region._rectangles); + _tempRectangles.AddRange (region._rectangles); } } - List mergedUnion = MergeRectangles(_tempRectangles, false); - _rectangles.Clear(); - _rectangles.AddRange(mergedUnion); + List mergedUnion = MergeRectangles (_tempRectangles, false); + _rectangles.Clear (); + _rectangles.AddRange (mergedUnion); break; case RegionOp.MinimalUnion: // Avoid collection initialization with spread operator - _tempRectangles.Clear(); - _tempRectangles.AddRange(_rectangles); + _tempRectangles.Clear (); + _tempRectangles.AddRange (_rectangles); if (region != null) { // Get the region's rectangles safely lock (region._syncLock) { - _tempRectangles.AddRange(region._rectangles); + _tempRectangles.AddRange (region._rectangles); } } - List mergedMinimalUnion = MergeRectangles(_tempRectangles, true); - _rectangles.Clear(); - _rectangles.AddRange(mergedMinimalUnion); + List mergedMinimalUnion = MergeRectangles (_tempRectangles, true); + _rectangles.Clear (); + _rectangles.AddRange (mergedMinimalUnion); break; case RegionOp.XOR: @@ -588,17 +587,26 @@ public class Region { // 1. Sort by X int cmp = a.x.CompareTo (b.x); - if (cmp != 0) return cmp; + if (cmp != 0) + { + return cmp; + } // 2. Sort End events before Start events bool aIsEnd = !a.isStart; bool bIsEnd = !b.isStart; cmp = aIsEnd.CompareTo (bIsEnd); // True (End) comes after False (Start) - if (cmp != 0) return -cmp; // Reverse: End (true) should come before Start (false) + if (cmp != 0) + { + return -cmp; // Reverse: End (true) should come before Start (false) + } // 3. Tie-breaker: Sort by yTop cmp = a.yTop.CompareTo (b.yTop); - if (cmp != 0) return cmp; + if (cmp != 0) + { + return cmp; + } // 4. Final Tie-breaker: Sort by yBottom return a.yBottom.CompareTo (b.yBottom); @@ -901,13 +909,16 @@ public class Region /// /// Fills the interior of all rectangles in the region with the specified attribute and fill rune. /// + /// /// The attribute (color/style) to use. /// /// The rune to fill the interior of the rectangles with. If space will be /// used. /// - public void FillRectangles (Attribute attribute, Rune? fillRune = null) + public void FillRectangles (IDriver? driver, Attribute? attribute, Rune? fillRune = null) { + ArgumentNullException.ThrowIfNull (driver); + if (_rectangles.Count == 0) { return; @@ -920,14 +931,14 @@ public class Region continue; } - Application.Driver?.SetAttribute (attribute); + driver?.SetAttribute (attribute!.Value); for (int y = rect.Top; y < rect.Bottom; y++) { for (int x = rect.Left; x < rect.Right; x++) { - Application.Driver?.Move (x, y); - Application.Driver?.AddRune (fillRune ?? (Rune)' '); + driver?.Move (x, y); + driver?.AddRune (fillRune ?? (Rune)' '); } } } @@ -1046,7 +1057,7 @@ public class Region if (bounds.Width > 1000 || bounds.Height > 1000) { // Fall back to drawing each rectangle's boundary - DrawBoundaries(lineCanvas, style, attribute); + DrawBoundaries (lineCanvas, style, attribute); return; } diff --git a/Terminal.Gui/Drawing/RegionOp.cs b/Terminal.Gui/Drawing/RegionOp.cs index e40de1663..0b4689a37 100644 --- a/Terminal.Gui/Drawing/RegionOp.cs +++ b/Terminal.Gui/Drawing/RegionOp.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.Drawing; /// diff --git a/Terminal.Gui/Drawing/Ruler.cs b/Terminal.Gui/Drawing/Ruler.cs index 89ef6b6d1..4c457976f 100644 --- a/Terminal.Gui/Drawing/Ruler.cs +++ b/Terminal.Gui/Drawing/Ruler.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.Drawing; /// Draws a ruler on the screen. @@ -21,11 +20,13 @@ internal class Ruler private string _vTemplate { get; } = "-123456789"; /// Draws the . + /// Optional Driver. If not provided, driver will be used. /// The location to start drawing the ruler, in screen-relative coordinates. /// The start value of the ruler. - /// Optional Driver. If not provided, driver will be used. - public void Draw (Point location, int start = 0, IDriver? driver = null) + public void Draw (IDriver? driver, Point location, int start = 0) { + ArgumentNullException.ThrowIfNull (driver); + if (start < 0) { throw new ArgumentException ("start must be greater than or equal to 0"); @@ -36,8 +37,6 @@ internal class Ruler return; } - driver ??= driver; - if (Orientation == Orientation.Horizontal) { string hrule = diff --git a/Terminal.Gui/Drawing/Scheme.cs b/Terminal.Gui/Drawing/Scheme.cs index ff0933aac..9bd612537 100644 --- a/Terminal.Gui/Drawing/Scheme.cs +++ b/Terminal.Gui/Drawing/Scheme.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Immutable; using System.Numerics; using System.Text.Json.Serialization; diff --git a/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs b/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs index 01df783a4..a9e1ae8aa 100644 --- a/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs +++ b/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs @@ -6,8 +6,21 @@ namespace Terminal.Gui.Drawing; /// Uses Ansi escape sequences to detect whether sixel is supported /// by the terminal. /// -public class SixelSupportDetector +public class SixelSupportDetector () { + private readonly IDriver? _driver; + + /// + /// Creates a new instance of the class. + /// + /// + public SixelSupportDetector (IDriver? driver) : this () + { + ArgumentNullException.ThrowIfNull (driver); + + _driver = driver; + } + /// /// Sends Ansi escape sequences to the console to determine whether /// sixel is supported (and @@ -127,17 +140,17 @@ public class SixelSupportDetector () => resultCallback (result)); } - private static void QueueRequest (AnsiEscapeSequence req, Action responseCallback, Action abandoned) + private void QueueRequest (AnsiEscapeSequence req, Action responseCallback, Action abandoned) { var newRequest = new AnsiEscapeSequenceRequest { Request = req.Request, Terminator = req.Terminator, - ResponseReceived = responseCallback, + ResponseReceived = responseCallback!, Abandoned = abandoned }; - Application.Driver?.QueueAnsiRequest (newRequest); + _driver?.QueueAnsiRequest (newRequest); } private static bool ResponseIndicatesSupport (string response) { return response.Split (';').Contains ("4"); } @@ -145,14 +158,12 @@ public class SixelSupportDetector private static bool IsVirtualTerminal () { return !string.IsNullOrWhiteSpace (Environment.GetEnvironmentVariable ("WT_SESSION")); - - ; } private static bool IsXtermWithTransparency () { // Check if running in real xterm (XTERM_VERSION is more reliable than TERM) - string xtermVersionStr = Environment.GetEnvironmentVariable ("XTERM_VERSION"); + string xtermVersionStr = Environment.GetEnvironmentVariable (@"XTERM_VERSION")!; // If XTERM_VERSION exists, we are in a real xterm if (!string.IsNullOrWhiteSpace (xtermVersionStr) && int.TryParse (xtermVersionStr, out int xtermVersion) && xtermVersion >= 370) diff --git a/Terminal.Gui/Drawing/Sixel/SixelToRender.cs b/Terminal.Gui/Drawing/Sixel/SixelToRender.cs index c66d4bdaf..89c3b26b5 100644 --- a/Terminal.Gui/Drawing/Sixel/SixelToRender.cs +++ b/Terminal.Gui/Drawing/Sixel/SixelToRender.cs @@ -10,7 +10,7 @@ public class SixelToRender /// gets or sets the encoded sixel data. Use to convert bitmaps /// into encoded sixel data. /// - public string SixelData { get; set; } + public string? SixelData { get; set; } /// /// gets or sets where to move the cursor to before outputting the . diff --git a/Terminal.Gui/Drawing/Thickness.cs b/Terminal.Gui/Drawing/Thickness.cs index c89773f1c..a15b49fd4 100644 --- a/Terminal.Gui/Drawing/Thickness.cs +++ b/Terminal.Gui/Drawing/Thickness.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Numerics; +using System.Numerics; using System.Text.Json.Serialization; namespace Terminal.Gui.Drawing; @@ -90,15 +89,15 @@ public record struct Thickness /// The diagnostics label to draw on the bottom of the . /// Optional driver. If not specified, will be used. /// The inner rectangle remaining to be drawn. - public Rectangle Draw (Rectangle rect, ViewDiagnosticFlags diagnosticFlags = ViewDiagnosticFlags.Off, string? label = null, IDriver? driver = null) + public Rectangle Draw (IDriver? driver, Rectangle rect, ViewDiagnosticFlags diagnosticFlags = ViewDiagnosticFlags.Off, string? label = null) { + ArgumentNullException.ThrowIfNull (driver); + if (rect.Size.Width < 1 || rect.Size.Height < 1) { return Rectangle.Empty; } - driver ??= Application.Driver; - var clearChar = (Rune)' '; Rune leftChar = clearChar; Rune rightChar = clearChar; @@ -165,7 +164,7 @@ public record struct Thickness if (Top > 0) { - hRuler.Draw (rect.Location, driver: driver); + hRuler.Draw (driver: driver, location: rect.Location); } //Left @@ -173,19 +172,19 @@ public record struct Thickness if (Left > 0) { - vRuler.Draw (rect.Location with { Y = rect.Y + 1 }, 1, driver); + vRuler.Draw (driver, rect.Location with { Y = rect.Y + 1 }, 1); } // Bottom if (Bottom > 0) { - hRuler.Draw (rect.Location with { Y = rect.Y + rect.Height - 1 }, driver: driver); + hRuler.Draw (driver: driver, location: rect.Location with { Y = rect.Y + rect.Height - 1 }); } // Right if (Right > 0) { - vRuler.Draw (new (rect.X + rect.Width - 1, rect.Y + 1), 1, driver); + vRuler.Draw (driver, new (rect.X + rect.Width - 1, rect.Y + 1), 1); } } @@ -205,7 +204,7 @@ public record struct Thickness if (driver?.CurrentAttribute is { }) { - tf.Draw (rect, driver!.CurrentAttribute, driver!.CurrentAttribute, rect, driver); + tf.Draw (driver, rect, driver!.CurrentAttribute, driver!.CurrentAttribute, rect); } } diff --git a/Terminal.Gui/Drawing/VisualRoleEventArgs.cs b/Terminal.Gui/Drawing/VisualRoleEventArgs.cs index 5f0389967..bdc54af8f 100644 --- a/Terminal.Gui/Drawing/VisualRoleEventArgs.cs +++ b/Terminal.Gui/Drawing/VisualRoleEventArgs.cs @@ -1,5 +1,4 @@ -#nullable enable -namespace Terminal.Gui.Drawing; +namespace Terminal.Gui.Drawing; using System; @@ -60,4 +59,4 @@ public class VisualRoleEventArgs : ResultEventArgs } } -#pragma warning restore CS1711 \ No newline at end of file +#pragma warning restore CS1711 diff --git a/Terminal.Gui/Drivers/AnsiHandling/AnsiEscapeSequence.cs b/Terminal.Gui/Drivers/AnsiHandling/AnsiEscapeSequence.cs index 5b0471776..b90bf3702 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/AnsiEscapeSequence.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/AnsiEscapeSequence.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/AnsiHandling/AnsiEscapeSequenceRequest.cs b/Terminal.Gui/Drivers/AnsiHandling/AnsiEscapeSequenceRequest.cs index de61ae920..cce053a1a 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/AnsiEscapeSequenceRequest.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/AnsiEscapeSequenceRequest.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace Terminal.Gui.Drivers; /// @@ -20,12 +18,12 @@ public class AnsiEscapeSequenceRequest : AnsiEscapeSequence /// public Action? Abandoned { get; init; } - /// /// Sends the to the raw output stream of the current . /// Only call this method from the main UI thread. You should use if /// sending many requests. /// - public void Send () { Application.Driver?.WriteRaw (Request); } + /// + public void Send (IDriver? driver) { driver?.WriteRaw (Request); } } diff --git a/Terminal.Gui/Drivers/AnsiHandling/AnsiMouseParser.cs b/Terminal.Gui/Drivers/AnsiHandling/AnsiMouseParser.cs index 7f3f82709..882863a6a 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/AnsiMouseParser.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/AnsiMouseParser.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Text.RegularExpressions; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/AnsiHandling/AnsiRequestScheduler.cs b/Terminal.Gui/Drivers/AnsiHandling/AnsiRequestScheduler.cs index 3f20af735..8fd50a4cc 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/AnsiRequestScheduler.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/AnsiRequestScheduler.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Concurrent; namespace Terminal.Gui.Drivers; @@ -70,15 +69,16 @@ public class AnsiRequestScheduler /// Sends the immediately or queues it if there is already /// an outstanding request for the given . /// + /// /// /// if request was sent immediately. if it was queued. - public bool SendOrSchedule (AnsiEscapeSequenceRequest request) { return SendOrSchedule (request, true); } + public bool SendOrSchedule (IDriver? driver, AnsiEscapeSequenceRequest request) { return SendOrSchedule (driver, request, true); } - private bool SendOrSchedule (AnsiEscapeSequenceRequest request, bool addToQueue) + private bool SendOrSchedule (IDriver? driver, AnsiEscapeSequenceRequest request, bool addToQueue) { if (CanSend (request, out ReasonCannotSend reason)) { - Send (request); + Send (driver, request); return true; } @@ -91,7 +91,7 @@ public class AnsiRequestScheduler // Try again after evicting if (CanSend (request, out _)) { - Send (request); + Send (driver, request); return true; } @@ -142,6 +142,7 @@ public class AnsiRequestScheduler /// Identifies and runs any that can be sent based on the /// current outstanding requests of the parser. /// + /// /// /// Repeated requests to run the schedule over short period of time will be ignored. /// Pass to override this behaviour and force evaluation of outstanding requests. @@ -150,7 +151,7 @@ public class AnsiRequestScheduler /// if a request was found and run. /// if no outstanding requests or all have existing outstanding requests underway in parser. /// - public bool RunSchedule (bool force = false) + public bool RunSchedule (IDriver? driver, bool force = false) { if (!force && Now () - _lastRun < _runScheduleThrottle) { @@ -163,7 +164,7 @@ public class AnsiRequestScheduler if (opportunity != null) { // Give it another go - if (SendOrSchedule (opportunity.Item1, false)) + if (SendOrSchedule (driver, opportunity.Item1, false)) { _queuedRequests.Remove (opportunity); @@ -176,11 +177,11 @@ public class AnsiRequestScheduler return false; } - private void Send (AnsiEscapeSequenceRequest r) + private void Send (IDriver? driver, AnsiEscapeSequenceRequest r) { _lastSend.AddOrUpdate (r.Terminator!, _ => Now (), (_, _) => Now ()); _parser.ExpectResponse (r.Terminator, r.ResponseReceived, r.Abandoned, false); - r.Send (); + r.Send (driver); } private bool CanSend (AnsiEscapeSequenceRequest r, out ReasonCannotSend reason) diff --git a/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseExpectation.cs b/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseExpectation.cs index fd087de75..1c6539316 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseExpectation.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseExpectation.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Drivers; internal record AnsiResponseExpectation (string? Terminator, Action Response, Action? Abandoned) diff --git a/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParser.cs b/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParser.cs index 5bafbfe55..60f6d87f5 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParser.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParser.cs @@ -1,5 +1,3 @@ -#nullable enable - using Microsoft.Extensions.Logging; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqReqStatus.cs b/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqReqStatus.cs index ab062e253..8943ab1bc 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqReqStatus.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqReqStatus.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqRequests.cs b/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqRequests.cs index 6e69f6a12..7732bd319 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqRequests.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqRequests.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs index 6257d1d66..3f9d27d0f 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics; using System.Globalization; diff --git a/Terminal.Gui/Drivers/AnsiHandling/GenericHeld.cs b/Terminal.Gui/Drivers/AnsiHandling/GenericHeld.cs index 6373003fa..6339b4ee2 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/GenericHeld.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/GenericHeld.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/AnsiHandling/IAnsiResponseParser.cs b/Terminal.Gui/Drivers/AnsiHandling/IAnsiResponseParser.cs index cfdb775f0..29892a2da 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/IAnsiResponseParser.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/IAnsiResponseParser.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/AnsiHandling/IHeld.cs b/Terminal.Gui/Drivers/AnsiHandling/IHeld.cs index 369ef4732..e6d5a3a06 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/IHeld.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/IHeld.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/AnsiHandling/Keyboard/AnsiKeyboardParser.cs b/Terminal.Gui/Drivers/AnsiHandling/Keyboard/AnsiKeyboardParser.cs index ee41d6cca..89ef61a58 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/Keyboard/AnsiKeyboardParser.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/Keyboard/AnsiKeyboardParser.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/AnsiHandling/Keyboard/AnsiKeyboardParserPattern.cs b/Terminal.Gui/Drivers/AnsiHandling/Keyboard/AnsiKeyboardParserPattern.cs index 62b0acb64..51011124a 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/Keyboard/AnsiKeyboardParserPattern.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/Keyboard/AnsiKeyboardParserPattern.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/AnsiHandling/Keyboard/CsiCursorPattern.cs b/Terminal.Gui/Drivers/AnsiHandling/Keyboard/CsiCursorPattern.cs index 744658a76..61adedd62 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/Keyboard/CsiCursorPattern.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/Keyboard/CsiCursorPattern.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Text.RegularExpressions; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/AnsiHandling/Keyboard/CsiKeyPattern.cs b/Terminal.Gui/Drivers/AnsiHandling/Keyboard/CsiKeyPattern.cs index f0fb3b20b..ed2bcecbe 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/Keyboard/CsiKeyPattern.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/Keyboard/CsiKeyPattern.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Text.RegularExpressions; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/AnsiHandling/Keyboard/EscAsAltPattern.cs b/Terminal.Gui/Drivers/AnsiHandling/Keyboard/EscAsAltPattern.cs index a9b16e90a..fd6b54321 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/Keyboard/EscAsAltPattern.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/Keyboard/EscAsAltPattern.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Text.RegularExpressions; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/AnsiHandling/Keyboard/Ss3Pattern.cs b/Terminal.Gui/Drivers/AnsiHandling/Keyboard/Ss3Pattern.cs index 988b584f1..cf2804072 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/Keyboard/Ss3Pattern.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/Keyboard/Ss3Pattern.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Text.RegularExpressions; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/AnsiHandling/Osc8UrlLinker.cs b/Terminal.Gui/Drivers/AnsiHandling/Osc8UrlLinker.cs index 7a3e41a6d..d4cd43065 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/Osc8UrlLinker.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/Osc8UrlLinker.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/AnsiHandling/ReasonCannotSend.cs b/Terminal.Gui/Drivers/AnsiHandling/ReasonCannotSend.cs index 675c9ff64..ba0aa399c 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/ReasonCannotSend.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/ReasonCannotSend.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Drivers; internal enum ReasonCannotSend diff --git a/Terminal.Gui/Drivers/AnsiHandling/StringHeld.cs b/Terminal.Gui/Drivers/AnsiHandling/StringHeld.cs index 3202410ab..79e922098 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/StringHeld.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/StringHeld.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/ComponentFactoryImpl.cs b/Terminal.Gui/Drivers/ComponentFactoryImpl.cs index d09099954..04f79a88e 100644 --- a/Terminal.Gui/Drivers/ComponentFactoryImpl.cs +++ b/Terminal.Gui/Drivers/ComponentFactoryImpl.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Collections.Concurrent; +using System.Collections.Concurrent; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/CursorVisibility.cs b/Terminal.Gui/Drivers/CursorVisibility.cs index ca86e9a0a..7d0c5d9f4 100644 --- a/Terminal.Gui/Drivers/CursorVisibility.cs +++ b/Terminal.Gui/Drivers/CursorVisibility.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Drivers; /// Terminal Cursor Visibility settings. diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetComponentFactory.cs b/Terminal.Gui/Drivers/DotNetDriver/NetComponentFactory.cs index 669c6efce..46b8b9efb 100644 --- a/Terminal.Gui/Drivers/DotNetDriver/NetComponentFactory.cs +++ b/Terminal.Gui/Drivers/DotNetDriver/NetComponentFactory.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Concurrent; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetInput.cs b/Terminal.Gui/Drivers/DotNetDriver/NetInput.cs index b44c59522..026689a45 100644 --- a/Terminal.Gui/Drivers/DotNetDriver/NetInput.cs +++ b/Terminal.Gui/Drivers/DotNetDriver/NetInput.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Logging; - +#nullable disable namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs b/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs index 8fbd11ba1..4f8ab1fc0 100644 --- a/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs +++ b/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.Logging; - namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetWinVTConsole.cs b/Terminal.Gui/Drivers/DotNetDriver/NetWinVTConsole.cs index d6730e044..36cbf0e2a 100644 --- a/Terminal.Gui/Drivers/DotNetDriver/NetWinVTConsole.cs +++ b/Terminal.Gui/Drivers/DotNetDriver/NetWinVTConsole.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Runtime.InteropServices; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index db1918459..ee125c143 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; namespace Terminal.Gui.Drivers; @@ -75,9 +74,7 @@ internal class DriverImpl : IDriver CreateClipboard (); } - /// - /// The event fired when the screen changes (size, position, etc.). - /// + /// public event EventHandler? SizeChanged; /// @@ -89,7 +86,6 @@ internal class DriverImpl : IDriver /// public ISizeMonitor SizeMonitor { get; } - private void CreateClipboard () { if (InputProcessor.DriverName is { } && InputProcessor.DriverName.Contains ("fake")) @@ -120,27 +116,18 @@ internal class DriverImpl : IDriver // Clipboard is set to FakeClipboard at initialization } - /// Gets the location and size of the terminal screen. - public Rectangle Screen - { - get - { - //if (Application.RunningUnitTests && _output is WindowsConsoleOutput or NetOutput) - //{ - // // In unit tests, we don't have a real output, so we return an empty rectangle. - // return Rectangle.Empty; - //} + /// - return new (0, 0, OutputBuffer.Cols, OutputBuffer.Rows); - } - } + public Rectangle Screen => - /// - /// Sets the screen size for testing purposes. Only supported by FakeDriver. - /// - /// The new width in columns. - /// The new height in rows. - /// Thrown when called on non-FakeDriver instances. + //if (Application.RunningUnitTests && _output is WindowsConsoleOutput or NetOutput) + //{ + // // In unit tests, we don't have a real output, so we return an empty rectangle. + // return Rectangle.Empty; + //} + new (0, 0, OutputBuffer.Cols, OutputBuffer.Rows); + + /// public virtual void SetScreenSize (int width, int height) { OutputBuffer.SetSize (width, height); @@ -148,64 +135,60 @@ internal class DriverImpl : IDriver SizeChanged?.Invoke (this, new (new (width, height))); } - /// - /// Gets or sets the clip rectangle that and are subject - /// to. - /// - /// The rectangle describing the of region. + /// + public Region? Clip { get => OutputBuffer.Clip; set => OutputBuffer.Clip = value; } - /// Get the operating system clipboard. + /// + public IClipboard? Clipboard { get; private set; } = new FakeClipboard (); - /// - /// Gets the column last set by . and are used by - /// and to determine where to add content. - /// + /// + public int Col => OutputBuffer.Col; - /// The number of columns visible in the terminal. + /// + public int Cols { get => OutputBuffer.Cols; set => OutputBuffer.Cols = value; } - /// - /// The contents of the application output. The driver outputs this buffer to the terminal. - /// The format of the array is rows, columns. The first index is the row, the second index is the column. - /// + /// + public Cell [,]? Contents { get => OutputBuffer.Contents; set => OutputBuffer.Contents = value; } - /// The leftmost column in the terminal. + /// + public int Left { get => OutputBuffer.Left; set => OutputBuffer.Left = value; } - /// - /// Gets the row last set by . and are used by - /// and to determine where to add content. - /// + /// + public int Row => OutputBuffer.Row; - /// The number of rows visible in the terminal. + /// + public int Rows { get => OutputBuffer.Rows; set => OutputBuffer.Rows = value; } - /// The topmost row in the terminal. + /// + public int Top { get => OutputBuffer.Top; @@ -214,75 +197,33 @@ internal class DriverImpl : IDriver // TODO: Probably not everyone right? - /// Gets whether the supports TrueColor output. + /// + public bool SupportsTrueColor => true; - // TODO: Currently ignored - /// - /// Gets or sets whether the should use 16 colors instead of the default TrueColors. - /// See to change this setting via . - /// - /// - /// - /// Will be forced to if is - /// , indicating that the cannot support TrueColor. - /// - /// + /// + public bool Force16Colors { get => Application.Force16Colors || !SupportsTrueColor; set => Application.Force16Colors = value || !SupportsTrueColor; } - /// - /// The that will be used for the next or - /// - /// call. - /// + /// + public Attribute CurrentAttribute { get => OutputBuffer.CurrentAttribute; set => OutputBuffer.CurrentAttribute = value; } - /// Adds the specified rune to the display at the current cursor position. - /// - /// - /// When the method returns, will be incremented by the number of columns - /// required, even if the new column value is outside of the - /// or screen - /// dimensions defined by . - /// - /// - /// If requires more than one column, and plus the number - /// of columns - /// needed exceeds the or screen dimensions, the default Unicode replacement - /// character (U+FFFD) - /// will be added instead. - /// - /// - /// Rune to add. + /// public void AddRune (Rune rune) { OutputBuffer.AddRune (rune); } - /// - /// Adds the specified to the display at the current cursor position. This method is a - /// convenience method that calls with the - /// constructor. - /// - /// Character to add. + /// public void AddRune (char c) { OutputBuffer.AddRune (c); } - /// Adds the to the display at the cursor position. - /// - /// - /// When the method returns, will be incremented by the number of columns - /// required, unless the new column value is outside of the - /// or screen - /// dimensions defined by . - /// - /// If requires more columns than are available, the output will be clipped. - /// - /// String. + /// public void AddStr (string str) { OutputBuffer.AddStr (str); } /// Clears the of the driver. @@ -292,28 +233,13 @@ internal class DriverImpl : IDriver ClearedContents?.Invoke (this, new MouseEventArgs ()); } - /// - /// Raised each time is called. For benchmarking. - /// + /// public event EventHandler? ClearedContents; - /// - /// Fills the specified rectangle with the specified rune, using - /// - /// - /// The value of is honored. Any parts of the rectangle not in the clip will not be - /// drawn. - /// - /// The Screen-relative rectangle. - /// The Rune used to fill the rectangle + /// public void FillRect (Rectangle rect, Rune rune = default) { OutputBuffer.FillRect (rect, rune); } - /// - /// Fills the specified rectangle with the specified . This method is a convenience method - /// that calls . - /// - /// - /// + /// public void FillRect (Rectangle rect, char c) { OutputBuffer.FillRect (rect, c); } /// @@ -324,48 +250,18 @@ internal class DriverImpl : IDriver return type; } - /// Tests if the specified rune is supported by the driver. - /// - /// - /// if the rune can be properly presented; if the driver does not - /// support displaying this rune. - /// - public bool IsRuneSupported (Rune rune) { return Rune.IsValid (rune.Value); } + /// + public bool IsRuneSupported (Rune rune) => Rune.IsValid (rune.Value); - /// Tests whether the specified coordinate are valid for drawing the specified Rune. - /// Used to determine if one or two columns are required. - /// The column. - /// The row. - /// - /// if the coordinate is outside the screen bounds or outside of - /// . - /// otherwise. - /// - public bool IsValidLocation (Rune rune, int col, int row) { return OutputBuffer.IsValidLocation (rune, col, row); } + /// + public bool IsValidLocation (Rune rune, int col, int row) => OutputBuffer.IsValidLocation (rune, col, row); - /// - /// Updates and to the specified column and row in - /// . - /// Used by and to determine - /// where to add content. - /// - /// - /// This does not move the cursor on the screen, it only updates the internal state of the driver. - /// - /// If or are negative or beyond - /// and - /// , the method still sets those properties. - /// - /// - /// Column to move to. - /// Row to move to. + /// public void Move (int col, int row) { OutputBuffer.Move (col, row); } // TODO: Probably part of output - /// Sets the terminal cursor visibility. - /// The wished - /// upon success + /// public bool SetCursorVisibility (CursorVisibility visibility) { _lastCursor = visibility; @@ -415,31 +311,22 @@ internal class DriverImpl : IDriver Logging.Error ($"Error suspending terminal: {ex.Message}"); } - Application.LayoutAndDraw (); - - Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents); } - /// - /// Sets the position of the terminal cursor to and - /// . - /// + /// public void UpdateCursor () { _output.SetCursorPosition (Col, Row); } - /// Initializes the driver + /// public void Init () { throw new NotSupportedException (); } - /// Ends the execution of the console driver. + /// public void End () { // TODO: Nope } - /// Selects the specified attribute as the attribute to use for future calls to AddRune and AddString. - /// Implementations should call base.SetAttribute(c). - /// C. - /// The previously set Attribute. + /// public Attribute SetAttribute (Attribute newAttribute) { Attribute currentAttribute = OutputBuffer.CurrentAttribute; @@ -448,51 +335,29 @@ internal class DriverImpl : IDriver return currentAttribute; } - /// Gets the current . - /// The current attribute. - public Attribute GetAttribute () { return OutputBuffer.CurrentAttribute; } + /// + public Attribute GetAttribute () => OutputBuffer.CurrentAttribute; /// Event fired when a key is pressed down. This is a precursor to . public event EventHandler? KeyDown; - /// Event fired when a key is released. - /// - /// Drivers that do not support key release events will fire this event after - /// processing is - /// complete. - /// + /// public event EventHandler? KeyUp; /// Event fired when a mouse event occurs. public event EventHandler? MouseEvent; - /// - /// Provide proper writing to send escape sequence recognized by the . - /// - /// + /// public void WriteRaw (string ansi) { _output.Write (ansi); } /// - public void EnqueueKeyEvent (Key key) - { - InputProcessor.EnqueueKeyDownEvent (key); - } + public void EnqueueKeyEvent (Key key) { InputProcessor.EnqueueKeyDownEvent (key); } - /// - /// Queues the specified ANSI escape sequence request for execution. - /// - /// The ANSI request to queue. - /// - /// The request is sent immediately if possible, or queued for later execution - /// by the to prevent overwhelming the console. - /// - public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) { _ansiRequestScheduler.SendOrSchedule (request); } + /// + public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) { _ansiRequestScheduler.SendOrSchedule (this, request); } - /// - /// Gets the instance used by this driver. - /// - /// The ANSI request scheduler. - public AnsiRequestScheduler GetRequestScheduler () { return _ansiRequestScheduler; } + /// + public AnsiRequestScheduler GetRequestScheduler () => _ansiRequestScheduler; /// public void Refresh () @@ -500,8 +365,51 @@ internal class DriverImpl : IDriver // No need we will always draw when dirty } - public string? GetName () + /// + public string? GetName () => InputProcessor.DriverName?.ToLowerInvariant (); + + /// + public new string ToString () { - return InputProcessor.DriverName?.ToLowerInvariant (); + StringBuilder sb = new (); + + Cell [,] contents = Contents!; + + for (var r = 0; r < Rows; r++) + { + for (var c = 0; c < Cols; c++) + { + Rune rune = contents [r, c].Rune; + + if (rune.DecodeSurrogatePair (out char []? sp)) + { + sb.Append (sp); + } + else + { + sb.Append ((char)rune.Value); + } + + if (rune.GetColumns () > 1) + { + c++; + } + + // See Issue #2616 + //foreach (var combMark in contents [r, c].CombiningMarks) { + // sb.Append ((char)combMark.Value); + //} + } + + sb.AppendLine (); + } + + return sb.ToString (); + } + + /// + public string ToAnsi () + { + return _output.ToAnsi (OutputBuffer); } } diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeClipboard.cs b/Terminal.Gui/Drivers/FakeDriver/FakeClipboard.cs index 6951b7923..9a7bc4d57 100644 --- a/Terminal.Gui/Drivers/FakeDriver/FakeClipboard.cs +++ b/Terminal.Gui/Drivers/FakeDriver/FakeClipboard.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs b/Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs index e7ce72f3d..5f4284bdc 100644 --- a/Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs +++ b/Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Concurrent; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeInput.cs b/Terminal.Gui/Drivers/FakeDriver/FakeInput.cs index 76093e785..b8a0a5240 100644 --- a/Terminal.Gui/Drivers/FakeDriver/FakeInput.cs +++ b/Terminal.Gui/Drivers/FakeDriver/FakeInput.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Concurrent; namespace Terminal.Gui.Drivers; @@ -44,4 +43,4 @@ public class FakeInput : InputImpl, ITestableInput if (Application.MainThreadId is { }) { // Application is running - use Invoke to defer to next iteration - Application.Invoke (() => RaiseMouseEvent (mouseEvent)); + ApplicationImpl.Instance.Invoke ((_) => RaiseMouseEvent (mouseEvent)); } else { diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs b/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs index 0722079fe..8fd790f19 100644 --- a/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs +++ b/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs @@ -1,4 +1,3 @@ -#nullable enable using System; namespace Terminal.Gui.Drivers; @@ -87,8 +86,29 @@ public class FakeOutput : OutputBase, IOutput /// protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle) { - // For testing, we can skip the actual color/style output - // or capture it if needed for verification + if (Application.Force16Colors) + { + output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ())); + output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ())); + } + else + { + EscSeqUtils.CSI_AppendForegroundColorRGB ( + output, + attr.Foreground.R, + attr.Foreground.G, + attr.Foreground.B + ); + + EscSeqUtils.CSI_AppendBackgroundColorRGB ( + output, + attr.Background.R, + attr.Background.G, + attr.Background.B + ); + } + + EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); } /// diff --git a/Terminal.Gui/Drivers/IComponentFactory.cs b/Terminal.Gui/Drivers/IComponentFactory.cs index 7122c4af3..d58a95f68 100644 --- a/Terminal.Gui/Drivers/IComponentFactory.cs +++ b/Terminal.Gui/Drivers/IComponentFactory.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Collections.Concurrent; +using System.Collections.Concurrent; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/IDriver.cs b/Terminal.Gui/Drivers/IDriver.cs index 32af99b05..090c192a4 100644 --- a/Terminal.Gui/Drivers/IDriver.cs +++ b/Terminal.Gui/Drivers/IDriver.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Drivers; @@ -295,4 +294,18 @@ public interface IDriver /// /// public AnsiRequestScheduler GetRequestScheduler (); + + + /// + /// Gets a string representation of . + /// + /// + public string ToString (); + + /// + /// Gets an ANSI escape sequence representation of . This is the + /// same output as would be written to the terminal to recreate the current screen contents. + /// + /// + public string ToAnsi (); } diff --git a/Terminal.Gui/Drivers/IInput.cs b/Terminal.Gui/Drivers/IInput.cs index 0b2ec7d41..c4a0af159 100644 --- a/Terminal.Gui/Drivers/IInput.cs +++ b/Terminal.Gui/Drivers/IInput.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Collections.Concurrent; +using System.Collections.Concurrent; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/IInputProcessor.cs b/Terminal.Gui/Drivers/IInputProcessor.cs index d0c0284b8..9c800946c 100644 --- a/Terminal.Gui/Drivers/IInputProcessor.cs +++ b/Terminal.Gui/Drivers/IInputProcessor.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/IOutput.cs b/Terminal.Gui/Drivers/IOutput.cs index bc04dfaa6..0eb647dec 100644 --- a/Terminal.Gui/Drivers/IOutput.cs +++ b/Terminal.Gui/Drivers/IOutput.cs @@ -53,4 +53,12 @@ public interface IOutput : IDisposable /// /// void Write (IOutputBuffer buffer); + + /// + /// Generates an ANSI escape sequence string representation of the given contents. + /// This is the same output that would be written to the terminal to recreate the current screen contents. + /// + /// The output buffer to convert to ANSI. + /// A string containing ANSI escape sequences representing the buffer contents. + string ToAnsi (IOutputBuffer buffer); } diff --git a/Terminal.Gui/Drivers/IOutputBuffer.cs b/Terminal.Gui/Drivers/IOutputBuffer.cs index 2b8991593..387fde46c 100644 --- a/Terminal.Gui/Drivers/IOutputBuffer.cs +++ b/Terminal.Gui/Drivers/IOutputBuffer.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/ISizeMonitor.cs b/Terminal.Gui/Drivers/ISizeMonitor.cs index 602d5e4b8..df8273bfd 100644 --- a/Terminal.Gui/Drivers/ISizeMonitor.cs +++ b/Terminal.Gui/Drivers/ISizeMonitor.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/InputImpl.cs b/Terminal.Gui/Drivers/InputImpl.cs index d340b3d85..b70df2b44 100644 --- a/Terminal.Gui/Drivers/InputImpl.cs +++ b/Terminal.Gui/Drivers/InputImpl.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Collections.Concurrent; +using System.Collections.Concurrent; namespace Terminal.Gui.Drivers; @@ -86,4 +85,4 @@ public abstract class InputImpl : IInput /// public virtual void Dispose () { } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Drivers/InputProcessorImpl.cs b/Terminal.Gui/Drivers/InputProcessorImpl.cs index f79a96364..24c249a84 100644 --- a/Terminal.Gui/Drivers/InputProcessorImpl.cs +++ b/Terminal.Gui/Drivers/InputProcessorImpl.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Collections.Concurrent; +using System.Collections.Concurrent; using Microsoft.Extensions.Logging; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/KeyCode.cs b/Terminal.Gui/Drivers/KeyCode.cs index eb87a7e92..431508e5f 100644 --- a/Terminal.Gui/Drivers/KeyCode.cs +++ b/Terminal.Gui/Drivers/KeyCode.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/MouseButtonStateEx.cs b/Terminal.Gui/Drivers/MouseButtonStateEx.cs index e6e2a1e96..1aba69677 100644 --- a/Terminal.Gui/Drivers/MouseButtonStateEx.cs +++ b/Terminal.Gui/Drivers/MouseButtonStateEx.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/MouseInterpreter.cs b/Terminal.Gui/Drivers/MouseInterpreter.cs index cb585ed9f..f5848ab65 100644 --- a/Terminal.Gui/Drivers/MouseInterpreter.cs +++ b/Terminal.Gui/Drivers/MouseInterpreter.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/OutputBase.cs b/Terminal.Gui/Drivers/OutputBase.cs index ac43dc93d..ca8b92c01 100644 --- a/Terminal.Gui/Drivers/OutputBase.cs +++ b/Terminal.Gui/Drivers/OutputBase.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Drivers; /// @@ -32,9 +31,6 @@ public abstract class OutputBase CursorVisibility? savedVisibility = _cachedCursorVisibility; SetCursorVisibility (CursorVisibility.Invisible); - const int MAX_CHARS_PER_RUNE = 2; - Span runeBuffer = stackalloc char [MAX_CHARS_PER_RUNE]; - for (int row = top; row < rows; row++) { if (!SetCursorPositionImpl (0, row)) @@ -75,32 +71,14 @@ public abstract class OutputBase lastCol = col; } - Attribute? attribute = buffer.Contents [row, col].Attribute; - - if (attribute is { }) - { - Attribute attr = attribute.Value; - - // Performance: Only send the escape sequence if the attribute has changed. - if (attr != redrawAttr) - { - redrawAttr = attr; - - AppendOrWriteAttribute (output, attr, _redrawTextStyle); - - _redrawTextStyle = attr.Style; - } - } + Cell cell = buffer.Contents [row, col]; + AppendCellAnsi (cell, output, ref redrawAttr, ref _redrawTextStyle, cols, ref col); outputWidth++; - // Avoid Rune.ToString() by appending the rune chars. - Rune rune = buffer.Contents [row, col].Rune; - int runeCharsWritten = rune.EncodeToUtf16 (runeBuffer); - ReadOnlySpan runeChars = runeBuffer [..runeCharsWritten]; - output.Append (runeChars); - - if (buffer.Contents [row, col].CombiningMarks.Count > 0) + // Handle special cases that AppendCellAnsi doesn't cover + Rune rune = cell.Rune; + if (cell.CombiningMarks.Count > 0) { // AtlasEngine does not support NON-NORMALIZED combining marks in a way // compatible with the driver architecture. Any CMs (except in the first col) @@ -170,6 +148,105 @@ public abstract class OutputBase /// protected abstract void Write (StringBuilder output); + /// + /// Builds ANSI escape sequences for the specified rectangular region of the buffer. + /// + /// The output buffer to build ANSI for. + /// The starting row (inclusive). + /// The ending row (exclusive). + /// The starting column (inclusive). + /// The ending column (exclusive). + /// The StringBuilder to append ANSI sequences to. + /// The last attribute used, for optimization. + /// Predicate to determine which cells to include. If null, includes all cells. + /// Whether to add newlines between rows. + protected void BuildAnsiForRegion ( + IOutputBuffer buffer, + int startRow, + int endRow, + int startCol, + int endCol, + StringBuilder output, + ref Attribute? lastAttr, + Func? includeCellPredicate = null, + bool addNewlines = true + ) + { + TextStyle redrawTextStyle = TextStyle.None; + + for (int row = startRow; row < endRow; row++) + { + for (int col = startCol; col < endCol; col++) + { + if (includeCellPredicate != null && !includeCellPredicate (row, col)) + { + continue; + } + + Cell cell = buffer.Contents![row, col]; + AppendCellAnsi (cell, output, ref lastAttr, ref redrawTextStyle, endCol, ref col); + } + + // Add newline at end of row if requested + if (addNewlines) + { + output.AppendLine (); + } + } + } + + /// + /// Appends ANSI sequences for a single cell to the output. + /// + /// The cell to append ANSI for. + /// The StringBuilder to append to. + /// The last attribute used, updated if the cell's attribute is different. + /// The current text style for optimization. + /// The maximum column, used for wide character handling. + /// The current column, updated for wide characters. + protected void AppendCellAnsi (Cell cell, StringBuilder output, ref Attribute? lastAttr, ref TextStyle redrawTextStyle, int maxCol, ref int currentCol) + { + Attribute? attribute = cell.Attribute; + + // Add ANSI escape sequence for attribute change + if (attribute.HasValue && attribute.Value != lastAttr) + { + lastAttr = attribute.Value; + AppendOrWriteAttribute (output, attribute.Value, redrawTextStyle); + redrawTextStyle = attribute.Value.Style; + } + + // Add the character + const int MAX_CHARS_PER_RUNE = 2; + Span runeBuffer = stackalloc char [MAX_CHARS_PER_RUNE]; + Rune rune = cell.Rune; + int runeCharsWritten = rune.EncodeToUtf16 (runeBuffer); + ReadOnlySpan runeChars = runeBuffer [..runeCharsWritten]; + output.Append (runeChars); + + // Handle wide characters + if (rune.GetColumns () > 1 && currentCol + 1 < maxCol) + { + currentCol++; // Skip next cell for wide character + } + } + + /// + /// Generates an ANSI escape sequence string representation of the given contents. + /// This is the same output that would be written to the terminal to recreate the current screen contents. + /// + /// The output buffer to convert to ANSI. + /// A string containing ANSI escape sequences representing the buffer contents. + public string ToAnsi (IOutputBuffer buffer) + { + var output = new StringBuilder (); + Attribute? lastAttr = null; + + BuildAnsiForRegion (buffer, 0, buffer.Rows, 0, buffer.Cols, output, ref lastAttr); + + return output.ToString (); + } + private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth) { SetCursorPositionImpl (lastCol, row); diff --git a/Terminal.Gui/Drivers/OutputBufferImpl.cs b/Terminal.Gui/Drivers/OutputBufferImpl.cs index ee2493d60..d12eb7991 100644 --- a/Terminal.Gui/Drivers/OutputBufferImpl.cs +++ b/Terminal.Gui/Drivers/OutputBufferImpl.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Diagnostics; +using System.Diagnostics; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/Platform.cs b/Terminal.Gui/Drivers/Platform.cs index 4b14ba053..4b7b20a18 100644 --- a/Terminal.Gui/Drivers/Platform.cs +++ b/Terminal.Gui/Drivers/Platform.cs @@ -80,4 +80,4 @@ internal static class Platform [DllImport ("libc")] private static extern int uname (nint buf); -} \ No newline at end of file +} diff --git a/Terminal.Gui/Drivers/SizeMonitorImpl.cs b/Terminal.Gui/Drivers/SizeMonitorImpl.cs index 409f27617..f41b50d7e 100644 --- a/Terminal.Gui/Drivers/SizeMonitorImpl.cs +++ b/Terminal.Gui/Drivers/SizeMonitorImpl.cs @@ -1,5 +1,4 @@ -#nullable enable -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/UnixDriver/UnixClipboard.cs b/Terminal.Gui/Drivers/UnixDriver/UnixClipboard.cs index c562ee66d..6562f3b2d 100644 --- a/Terminal.Gui/Drivers/UnixDriver/UnixClipboard.cs +++ b/Terminal.Gui/Drivers/UnixDriver/UnixClipboard.cs @@ -1,3 +1,4 @@ +#nullable disable using System.Runtime.InteropServices; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/UnixDriver/UnixComponentFactory.cs b/Terminal.Gui/Drivers/UnixDriver/UnixComponentFactory.cs index f53b88f85..5067832d7 100644 --- a/Terminal.Gui/Drivers/UnixDriver/UnixComponentFactory.cs +++ b/Terminal.Gui/Drivers/UnixDriver/UnixComponentFactory.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Concurrent; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/UnixDriver/UnixInput.cs b/Terminal.Gui/Drivers/UnixDriver/UnixInput.cs index 0337e0aff..60807d5c4 100644 --- a/Terminal.Gui/Drivers/UnixDriver/UnixInput.cs +++ b/Terminal.Gui/Drivers/UnixDriver/UnixInput.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Runtime.InteropServices; // ReSharper disable IdentifierTypo diff --git a/Terminal.Gui/Drivers/UnixDriver/UnixKeyConverter.cs b/Terminal.Gui/Drivers/UnixDriver/UnixKeyConverter.cs index 62c05a0ed..22f95d4d3 100644 --- a/Terminal.Gui/Drivers/UnixDriver/UnixKeyConverter.cs +++ b/Terminal.Gui/Drivers/UnixDriver/UnixKeyConverter.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs b/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs index 17dbe2bb4..dfbf63ead 100644 --- a/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs +++ b/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs @@ -1,4 +1,4 @@ -#nullable enable + using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; diff --git a/Terminal.Gui/Drivers/WindowsDriver/ClipboardImpl.cs b/Terminal.Gui/Drivers/WindowsDriver/ClipboardImpl.cs index 6dc708110..cab68fc74 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/ClipboardImpl.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/ClipboardImpl.cs @@ -1,4 +1,3 @@ -#nullable enable using System.ComponentModel; using System.Runtime.InteropServices; diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsComponentFactory.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsComponentFactory.cs index 1f16fd66a..4c361b00d 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsComponentFactory.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsComponentFactory.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Concurrent; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs index 744c32588..fa191262e 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Runtime.InteropServices; // ReSharper disable InconsistentNaming diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsInput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsInput.cs index 026e1f61b..dc3c98205 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsInput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsInput.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using static Terminal.Gui.Drivers.WindowsConsole; diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsInputProcessor.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsInputProcessor.cs index 028eb4dc1..739a393fb 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsInputProcessor.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsInputProcessor.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Concurrent; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyConverter.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyConverter.cs index 4458ad6ff..0a8f2427f 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyConverter.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyConverter.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace Terminal.Gui.Drivers; /// diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyHelper.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyHelper.cs index 56193db21..cd424136e 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyHelper.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyHelper.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics; // ReSharper disable InconsistentNaming diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyboardLayout.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyboardLayout.cs index e368dfa8d..38fc85bfe 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyboardLayout.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyboardLayout.cs @@ -1,3 +1,4 @@ +#nullable disable using System.Runtime.InteropServices; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs index a0bea436c..5a81ae0ab 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs @@ -1,4 +1,3 @@ -#nullable enable using System.ComponentModel; using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; @@ -148,7 +147,9 @@ internal partial class WindowsOutput : OutputBase, IOutput } // Force 16 colors if not in virtual terminal mode. - Application.Force16Colors = true; + // BUGBUG: This is bad. It does not work if the app was crated without + // BUGBUG: Apis. + ApplicationImpl.Instance.Force16Colors = true; } @@ -263,7 +264,10 @@ internal partial class WindowsOutput : OutputBase, IOutput public override void Write (IOutputBuffer outputBuffer) { - _force16Colors = Application.Driver!.Force16Colors; + // BUGBUG: This is bad. It does not work if the app was crated without + // BUGBUG: Apis. + //_force16Colors = ApplicationImpl.Instance.Driver!.Force16Colors; + _force16Colors = false; _everythingStringBuilder.Clear (); // for 16 color mode we will write to a backing buffer then flip it to the active one at the end to avoid jitter. @@ -303,6 +307,12 @@ internal partial class WindowsOutput : OutputBase, IOutput { int err = Marshal.GetLastWin32Error (); + if (err == 1) + { + Logging.Logger.LogError ($"Error: {Marshal.GetLastWin32Error ()} in {nameof (WindowsOutput)}"); + + return; + } if (err != 0) { throw new Win32Exception (err); @@ -345,7 +355,9 @@ internal partial class WindowsOutput : OutputBase, IOutput /// protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle) { - bool force16Colors = Application.Force16Colors; + // BUGBUG: This is bad. It does not work if the app was crated without + // BUGBUG: Apis. + bool force16Colors = ApplicationImpl.Instance.Force16Colors; if (force16Colors) { diff --git a/Terminal.Gui/FileServices/DefaultSearchMatcher.cs b/Terminal.Gui/FileServices/DefaultSearchMatcher.cs index 3c10e2a1c..3d9ce8046 100644 --- a/Terminal.Gui/FileServices/DefaultSearchMatcher.cs +++ b/Terminal.Gui/FileServices/DefaultSearchMatcher.cs @@ -4,8 +4,8 @@ namespace Terminal.Gui.FileServices; internal class DefaultSearchMatcher : ISearchMatcher { - private string [] terms; - public void Initialize (string terms) { this.terms = terms.Split (new [] { " " }, StringSplitOptions.RemoveEmptyEntries); } + private string []? _terms; + public void Initialize (string terms) { _terms = terms.Split ([" "], StringSplitOptions.RemoveEmptyEntries); } public bool IsMatch (IFileSystemInfo f) { @@ -15,10 +15,10 @@ internal class DefaultSearchMatcher : ISearchMatcher return // At least one term must match the file name only e.g. "my" in "myfile.csv" - terms.Any (t => f.Name.IndexOf (t, StringComparison.OrdinalIgnoreCase) >= 0) + _terms!.Any (t => f.Name.IndexOf (t, StringComparison.OrdinalIgnoreCase) >= 0) && // All terms must exist in full path e.g. "dos my" can match "c:\documents\myfile.csv" - terms.All (t => f.FullName.IndexOf (t, StringComparison.OrdinalIgnoreCase) >= 0); + _terms!.All (t => f.FullName.IndexOf (t, StringComparison.OrdinalIgnoreCase) >= 0); } } diff --git a/Terminal.Gui/FileServices/FileSystemIconProvider.cs b/Terminal.Gui/FileServices/FileSystemIconProvider.cs index 607582576..de76d91c6 100644 --- a/Terminal.Gui/FileServices/FileSystemIconProvider.cs +++ b/Terminal.Gui/FileServices/FileSystemIconProvider.cs @@ -1,4 +1,3 @@ -#nullable enable using System.IO.Abstractions; namespace Terminal.Gui.FileServices; diff --git a/Terminal.Gui/FileServices/FileSystemInfoStats.cs b/Terminal.Gui/FileServices/FileSystemInfoStats.cs index 72f8ded2d..5444aa26f 100644 --- a/Terminal.Gui/FileServices/FileSystemInfoStats.cs +++ b/Terminal.Gui/FileServices/FileSystemInfoStats.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Globalization; +using System.Globalization; using System.IO.Abstractions; namespace Terminal.Gui.FileServices; diff --git a/Terminal.Gui/FileServices/FileSystemTreeBuilder.cs b/Terminal.Gui/FileServices/FileSystemTreeBuilder.cs index 87aba39af..a44ddd049 100644 --- a/Terminal.Gui/FileServices/FileSystemTreeBuilder.cs +++ b/Terminal.Gui/FileServices/FileSystemTreeBuilder.cs @@ -1,4 +1,5 @@ -using System.IO.Abstractions; +#nullable disable +using System.IO.Abstractions; namespace Terminal.Gui.FileServices; diff --git a/Terminal.Gui/Input/Command.cs b/Terminal.Gui/Input/Command.cs index 88d99e639..4d1b6d008 100644 --- a/Terminal.Gui/Input/Command.cs +++ b/Terminal.Gui/Input/Command.cs @@ -336,4 +336,4 @@ public enum Command Edit, #endregion -} \ No newline at end of file +} diff --git a/Terminal.Gui/Input/CommandContext.cs b/Terminal.Gui/Input/CommandContext.cs index fe6c9c194..dd6c529ba 100644 --- a/Terminal.Gui/Input/CommandContext.cs +++ b/Terminal.Gui/Input/CommandContext.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.Input; #pragma warning disable CS1574, CS0419 // XML comment has cref attribute that could not be resolved @@ -33,4 +32,4 @@ public record struct CommandContext : ICommandContext /// The keyboard or mouse minding that was used to invoke the , if any. /// public TBinding? Binding { get; set; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Input/CommandEventArgs.cs b/Terminal.Gui/Input/CommandEventArgs.cs index 05d662437..659d0db64 100644 --- a/Terminal.Gui/Input/CommandEventArgs.cs +++ b/Terminal.Gui/Input/CommandEventArgs.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.ComponentModel; +using System.ComponentModel; namespace Terminal.Gui.Input; diff --git a/Terminal.Gui/Input/ICommandContext.cs b/Terminal.Gui/Input/ICommandContext.cs index a9d4e641c..c4e57de22 100644 --- a/Terminal.Gui/Input/ICommandContext.cs +++ b/Terminal.Gui/Input/ICommandContext.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.Input; #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved diff --git a/Terminal.Gui/Input/IInputBinding.cs b/Terminal.Gui/Input/IInputBinding.cs index f0ae3a25a..34170c73c 100644 --- a/Terminal.Gui/Input/IInputBinding.cs +++ b/Terminal.Gui/Input/IInputBinding.cs @@ -1,5 +1,4 @@ -#nullable enable -namespace Terminal.Gui.Input; +namespace Terminal.Gui.Input; /// /// Describes an input binding. Used to bind a set of objects to a specific input event. diff --git a/Terminal.Gui/Input/InputBindings.cs b/Terminal.Gui/Input/InputBindings.cs index 75a4711e5..8711cf87c 100644 --- a/Terminal.Gui/Input/InputBindings.cs +++ b/Terminal.Gui/Input/InputBindings.cs @@ -1,5 +1,4 @@ -#nullable enable -namespace Terminal.Gui.Input; +namespace Terminal.Gui.Input; /// /// Abstract class for and . diff --git a/Terminal.Gui/Input/Keyboard/Key.cs b/Terminal.Gui/Input/Keyboard/Key.cs index cd9924047..7a60b6cb3 100644 --- a/Terminal.Gui/Input/Keyboard/Key.cs +++ b/Terminal.Gui/Input/Keyboard/Key.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics.CodeAnalysis; using System.Globalization; diff --git a/Terminal.Gui/Input/Keyboard/KeyBinding.cs b/Terminal.Gui/Input/Keyboard/KeyBinding.cs index b55a66842..6d97a6b6e 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBinding.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBinding.cs @@ -1,4 +1,4 @@ -#nullable enable + // These classes use a key binding system based on the design implemented in Scintilla.Net which is an diff --git a/Terminal.Gui/Input/Keyboard/KeyBindings.cs b/Terminal.Gui/Input/Keyboard/KeyBindings.cs index c4a2952eb..46a7081de 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBindings.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindings.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Input; diff --git a/Terminal.Gui/Input/Keyboard/KeyEqualityComparer.cs b/Terminal.Gui/Input/Keyboard/KeyEqualityComparer.cs index 287dd9cfa..25841107d 100644 --- a/Terminal.Gui/Input/Keyboard/KeyEqualityComparer.cs +++ b/Terminal.Gui/Input/Keyboard/KeyEqualityComparer.cs @@ -1,4 +1,3 @@ -#nullable enable /// /// diff --git a/Terminal.Gui/Input/Mouse/GrabMouseEventArgs.cs b/Terminal.Gui/Input/Mouse/GrabMouseEventArgs.cs index c05fd7471..fd3ae5654 100644 --- a/Terminal.Gui/Input/Mouse/GrabMouseEventArgs.cs +++ b/Terminal.Gui/Input/Mouse/GrabMouseEventArgs.cs @@ -1,4 +1,3 @@ - namespace Terminal.Gui.Input; /// Args GrabMouse related events. diff --git a/Terminal.Gui/Input/Mouse/MouseBinding.cs b/Terminal.Gui/Input/Mouse/MouseBinding.cs index 1c6ebf386..40f32ea8f 100644 --- a/Terminal.Gui/Input/Mouse/MouseBinding.cs +++ b/Terminal.Gui/Input/Mouse/MouseBinding.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Input; diff --git a/Terminal.Gui/Input/Mouse/MouseBindings.cs b/Terminal.Gui/Input/Mouse/MouseBindings.cs index 55d0bf61d..255be246b 100644 --- a/Terminal.Gui/Input/Mouse/MouseBindings.cs +++ b/Terminal.Gui/Input/Mouse/MouseBindings.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Input; /// diff --git a/Terminal.Gui/Input/Mouse/MouseEventArgs.cs b/Terminal.Gui/Input/Mouse/MouseEventArgs.cs index db0008ef3..3e10c68a4 100644 --- a/Terminal.Gui/Input/Mouse/MouseEventArgs.cs +++ b/Terminal.Gui/Input/Mouse/MouseEventArgs.cs @@ -1,4 +1,4 @@ -#nullable enable + using System.ComponentModel; namespace Terminal.Gui.Input; diff --git a/Terminal.Gui/Resources/GlobalResources.cs b/Terminal.Gui/Resources/GlobalResources.cs index 8a9ac5a0c..4c05e3415 100644 --- a/Terminal.Gui/Resources/GlobalResources.cs +++ b/Terminal.Gui/Resources/GlobalResources.cs @@ -1,5 +1,4 @@ -#nullable enable - + using System.Collections; using System.Globalization; using System.Resources; diff --git a/Terminal.Gui/Resources/ResourceManagerWrapper.cs b/Terminal.Gui/Resources/ResourceManagerWrapper.cs index 8bcc9271f..dc3651454 100644 --- a/Terminal.Gui/Resources/ResourceManagerWrapper.cs +++ b/Terminal.Gui/Resources/ResourceManagerWrapper.cs @@ -1,5 +1,4 @@ -#nullable enable - + using System.Collections; using System.Globalization; using System.Resources; diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index 15d586975..6f1e53802 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -24,6 +24,7 @@ net8.0 12 enable + enable $(AssemblyName) true @@ -163,6 +164,16 @@ + + + + + + + + + + diff --git a/Terminal.Gui/Text/NerdFonts.cs b/Terminal.Gui/Text/NerdFonts.cs index 2ceaf4fa7..c6da80697 100644 --- a/Terminal.Gui/Text/NerdFonts.cs +++ b/Terminal.Gui/Text/NerdFonts.cs @@ -741,8 +741,13 @@ internal class NerdFonts { "nf-seti-typescript", '' } }; - public char GetNerdIcon (IFileSystemInfo file, bool isOpen) + public char GetNerdIcon (IFileSystemInfo? file, bool isOpen) { + if (file == null) + { + throw new ArgumentNullException (nameof (file)); + } + if (FilenameToIcon.ContainsKey (file.Name)) { return Glyphs [FilenameToIcon [file.Name]]; diff --git a/Terminal.Gui/Text/RuneExtensions.cs b/Terminal.Gui/Text/RuneExtensions.cs index 4dfb4bb3f..c8a273c83 100644 --- a/Terminal.Gui/Text/RuneExtensions.cs +++ b/Terminal.Gui/Text/RuneExtensions.cs @@ -1,5 +1,4 @@ -#nullable enable - + using System.Globalization; using Wcwidth; diff --git a/Terminal.Gui/Text/StringExtensions.cs b/Terminal.Gui/Text/StringExtensions.cs index e379cf3da..eb053a3d8 100644 --- a/Terminal.Gui/Text/StringExtensions.cs +++ b/Terminal.Gui/Text/StringExtensions.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Buffers; +using System.Buffers; using System.Globalization; namespace Terminal.Gui.Text; diff --git a/Terminal.Gui/Text/TextDirection.cs b/Terminal.Gui/Text/TextDirection.cs index 2ea3ba3a4..b989c7337 100644 --- a/Terminal.Gui/Text/TextDirection.cs +++ b/Terminal.Gui/Text/TextDirection.cs @@ -58,4 +58,4 @@ public enum TextDirection /// This is a vertical direction. D O
L L
R L
O E
W H
BottomTop_RightLeft -} \ No newline at end of file +} diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs index 70636c018..a11289a2b 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Buffers; using System.Diagnostics; @@ -11,7 +10,7 @@ namespace Terminal.Gui.Text; public class TextFormatter { // Utilized in CRLF related helper methods for faster newline char index search. - private static readonly SearchValues NewlineSearchValues = SearchValues.Create(['\r', '\n']); + private static readonly SearchValues NewlineSearchValues = SearchValues.Create (['\r', '\n']); private Key _hotKey = new (); private int _hotKeyPos = -1; @@ -52,31 +51,28 @@ public class TextFormatter /// Causes the text to be formatted (references ). Sets to /// false. /// + /// The console driver currently used by the application. /// Specifies the screen-relative location and maximum size for drawing the text. /// The color to use for all text except the hotkey /// The color to use to draw the hotkey /// Specifies the screen-relative location and maximum container size. - /// The console driver currently used by the application. /// public void Draw ( + IDriver? driver, Rectangle screen, Attribute normalColor, Attribute hotColor, - Rectangle maximum = default, - IDriver? driver = null + Rectangle maximum = default ) { + ArgumentNullException.ThrowIfNull (driver); + // With this check, we protect against subclasses with overrides of Text (like Button) if (string.IsNullOrEmpty (Text)) { return; } - if (driver is null) - { - driver = Application.Driver; - } - driver?.SetAttribute (normalColor); List linesFormatted = GetLines (); @@ -1199,8 +1195,8 @@ public class TextFormatter return str; } - StringBuilder stringBuilder = new(); - ReadOnlySpan firstSegment = remaining[..firstNewlineCharIndex]; + StringBuilder stringBuilder = new (); + ReadOnlySpan firstSegment = remaining [..firstNewlineCharIndex]; stringBuilder.Append (firstSegment); // The first newline is not yet skipped because the "keepNewLine" condition has not been evaluated. @@ -1215,7 +1211,7 @@ public class TextFormatter break; } - ReadOnlySpan segment = remaining[..newlineCharIndex]; + ReadOnlySpan segment = remaining [..newlineCharIndex]; stringBuilder.Append (segment); int stride = segment.Length; @@ -1267,8 +1263,8 @@ public class TextFormatter return str; } - StringBuilder stringBuilder = new(); - ReadOnlySpan firstSegment = remaining[..firstNewlineCharIndex]; + StringBuilder stringBuilder = new (); + ReadOnlySpan firstSegment = remaining [..firstNewlineCharIndex]; stringBuilder.Append (firstSegment); // The first newline is not yet skipped because the newline type has not been evaluated. @@ -1283,7 +1279,7 @@ public class TextFormatter break; } - ReadOnlySpan segment = remaining[..newlineCharIndex]; + ReadOnlySpan segment = remaining [..newlineCharIndex]; stringBuilder.Append (segment); int stride = segment.Length; @@ -2445,12 +2441,12 @@ public class TextFormatter } const int maxStackallocCharBufferSize = 512; // ~1 kB - char[]? rentedBufferArray = null; + char []? rentedBufferArray = null; try { Span buffer = text.Length <= maxStackallocCharBufferSize - ? stackalloc char[text.Length] - : (rentedBufferArray = ArrayPool.Shared.Rent(text.Length)); + ? stackalloc char [text.Length] + : (rentedBufferArray = ArrayPool.Shared.Rent (text.Length)); int i = 0; var remainingBuffer = buffer; @@ -2468,7 +2464,7 @@ public class TextFormatter ReadOnlySpan newText = buffer [..^remainingBuffer.Length]; // If the resulting string would be the same as original then just return the original. - if (newText.Equals(text, StringComparison.Ordinal)) + if (newText.Equals (text, StringComparison.Ordinal)) { return text; } diff --git a/Terminal.Gui/ViewBase/Adornment/Adornment.cs b/Terminal.Gui/ViewBase/Adornment/Adornment.cs index 9d5675b6b..6852efea4 100644 --- a/Terminal.Gui/ViewBase/Adornment/Adornment.cs +++ b/Terminal.Gui/ViewBase/Adornment/Adornment.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.ViewBase; @@ -84,6 +84,12 @@ public class Adornment : View, IDesignable #region View Overrides + /// + protected override IApplication? GetApp () => Parent?.App; + + /// + protected override IDriver? GetDriver () => Parent?.Driver ?? base.GetDriver(); + // If a scheme is explicitly set, use that. Otherwise, use the scheme of the parent view. private Scheme? _scheme; @@ -176,7 +182,10 @@ public class Adornment : View, IDesignable } // This just draws/clears the thickness, not the insides. - Thickness.Draw (ViewportToScreen (Viewport), Diagnostics, ToString ()); + if (Driver is { }) + { + Thickness.Draw (Driver, ViewportToScreen (Viewport), Diagnostics, ToString ()); + } NeedsDraw = true; diff --git a/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs b/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs index b55fec027..f2e52fab8 100644 --- a/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs +++ b/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs @@ -1,4 +1,3 @@ -#nullable enable using System.ComponentModel; using System.Diagnostics; @@ -40,7 +39,10 @@ public partial class Border // Add Commands and KeyBindings - Note it's ok these get added each time. KeyBindings are cleared in EndArrange() AddArrangeModeKeyBindings (); - Application.MouseEvent += ApplicationOnMouseEvent; + if (App is { }) + { + App.Mouse.MouseEvent += ApplicationOnMouseEvent; + } // Create all necessary arrangement buttons CreateArrangementButtons (); @@ -429,11 +431,14 @@ public partial class Border MouseState &= ~MouseState.Pressed; - Application.MouseEvent -= ApplicationOnMouseEvent; - - if (Application.Mouse.MouseGrabView == this && _dragPosition.HasValue) + if (App is { }) { - Application.Mouse.UngrabMouse (); + App.Mouse.MouseEvent -= ApplicationOnMouseEvent; + + if (App.Mouse.MouseGrabView == this && _dragPosition.HasValue) + { + App.Mouse.UngrabMouse (); + } } // Clean up all arrangement buttons @@ -498,7 +503,7 @@ public partial class Border // Set the start grab point to the Frame coords _startGrabPoint = new (mouseEvent.Position.X + Frame.X, mouseEvent.Position.Y + Frame.Y); _dragPosition = mouseEvent.Position; - Application.Mouse.GrabMouse (this); + App?.Mouse.GrabMouse (this); // Determine the mode based on where the click occurred ViewArrangement arrangeMode = DetermineArrangeModeFromClick (); @@ -511,7 +516,7 @@ public partial class Border return true; } - if (mouseEvent.Flags is (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && Application.Mouse.MouseGrabView == this) + if (mouseEvent.Flags is (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && App?.Mouse.MouseGrabView == this) { if (_dragPosition.HasValue) { @@ -523,7 +528,7 @@ public partial class Border if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && _dragPosition.HasValue) { _dragPosition = null; - Application.Mouse.UngrabMouse (); + App?.Mouse.UngrabMouse (); EndArrangeMode (); @@ -652,7 +657,7 @@ public partial class Border if (Parent!.SuperView is null) { // Redraw the entire app window. - Application.Top!.SetNeedsDraw (); + App?.Current?.SetNeedsDraw (); } else { @@ -763,7 +768,7 @@ public partial class Border private void Application_GrabbingMouse (object? sender, GrabMouseEventArgs e) { - if (Application.Mouse.MouseGrabView == this && _dragPosition.HasValue) + if (App?.Mouse.MouseGrabView == this && _dragPosition.HasValue) { e.Cancel = true; } @@ -771,7 +776,7 @@ public partial class Border private void Application_UnGrabbingMouse (object? sender, GrabMouseEventArgs e) { - if (Application.Mouse.MouseGrabView == this && _dragPosition.HasValue) + if (App?.Mouse.MouseGrabView == this && _dragPosition.HasValue) { e.Cancel = true; } @@ -784,8 +789,11 @@ public partial class Border /// protected override void Dispose (bool disposing) { - Application.Mouse.GrabbingMouse -= Application_GrabbingMouse; - Application.Mouse.UnGrabbingMouse -= Application_UnGrabbingMouse; + if (App is { }) + { + App.Mouse.GrabbingMouse -= Application_GrabbingMouse; + App.Mouse.UnGrabbingMouse -= Application_UnGrabbingMouse; + } _dragPosition = null; base.Dispose (disposing); diff --git a/Terminal.Gui/ViewBase/Adornment/Border.cs b/Terminal.Gui/ViewBase/Adornment/Border.cs index 18a99b1c1..bfbe92a08 100644 --- a/Terminal.Gui/ViewBase/Adornment/Border.cs +++ b/Terminal.Gui/ViewBase/Adornment/Border.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics; namespace Terminal.Gui.ViewBase; @@ -49,10 +48,6 @@ public partial class Border : Adornment Parent = parent; CanFocus = false; TabStop = TabBehavior.TabGroup; - - Application.Mouse.GrabbingMouse += Application_GrabbingMouse; - Application.Mouse.UnGrabbingMouse += Application_UnGrabbingMouse; - ThicknessChanged += OnThicknessChanged; } @@ -113,6 +108,12 @@ public partial class Border : Adornment { base.BeginInit (); + if (App is { }) + { + App.Mouse.GrabbingMouse += Application_GrabbingMouse; + App.Mouse.UnGrabbingMouse += Application_UnGrabbingMouse; + } + if (Parent is null) { return; @@ -312,7 +313,8 @@ public partial class Border : Adornment } } - if (Parent is { } + if (Driver is { } + && Parent is { } && canDrawBorder && Thickness.Top > 0 && maxTitleWidth > 0 @@ -321,10 +323,7 @@ public partial class Border : Adornment { Rectangle titleRect = new (borderBounds.X + 2, titleY, maxTitleWidth, 1); - Parent.TitleTextFormatter.Draw ( - titleRect, - GetAttributeForRole (Parent.HasFocus ? VisualRole.Focus : VisualRole.Normal), - GetAttributeForRole (Parent.HasFocus ? VisualRole.HotFocus : VisualRole.HotNormal)); + Parent.TitleTextFormatter.Draw (driver: Driver, screen: titleRect, normalColor: GetAttributeForRole (Parent.HasFocus ? VisualRole.Focus : VisualRole.Normal), hotColor: GetAttributeForRole (Parent.HasFocus ? VisualRole.HotFocus : VisualRole.HotNormal)); Parent?.LineCanvas.Exclude (new (titleRect)); } @@ -498,16 +497,13 @@ public partial class Border : Adornment if (drawTop) { - hruler.Draw (new (screenBounds.X, screenBounds.Y)); + hruler.Draw (driver: Driver, location: new (screenBounds.X, screenBounds.Y)); } // Redraw title if (drawTop && maxTitleWidth > 0 && Settings.FastHasFlags (BorderSettings.Title)) { - Parent!.TitleTextFormatter.Draw ( - new (borderBounds.X + 2, titleY, maxTitleWidth, 1), - Parent.HasFocus ? Parent.GetAttributeForRole (VisualRole.Focus) : Parent.GetAttributeForRole (VisualRole.Normal), - Parent.HasFocus ? Parent.GetAttributeForRole (VisualRole.Focus) : Parent.GetAttributeForRole (VisualRole.Normal)); + Parent!.TitleTextFormatter.Draw (driver: Driver, screen: new (borderBounds.X + 2, titleY, maxTitleWidth, 1), normalColor: Parent.HasFocus ? Parent.GetAttributeForRole (VisualRole.Focus) : Parent.GetAttributeForRole (VisualRole.Normal), hotColor: Parent.HasFocus ? Parent.GetAttributeForRole (VisualRole.Focus) : Parent.GetAttributeForRole (VisualRole.Normal)); } //Left @@ -515,19 +511,19 @@ public partial class Border : Adornment if (drawLeft) { - vruler.Draw (new (screenBounds.X, screenBounds.Y + 1), 1); + vruler.Draw (driver: Driver, location: new (screenBounds.X, screenBounds.Y + 1), start: 1); } // Bottom if (drawBottom) { - hruler.Draw (new (screenBounds.X, screenBounds.Y + screenBounds.Height - 1)); + hruler.Draw (driver: Driver, location: new (screenBounds.X, screenBounds.Y + screenBounds.Height - 1)); } // Right if (drawRight) { - vruler.Draw (new (screenBounds.X + screenBounds.Width - 1, screenBounds.Y + 1), 1); + vruler.Draw (driver: Driver, location: new (screenBounds.X + screenBounds.Width - 1, screenBounds.Y + 1), start: 1); } } diff --git a/Terminal.Gui/ViewBase/Adornment/BorderSettings.cs b/Terminal.Gui/ViewBase/Adornment/BorderSettings.cs index 516842000..4921446ae 100644 --- a/Terminal.Gui/ViewBase/Adornment/BorderSettings.cs +++ b/Terminal.Gui/ViewBase/Adornment/BorderSettings.cs @@ -1,12 +1,9 @@ - - namespace Terminal.Gui.ViewBase; /// /// Determines the settings for . /// [Flags] - public enum BorderSettings { /// diff --git a/Terminal.Gui/ViewBase/Adornment/Margin.cs b/Terminal.Gui/ViewBase/Adornment/Margin.cs index 59b39930f..0ce7740ba 100644 --- a/Terminal.Gui/ViewBase/Adornment/Margin.cs +++ b/Terminal.Gui/ViewBase/Adornment/Margin.cs @@ -1,4 +1,4 @@ -#nullable enable + using System.Runtime.InteropServices; @@ -80,10 +80,10 @@ public class Margin : Adornment if (view.Margin?.GetCachedClip () != null) { view.Margin!.NeedsDraw = true; - Region? saved = GetClip (); - View.SetClip (view.Margin!.GetCachedClip ()); - view.Margin!.Draw (); - View.SetClip (saved); + Region? saved = view.GetClip (); + view.SetClip (view.Margin!.GetCachedClip ()); + view.Margin!.Draw (); + view.SetClip (saved); view.Margin!.ClearCachedClip (); } @@ -128,7 +128,7 @@ public class Margin : Adornment // This just draws/clears the thickness, not the insides. // TODO: This is a hack. See https://github.com/gui-cs/Terminal.Gui/issues/4016 //SetAttribute (GetAttributeForRole (VisualRole.Normal)); - Thickness.Draw (screen, Diagnostics, ToString ()); + Thickness.Draw (Driver, screen, Diagnostics, ToString ()); } if (ShadowStyle != ShadowStyle.None) diff --git a/Terminal.Gui/ViewBase/Adornment/Padding.cs b/Terminal.Gui/ViewBase/Adornment/Padding.cs index 508670504..0f19073a4 100644 --- a/Terminal.Gui/ViewBase/Adornment/Padding.cs +++ b/Terminal.Gui/ViewBase/Adornment/Padding.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.ViewBase; diff --git a/Terminal.Gui/ViewBase/Adornment/ShadowView.cs b/Terminal.Gui/ViewBase/Adornment/ShadowView.cs index 151aa149c..9212e7271 100644 --- a/Terminal.Gui/ViewBase/Adornment/ShadowView.cs +++ b/Terminal.Gui/ViewBase/Adornment/ShadowView.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.ViewBase; @@ -144,9 +144,9 @@ internal class ShadowView : View { if (SuperView is not Adornment adornment || location.X < 0 - || location.X >= Application.Screen.Width + || location.X >= App?.Screen.Width || location.Y < 0 - || location.Y >= Application.Screen.Height) + || location.Y >= App?.Screen.Height) { return Attribute.Default; } @@ -170,7 +170,7 @@ internal class ShadowView : View // use the Normal attribute from the View under the shadow. if (newAttribute.Background == Color.DarkGray) { - List currentViewsUnderMouse = View.GetViewsUnderLocation (location, ViewportSettingsFlags.Transparent); + List currentViewsUnderMouse = GetViewsUnderLocation (location, ViewportSettingsFlags.Transparent); View? underView = currentViewsUnderMouse!.LastOrDefault (); attr = underView?.GetAttributeForRole (VisualRole.Normal) ?? Attribute.Default; diff --git a/Terminal.Gui/ViewBase/DrawAdornmentsEventArgs.cs b/Terminal.Gui/ViewBase/DrawAdornmentsEventArgs.cs index 97092cf91..4fc8b826b 100644 --- a/Terminal.Gui/ViewBase/DrawAdornmentsEventArgs.cs +++ b/Terminal.Gui/ViewBase/DrawAdornmentsEventArgs.cs @@ -1,5 +1,4 @@ -#nullable enable -namespace Terminal.Gui.ViewBase; +namespace Terminal.Gui.ViewBase; /// /// Provides data for events that allow cancellation of adornment drawing in the Cancellable Work Pattern (CWP). diff --git a/Terminal.Gui/ViewBase/DrawContext.cs b/Terminal.Gui/ViewBase/DrawContext.cs index 95eec9a2e..e6df3033b 100644 --- a/Terminal.Gui/ViewBase/DrawContext.cs +++ b/Terminal.Gui/ViewBase/DrawContext.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.ViewBase; /// diff --git a/Terminal.Gui/ViewBase/DrawEventArgs.cs b/Terminal.Gui/ViewBase/DrawEventArgs.cs index f00bdb618..68be3544c 100644 --- a/Terminal.Gui/ViewBase/DrawEventArgs.cs +++ b/Terminal.Gui/ViewBase/DrawEventArgs.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.ComponentModel; +using System.ComponentModel; namespace Terminal.Gui.ViewBase; diff --git a/Terminal.Gui/ViewBase/EnumExtensions/AddOrSubtractExtensions.cs b/Terminal.Gui/ViewBase/EnumExtensions/AddOrSubtractExtensions.cs index eef2b4372..65ee7fd9e 100644 --- a/Terminal.Gui/ViewBase/EnumExtensions/AddOrSubtractExtensions.cs +++ b/Terminal.Gui/ViewBase/EnumExtensions/AddOrSubtractExtensions.cs @@ -1,4 +1,3 @@ -#nullable enable using System.CodeDom.Compiler; using System.Diagnostics; diff --git a/Terminal.Gui/ViewBase/EnumExtensions/AlignmentExtensions.cs b/Terminal.Gui/ViewBase/EnumExtensions/AlignmentExtensions.cs index e94e6b93e..b6c2b05d4 100644 --- a/Terminal.Gui/ViewBase/EnumExtensions/AlignmentExtensions.cs +++ b/Terminal.Gui/ViewBase/EnumExtensions/AlignmentExtensions.cs @@ -1,4 +1,3 @@ -#nullable enable using System.CodeDom.Compiler; using System.Diagnostics; diff --git a/Terminal.Gui/ViewBase/EnumExtensions/AlignmentModesExtensions.cs b/Terminal.Gui/ViewBase/EnumExtensions/AlignmentModesExtensions.cs index da91e93df..b129642f0 100644 --- a/Terminal.Gui/ViewBase/EnumExtensions/AlignmentModesExtensions.cs +++ b/Terminal.Gui/ViewBase/EnumExtensions/AlignmentModesExtensions.cs @@ -1,4 +1,3 @@ -#nullable enable using System.CodeDom.Compiler; using System.Diagnostics; diff --git a/Terminal.Gui/ViewBase/EnumExtensions/BorderSettingsExtensions.cs b/Terminal.Gui/ViewBase/EnumExtensions/BorderSettingsExtensions.cs index b908950d6..c01b5b9b5 100644 --- a/Terminal.Gui/ViewBase/EnumExtensions/BorderSettingsExtensions.cs +++ b/Terminal.Gui/ViewBase/EnumExtensions/BorderSettingsExtensions.cs @@ -1,4 +1,4 @@ -#nullable enable + using System.CodeDom.Compiler; using System.Diagnostics; diff --git a/Terminal.Gui/ViewBase/EnumExtensions/DimAutoStyleExtensions.cs b/Terminal.Gui/ViewBase/EnumExtensions/DimAutoStyleExtensions.cs index edccb473e..3633cc5e8 100644 --- a/Terminal.Gui/ViewBase/EnumExtensions/DimAutoStyleExtensions.cs +++ b/Terminal.Gui/ViewBase/EnumExtensions/DimAutoStyleExtensions.cs @@ -1,4 +1,3 @@ -#nullable enable using System.CodeDom.Compiler; using System.Diagnostics; diff --git a/Terminal.Gui/ViewBase/EnumExtensions/DimPercentModeExtensions.cs b/Terminal.Gui/ViewBase/EnumExtensions/DimPercentModeExtensions.cs index f6a62db75..0eac30890 100644 --- a/Terminal.Gui/ViewBase/EnumExtensions/DimPercentModeExtensions.cs +++ b/Terminal.Gui/ViewBase/EnumExtensions/DimPercentModeExtensions.cs @@ -1,4 +1,3 @@ -#nullable enable using System.CodeDom.Compiler; using System.Diagnostics; diff --git a/Terminal.Gui/ViewBase/EnumExtensions/DimensionExtensions.cs b/Terminal.Gui/ViewBase/EnumExtensions/DimensionExtensions.cs index 0a217e1a8..7d34f36ad 100644 --- a/Terminal.Gui/ViewBase/EnumExtensions/DimensionExtensions.cs +++ b/Terminal.Gui/ViewBase/EnumExtensions/DimensionExtensions.cs @@ -1,4 +1,3 @@ -#nullable enable using System.CodeDom.Compiler; using System.Diagnostics; diff --git a/Terminal.Gui/ViewBase/EnumExtensions/SideExtensions.cs b/Terminal.Gui/ViewBase/EnumExtensions/SideExtensions.cs index f7a2a548f..cbf67f18d 100644 --- a/Terminal.Gui/ViewBase/EnumExtensions/SideExtensions.cs +++ b/Terminal.Gui/ViewBase/EnumExtensions/SideExtensions.cs @@ -1,4 +1,3 @@ -#nullable enable using System.CodeDom.Compiler; using System.Diagnostics; diff --git a/Terminal.Gui/ViewBase/EnumExtensions/ViewDiagnosticFlagsExtensions.cs b/Terminal.Gui/ViewBase/EnumExtensions/ViewDiagnosticFlagsExtensions.cs index e0d089184..400d2d081 100644 --- a/Terminal.Gui/ViewBase/EnumExtensions/ViewDiagnosticFlagsExtensions.cs +++ b/Terminal.Gui/ViewBase/EnumExtensions/ViewDiagnosticFlagsExtensions.cs @@ -1,4 +1,3 @@ -#nullable enable using System.CodeDom.Compiler; using System.Diagnostics; diff --git a/Terminal.Gui/ViewBase/Helpers/StackExtensions.cs b/Terminal.Gui/ViewBase/Helpers/StackExtensions.cs index 96201df73..e76788029 100644 --- a/Terminal.Gui/ViewBase/Helpers/StackExtensions.cs +++ b/Terminal.Gui/ViewBase/Helpers/StackExtensions.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.ViewBase; /// Extension of helper to work with specific diff --git a/Terminal.Gui/ViewBase/IDesignable.cs b/Terminal.Gui/ViewBase/IDesignable.cs index 382bdd91b..c7625d079 100644 --- a/Terminal.Gui/ViewBase/IDesignable.cs +++ b/Terminal.Gui/ViewBase/IDesignable.cs @@ -9,10 +9,10 @@ public interface IDesignable /// Causes the View to enable design-time mode. This typically means that the view will load demo data and /// be configured to allow for design-time manipulation. /// - /// Optional arbitrary, View-specific, context. - /// A non-null type for . + /// + /// A non-null type for . /// if the view successfully loaded demo data. - public bool EnableForDesign (ref TContext context) where TContext : notnull => EnableForDesign (); + public bool EnableForDesign (ref TContext targetView) where TContext : notnull => EnableForDesign (); /// /// Causes the View to enable design-time mode. This typically means that the view will load demo data and diff --git a/Terminal.Gui/ViewBase/IMouseHeldDown.cs b/Terminal.Gui/ViewBase/IMouseHeldDown.cs index 5f7435793..d0233fcd3 100644 --- a/Terminal.Gui/ViewBase/IMouseHeldDown.cs +++ b/Terminal.Gui/ViewBase/IMouseHeldDown.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.ComponentModel; +using System.ComponentModel; namespace Terminal.Gui.ViewBase; diff --git a/Terminal.Gui/ViewBase/Layout/Aligner.cs b/Terminal.Gui/ViewBase/Layout/Aligner.cs index 72aae7ad5..c3907bf92 100644 --- a/Terminal.Gui/ViewBase/Layout/Aligner.cs +++ b/Terminal.Gui/ViewBase/Layout/Aligner.cs @@ -1,3 +1,4 @@ +#nullable disable using System.ComponentModel; namespace Terminal.Gui.ViewBase; diff --git a/Terminal.Gui/ViewBase/Layout/Dim.cs b/Terminal.Gui/ViewBase/Layout/Dim.cs index a3326e80a..8952b980a 100644 --- a/Terminal.Gui/ViewBase/Layout/Dim.cs +++ b/Terminal.Gui/ViewBase/Layout/Dim.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.ViewBase; using System.Numerics; @@ -188,18 +187,18 @@ public abstract record Dim : IEqualityOperators /// - /// Indicates whether the specified type is in the hierarchy of this Dim object. + /// Indicates whether the specified type is in the hierarchy of this Dim object. /// /// A reference to this instance. /// - public bool Has (out T dim) where T : Dim + public bool Has (out TDim dim) where TDim : Dim { - dim = (this as T)!; + dim = (this as TDim)!; return this switch { - DimCombine combine => combine.Left.Has (out dim) || combine.Right.Has (out dim), - T => true, + DimCombine combine => combine.Left.Has (out dim) || combine.Right.Has (out dim), + TDim => true, _ => false }; } diff --git a/Terminal.Gui/ViewBase/Layout/DimAbsolute.cs b/Terminal.Gui/ViewBase/Layout/DimAbsolute.cs index 6fd9e8072..f630b3470 100644 --- a/Terminal.Gui/ViewBase/Layout/DimAbsolute.cs +++ b/Terminal.Gui/ViewBase/Layout/DimAbsolute.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.ViewBase; /// diff --git a/Terminal.Gui/ViewBase/Layout/DimAuto.cs b/Terminal.Gui/ViewBase/Layout/DimAuto.cs index b70aca542..1436eb9bc 100644 --- a/Terminal.Gui/ViewBase/Layout/DimAuto.cs +++ b/Terminal.Gui/ViewBase/Layout/DimAuto.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics; namespace Terminal.Gui.ViewBase; @@ -31,8 +30,10 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt var textSize = 0; var maxCalculatedSize = 0; + // 2048 x 2048 supports unit testing where no App is running. + Size screenSize = us.App?.Screen.Size ?? new (2048, 2048); int autoMin = MinimumContentDim?.GetAnchor (superviewContentSize) ?? 0; - int screenX4 = dimension == Dimension.Width ? Application.Screen.Width * 4 : Application.Screen.Height * 4; + int screenX4 = dimension == Dimension.Width ? screenSize.Width * 4 : screenSize.Height * 4; int autoMax = MaximumContentDim?.GetAnchor (superviewContentSize) ?? screenX4; //Debug.WriteLineIf (autoMin > autoMax, "MinimumContentDim must be less than or equal to MaximumContentDim."); diff --git a/Terminal.Gui/ViewBase/Layout/DimAutoStyle.cs b/Terminal.Gui/ViewBase/Layout/DimAutoStyle.cs index d3080dd94..ddd6e0ee5 100644 --- a/Terminal.Gui/ViewBase/Layout/DimAutoStyle.cs +++ b/Terminal.Gui/ViewBase/Layout/DimAutoStyle.cs @@ -1,5 +1,3 @@ - - namespace Terminal.Gui.ViewBase; /// @@ -45,4 +43,4 @@ public enum DimAutoStyle /// corresponding dimension /// Auto = Content | Text, -} \ No newline at end of file +} diff --git a/Terminal.Gui/ViewBase/Layout/DimCombine.cs b/Terminal.Gui/ViewBase/Layout/DimCombine.cs index 3bf3b3534..b246d88bb 100644 --- a/Terminal.Gui/ViewBase/Layout/DimCombine.cs +++ b/Terminal.Gui/ViewBase/Layout/DimCombine.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.ViewBase; /// diff --git a/Terminal.Gui/ViewBase/Layout/DimFill.cs b/Terminal.Gui/ViewBase/Layout/DimFill.cs index 0e278cbc5..feda107a0 100644 --- a/Terminal.Gui/ViewBase/Layout/DimFill.cs +++ b/Terminal.Gui/ViewBase/Layout/DimFill.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.ViewBase; /// diff --git a/Terminal.Gui/ViewBase/Layout/DimFunc.cs b/Terminal.Gui/ViewBase/Layout/DimFunc.cs index 0773dc316..678406a9b 100644 --- a/Terminal.Gui/ViewBase/Layout/DimFunc.cs +++ b/Terminal.Gui/ViewBase/Layout/DimFunc.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.ViewBase; /// diff --git a/Terminal.Gui/ViewBase/Layout/DimPercent.cs b/Terminal.Gui/ViewBase/Layout/DimPercent.cs index 499ffb6fd..2b9ade3b3 100644 --- a/Terminal.Gui/ViewBase/Layout/DimPercent.cs +++ b/Terminal.Gui/ViewBase/Layout/DimPercent.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.ViewBase; /// diff --git a/Terminal.Gui/ViewBase/Layout/DimPercentMode.cs b/Terminal.Gui/ViewBase/Layout/DimPercentMode.cs index 3f9aed836..bdb458d38 100644 --- a/Terminal.Gui/ViewBase/Layout/DimPercentMode.cs +++ b/Terminal.Gui/ViewBase/Layout/DimPercentMode.cs @@ -1,5 +1,3 @@ - - namespace Terminal.Gui.ViewBase; /// @@ -16,4 +14,4 @@ public enum DimPercentMode /// The dimension is computed using the View's . /// ContentSize = 1 -} \ No newline at end of file +} diff --git a/Terminal.Gui/ViewBase/Layout/DimView.cs b/Terminal.Gui/ViewBase/Layout/DimView.cs index 0a25e1983..fec551d67 100644 --- a/Terminal.Gui/ViewBase/Layout/DimView.cs +++ b/Terminal.Gui/ViewBase/Layout/DimView.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.ViewBase; /// diff --git a/Terminal.Gui/ViewBase/Layout/Dimension.cs b/Terminal.Gui/ViewBase/Layout/Dimension.cs index 5fae94360..60fbcaea6 100644 --- a/Terminal.Gui/ViewBase/Layout/Dimension.cs +++ b/Terminal.Gui/ViewBase/Layout/Dimension.cs @@ -1,5 +1,3 @@ - - namespace Terminal.Gui.ViewBase; /// @@ -21,4 +19,4 @@ public enum Dimension /// The width dimension. /// Width = 2 -} \ No newline at end of file +} diff --git a/Terminal.Gui/ViewBase/Layout/LayoutException.cs b/Terminal.Gui/ViewBase/Layout/LayoutException.cs index e21dc51f8..176f26982 100644 --- a/Terminal.Gui/ViewBase/Layout/LayoutException.cs +++ b/Terminal.Gui/ViewBase/Layout/LayoutException.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.ViewBase; /// diff --git a/Terminal.Gui/ViewBase/Layout/Pos.cs b/Terminal.Gui/ViewBase/Layout/Pos.cs index 09694adc3..1e6e00688 100644 --- a/Terminal.Gui/ViewBase/Layout/Pos.cs +++ b/Terminal.Gui/ViewBase/Layout/Pos.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.ViewBase; @@ -334,18 +333,18 @@ public abstract record Pos internal virtual bool ReferencesOtherViews () { return false; } /// - /// Indicates whether the specified type is in the hierarchy of this Pos object. + /// Indicates whether the specified type is in the hierarchy of this Pos object. /// /// A reference to this instance. /// - public bool Has (out T pos) where T : Pos + public bool Has (out TPos pos) where TPos : Pos { - pos = (this as T)!; + pos = (this as TPos)!; return this switch { - PosCombine combine => combine.Left.Has (out pos) || combine.Right.Has (out pos), - T => true, + PosCombine combine => combine.Left.Has (out pos) || combine.Right.Has (out pos), + TPos => true, _ => false }; } diff --git a/Terminal.Gui/ViewBase/Layout/PosAbsolute.cs b/Terminal.Gui/ViewBase/Layout/PosAbsolute.cs index b21711355..494c63a50 100644 --- a/Terminal.Gui/ViewBase/Layout/PosAbsolute.cs +++ b/Terminal.Gui/ViewBase/Layout/PosAbsolute.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.ViewBase; /// diff --git a/Terminal.Gui/ViewBase/Layout/PosAlign.cs b/Terminal.Gui/ViewBase/Layout/PosAlign.cs index 4d72cc9af..d6289e510 100644 --- a/Terminal.Gui/ViewBase/Layout/PosAlign.cs +++ b/Terminal.Gui/ViewBase/Layout/PosAlign.cs @@ -1,4 +1,3 @@ -#nullable enable using System.ComponentModel; diff --git a/Terminal.Gui/ViewBase/Layout/PosAnchorEnd.cs b/Terminal.Gui/ViewBase/Layout/PosAnchorEnd.cs index 3cf2195ac..d7c6d30a7 100644 --- a/Terminal.Gui/ViewBase/Layout/PosAnchorEnd.cs +++ b/Terminal.Gui/ViewBase/Layout/PosAnchorEnd.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.ViewBase; /// diff --git a/Terminal.Gui/ViewBase/Layout/PosCenter.cs b/Terminal.Gui/ViewBase/Layout/PosCenter.cs index 4a7945cd2..d1584f8e5 100644 --- a/Terminal.Gui/ViewBase/Layout/PosCenter.cs +++ b/Terminal.Gui/ViewBase/Layout/PosCenter.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.ViewBase; /// diff --git a/Terminal.Gui/ViewBase/Layout/PosCombine.cs b/Terminal.Gui/ViewBase/Layout/PosCombine.cs index 833b499f0..1be5325c2 100644 --- a/Terminal.Gui/ViewBase/Layout/PosCombine.cs +++ b/Terminal.Gui/ViewBase/Layout/PosCombine.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.ViewBase; /// diff --git a/Terminal.Gui/ViewBase/Layout/PosFunc.cs b/Terminal.Gui/ViewBase/Layout/PosFunc.cs index 3900beb46..078459fd9 100644 --- a/Terminal.Gui/ViewBase/Layout/PosFunc.cs +++ b/Terminal.Gui/ViewBase/Layout/PosFunc.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.ViewBase; /// diff --git a/Terminal.Gui/ViewBase/Layout/PosPercent.cs b/Terminal.Gui/ViewBase/Layout/PosPercent.cs index 98505dd3b..08d968210 100644 --- a/Terminal.Gui/ViewBase/Layout/PosPercent.cs +++ b/Terminal.Gui/ViewBase/Layout/PosPercent.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.ViewBase; /// diff --git a/Terminal.Gui/ViewBase/Layout/PosView.cs b/Terminal.Gui/ViewBase/Layout/PosView.cs index fb7f7266a..d42e8cb10 100644 --- a/Terminal.Gui/ViewBase/Layout/PosView.cs +++ b/Terminal.Gui/ViewBase/Layout/PosView.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics; namespace Terminal.Gui.ViewBase; diff --git a/Terminal.Gui/ViewBase/Layout/Side.cs b/Terminal.Gui/ViewBase/Layout/Side.cs index b2256faac..0cc52f064 100644 --- a/Terminal.Gui/ViewBase/Layout/Side.cs +++ b/Terminal.Gui/ViewBase/Layout/Side.cs @@ -1,5 +1,3 @@ - - namespace Terminal.Gui.ViewBase; /// @@ -27,4 +25,4 @@ public enum Side /// The bottom (Y + Height) side of the view. /// Bottom = 3 -} \ No newline at end of file +} diff --git a/Terminal.Gui/ViewBase/Layout/SuperViewChangedEventArgs.cs b/Terminal.Gui/ViewBase/Layout/SuperViewChangedEventArgs.cs index f1e79536d..611949a94 100644 --- a/Terminal.Gui/ViewBase/Layout/SuperViewChangedEventArgs.cs +++ b/Terminal.Gui/ViewBase/Layout/SuperViewChangedEventArgs.cs @@ -9,17 +9,17 @@ public class SuperViewChangedEventArgs : EventArgs /// Creates a new instance of the class. /// /// - public SuperViewChangedEventArgs (View superView, View subView) + public SuperViewChangedEventArgs (View? superView, View? subView) { SuperView = superView; SubView = subView; } /// The view that is having it's changed - public View SubView { get; } + public View? SubView { get; } /// /// The parent. For this is the old parent (new parent now being null). /// - public View SuperView { get; } + public View? SuperView { get; } } diff --git a/Terminal.Gui/ViewBase/MouseHeldDown.cs b/Terminal.Gui/ViewBase/MouseHeldDown.cs index ff0764733..f902980a3 100644 --- a/Terminal.Gui/ViewBase/MouseHeldDown.cs +++ b/Terminal.Gui/ViewBase/MouseHeldDown.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.ComponentModel; +using System.ComponentModel; namespace Terminal.Gui.ViewBase; diff --git a/Terminal.Gui/ViewBase/Navigation/FocusEventArgs.cs b/Terminal.Gui/ViewBase/Navigation/FocusEventArgs.cs index 55cc41c6e..d58c68ee3 100644 --- a/Terminal.Gui/ViewBase/Navigation/FocusEventArgs.cs +++ b/Terminal.Gui/ViewBase/Navigation/FocusEventArgs.cs @@ -10,16 +10,15 @@ public class HasFocusEventArgs : CancelEventArgs /// The value will have if the event is not cancelled. /// The view that is losing focus. /// The view that is gaining focus. - public HasFocusEventArgs (bool currentHasFocus, bool newHasFocus, View currentFocused, View newFocused) : base (ref currentHasFocus, ref newHasFocus) + public HasFocusEventArgs (bool currentHasFocus, bool newHasFocus, View? currentFocused, View? newFocused) : base (ref currentHasFocus, ref newHasFocus) { CurrentFocused = currentFocused; NewFocused = newFocused; } /// Gets or sets the view that is losing focus. - public View CurrentFocused { get; set; } + public View? CurrentFocused { get; set; } /// Gets or sets the view that is gaining focus. - public View NewFocused { get; set; } - -} \ No newline at end of file + public View? NewFocused { get; set; } +} diff --git a/Terminal.Gui/ViewBase/Orientation/IOrientation.cs b/Terminal.Gui/ViewBase/Orientation/IOrientation.cs index 4f86d21dd..c56a0cfe5 100644 --- a/Terminal.Gui/ViewBase/Orientation/IOrientation.cs +++ b/Terminal.Gui/ViewBase/Orientation/IOrientation.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.ViewBase; using System; @@ -40,4 +40,4 @@ public interface IOrientation /// /// public void OnOrientationChanged (Orientation newOrientation) { return; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/ViewBase/Orientation/Orientation.cs b/Terminal.Gui/ViewBase/Orientation/Orientation.cs index fa4c766ee..5c847f57a 100644 --- a/Terminal.Gui/ViewBase/Orientation/Orientation.cs +++ b/Terminal.Gui/ViewBase/Orientation/Orientation.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.ViewBase; /// Direction of an element (horizontal or vertical) diff --git a/Terminal.Gui/ViewBase/Orientation/OrientationHelper.cs b/Terminal.Gui/ViewBase/Orientation/OrientationHelper.cs index a7079128c..dfd8a3d44 100644 --- a/Terminal.Gui/ViewBase/Orientation/OrientationHelper.cs +++ b/Terminal.Gui/ViewBase/Orientation/OrientationHelper.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.ViewBase; diff --git a/Terminal.Gui/ViewBase/View.Adornments.cs b/Terminal.Gui/ViewBase/View.Adornments.cs index de0ca20a1..97d2da40c 100644 --- a/Terminal.Gui/ViewBase/View.Adornments.cs +++ b/Terminal.Gui/ViewBase/View.Adornments.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.ViewBase; public partial class View // Adornments diff --git a/Terminal.Gui/ViewBase/View.Arrangement.cs b/Terminal.Gui/ViewBase/View.Arrangement.cs index dba4f6c83..58323f11a 100644 --- a/Terminal.Gui/ViewBase/View.Arrangement.cs +++ b/Terminal.Gui/ViewBase/View.Arrangement.cs @@ -1,5 +1,4 @@ -#nullable enable -namespace Terminal.Gui.ViewBase; +namespace Terminal.Gui.ViewBase; public partial class View { diff --git a/Terminal.Gui/ViewBase/View.Command.cs b/Terminal.Gui/ViewBase/View.Command.cs index 0c3a741ac..ca8de67a1 100644 --- a/Terminal.Gui/ViewBase/View.Command.cs +++ b/Terminal.Gui/ViewBase/View.Command.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.ViewBase; public partial class View // Command APIs diff --git a/Terminal.Gui/ViewBase/View.Content.cs b/Terminal.Gui/ViewBase/View.Content.cs index cbb29308a..8d6345a65 100644 --- a/Terminal.Gui/ViewBase/View.Content.cs +++ b/Terminal.Gui/ViewBase/View.Content.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.ViewBase; public partial class View diff --git a/Terminal.Gui/ViewBase/View.Cursor.cs b/Terminal.Gui/ViewBase/View.Cursor.cs index daaf75d69..d710a273e 100644 --- a/Terminal.Gui/ViewBase/View.Cursor.cs +++ b/Terminal.Gui/ViewBase/View.Cursor.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.ViewBase; diff --git a/Terminal.Gui/ViewBase/View.Diagnostics.cs b/Terminal.Gui/ViewBase/View.Diagnostics.cs index d920ef4bf..19f77eac7 100644 --- a/Terminal.Gui/ViewBase/View.Diagnostics.cs +++ b/Terminal.Gui/ViewBase/View.Diagnostics.cs @@ -1,5 +1,4 @@ -#nullable enable -namespace Terminal.Gui.ViewBase; +namespace Terminal.Gui.ViewBase; public partial class View { diff --git a/Terminal.Gui/ViewBase/View.Drawing.Attribute.cs b/Terminal.Gui/ViewBase/View.Drawing.Attribute.cs index b43a426d2..81167c439 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.Attribute.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.Attribute.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.ComponentModel; +using System.ComponentModel; namespace Terminal.Gui.ViewBase; diff --git a/Terminal.Gui/ViewBase/View.Drawing.Clipping.cs b/Terminal.Gui/ViewBase/View.Drawing.Clipping.cs index 49a0e1fe3..6a5bbeb67 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.Clipping.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.Clipping.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.ViewBase; public partial class View @@ -16,7 +15,7 @@ public partial class View /// /// /// The current Clip. - public static Region? GetClip () { return Application.Driver?.Clip; } + public Region? GetClip () => Driver?.Clip; /// /// Sets the Clip to the specified region. @@ -28,11 +27,13 @@ public partial class View /// /// /// - public static void SetClip (Region? region) + public void SetClip (Region? region) { - if (Application.Driver is { } && region is { }) + // BUGBUG: If region is null we should set the clip to null. + // BUGBUG: Fixing this probably breaks other things. + if (Driver is { } && region is { }) { - Application.Driver.Clip = region; + Driver.Clip = region; } } @@ -51,13 +52,13 @@ public partial class View /// /// The current Clip, which can be then re-applied /// - public static Region? SetClipToScreen () + public Region? SetClipToScreen () { Region? previous = GetClip (); - if (Application.Driver is { }) + if (Driver is { }) { - Application.Driver.Clip = new (Application.Screen); + Driver.Clip = new (Driver!.Screen); } return previous; @@ -72,7 +73,7 @@ public partial class View /// /// /// - public static void ExcludeFromClip (Rectangle rectangle) { Application.Driver?.Clip?.Exclude (rectangle); } + public void ExcludeFromClip (Rectangle rectangle) { Driver?.Clip?.Exclude (rectangle); } /// /// Removes the specified rectangle from the Clip. @@ -83,7 +84,7 @@ public partial class View /// /// /// - public static void ExcludeFromClip (Region? region) { Application.Driver?.Clip?.Exclude (region); } + public void ExcludeFromClip (Region? region) { Driver?.Clip?.Exclude (region); } /// /// Changes the Clip to the intersection of the current Clip and the of this View. @@ -104,7 +105,7 @@ public partial class View return null; } - Region previous = GetClip () ?? new (Application.Screen); + Region previous = GetClip () ?? new (Driver.Screen); Region frameRegion = previous.Clone (); @@ -151,7 +152,7 @@ public partial class View return null; } - Region previous = GetClip () ?? new (Application.Screen); + Region previous = GetClip () ?? new (App!.Screen); Region viewportRegion = previous.Clone (); diff --git a/Terminal.Gui/ViewBase/View.Drawing.Primitives.cs b/Terminal.Gui/ViewBase/View.Drawing.Primitives.cs index d9d9333ee..6b169718b 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.Primitives.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.Primitives.cs @@ -63,7 +63,7 @@ public partial class View /// /// /// When the method returns, the draw position will be incremented by the number of columns - /// required, unless the new column value is outside the or . + /// required, unless the new column value is outside the or . /// /// If requires more columns than are available, the output will be clipped. /// @@ -121,8 +121,8 @@ public partial class View { DrawHotString ( text, - Enabled ? GetAttributeForRole (VisualRole.HotNormal) : GetScheme ()!.Disabled, - Enabled ? GetAttributeForRole (VisualRole.Normal) : GetScheme ()!.Disabled + Enabled ? GetAttributeForRole (VisualRole.HotNormal) : GetScheme ().Disabled, + Enabled ? GetAttributeForRole (VisualRole.Normal) : GetScheme ().Disabled ); } } @@ -137,7 +137,7 @@ public partial class View return; } - Region prevClip = AddViewportToClip (); + Region? prevClip = AddViewportToClip (); Rectangle toClear = ViewportToScreen (rect); Attribute prev = SetAttribute (new (color ?? GetAttributeForRole (VisualRole.Normal).Background)); Driver.FillRect (toClear); @@ -155,7 +155,7 @@ public partial class View return; } - Region prevClip = AddViewportToClip (); + Region? prevClip = AddViewportToClip (); Rectangle toClear = ViewportToScreen (rect); Driver.FillRect (toClear, rune); SetClip (prevClip); diff --git a/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs b/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs index dcd28794b..5ad8c0101 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs @@ -1,5 +1,4 @@ -#nullable enable -namespace Terminal.Gui.ViewBase; +namespace Terminal.Gui.ViewBase; public partial class View { diff --git a/Terminal.Gui/ViewBase/View.Drawing.cs b/Terminal.Gui/ViewBase/View.Drawing.cs index 1f2756123..ed300ee5d 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.ComponentModel; +using System.ComponentModel; using System.Diagnostics; namespace Terminal.Gui.ViewBase; @@ -206,7 +205,7 @@ public partial class View // Drawing APIs if (Margin?.NeedsLayout == true) { Margin.NeedsLayout = false; - Margin?.Thickness.Draw (FrameToScreen ()); + Margin?.Thickness.Draw (Driver, FrameToScreen ()); Margin?.Parent?.SetSubViewNeedsDraw (); } @@ -446,12 +445,15 @@ public partial class View // Drawing APIs // Report the drawn area to the context context?.AddDrawnRegion (textRegion); - TextFormatter?.Draw ( - drawRect, - HasFocus ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal), - HasFocus ? GetAttributeForRole (VisualRole.HotFocus) : GetAttributeForRole (VisualRole.HotNormal), - Rectangle.Empty - ); + if (Driver is { }) + { + TextFormatter?.Draw ( + Driver, + drawRect, + HasFocus ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal), + HasFocus ? GetAttributeForRole (VisualRole.HotFocus) : GetAttributeForRole (VisualRole.HotNormal), + Rectangle.Empty); + } // We assume that the text has been drawn over the entire area; ensure that the subviews are redrawn. SetSubViewNeedsDraw (); diff --git a/Terminal.Gui/ViewBase/View.Hierarchy.cs b/Terminal.Gui/ViewBase/View.Hierarchy.cs index a80193c38..b88a07199 100644 --- a/Terminal.Gui/ViewBase/View.Hierarchy.cs +++ b/Terminal.Gui/ViewBase/View.Hierarchy.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -363,12 +362,12 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, #endregion AddRemove - // TODO: This drives a weird coupling of Application.Top and View. It's not clear why this is needed. + // TODO: This drives a weird coupling of Application.Current and View. It's not clear why this is needed. /// Get the top superview of a given . /// The superview view. internal View? GetTopSuperView (View? view = null, View? superview = null) { - View? top = superview ?? Application.Top; + View? top = superview ?? App?.Current; for (View? v = view?.SuperView ?? this?.SuperView; v != null; v = v.SuperView) { diff --git a/Terminal.Gui/ViewBase/View.Keyboard.cs b/Terminal.Gui/ViewBase/View.Keyboard.cs index 51350dca7..8f9a5127a 100644 --- a/Terminal.Gui/ViewBase/View.Keyboard.cs +++ b/Terminal.Gui/ViewBase/View.Keyboard.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.ViewBase; public partial class View // Keyboard APIs diff --git a/Terminal.Gui/ViewBase/View.Layout.cs b/Terminal.Gui/ViewBase/View.Layout.cs index 0c73aacbb..ecd58e737 100644 --- a/Terminal.Gui/ViewBase/View.Layout.cs +++ b/Terminal.Gui/ViewBase/View.Layout.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics; namespace Terminal.Gui.ViewBase; @@ -438,10 +437,10 @@ public partial class View // Layout APIs private void NeedsClearScreenNextIteration () { - if (Application.Top is { } && Application.Top == this && Application.TopLevels.Count == 1) + if (App is { Current: { } } && App.Current == this && App.SessionStack.Count == 1) { // If this is the only TopLevel, we need to redraw the screen - Application.ClearScreenNextIteration = true; + App.ClearScreenNextIteration = true; } } @@ -532,7 +531,7 @@ public partial class View // Layout APIs /// /// Performs layout of the view and its subviews using the content size of either the or - /// . + /// . /// /// /// @@ -545,7 +544,7 @@ public partial class View // Layout APIs /// /// /// If the view could not be laid out (typically because dependency was not ready). - public bool Layout () { return Layout (GetContainerSize ()); } + public bool Layout () => Layout (GetContainerSize ()); /// /// Sets the position and size of this view, relative to the SuperView's ContentSize (nominally the same as @@ -1114,11 +1113,12 @@ public partial class View // Layout APIs { // TODO: Get rid of refs to Top Size superViewContentSize = SuperView?.GetContentSize () - ?? (Application.Top is { } && Application.Top != this && Application.Top.IsInitialized - ? Application.Top.GetContentSize () - : Application.Screen.Size); + ?? (App?.Current is { } && App?.Current != this && App!.Current.IsInitialized + ? App.Current.GetContentSize () + : App?.Screen.Size ?? new (2048, 2048)); return superViewContentSize; + } // BUGBUG: This method interferes with Dialog/MessageBox default min/max size. @@ -1130,7 +1130,7 @@ public partial class View // Layout APIs /// /// /// If does not have a or it's SuperView is not - /// the position will be bound by . + /// the position will be bound by . /// /// The View that is to be moved. /// The target x location. @@ -1138,7 +1138,7 @@ public partial class View // Layout APIs /// The new x location that will ensure will be fully visible. /// The new y location that will ensure will be fully visible. /// - /// Either (if does not have a Super View) or + /// Either (if does not have a Super View) or /// 's SuperView. This can be used to ensure LayoutSubViews is called on the correct View. /// internal static View? GetLocationEnsuringFullVisibility ( @@ -1152,10 +1152,12 @@ public partial class View // Layout APIs int maxDimension; View? superView; - if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) + IApplication? app = viewToMove.App; + + if (viewToMove?.SuperView is null || viewToMove == app?.Current || viewToMove?.SuperView == app?.Current) { - maxDimension = Application.Screen.Width; - superView = Application.Top; + maxDimension = app?.Screen.Width ?? 0; + superView = app?.Current; } else { @@ -1188,9 +1190,9 @@ public partial class View // Layout APIs var menuVisible = false; var statusVisible = false; - if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) + if (viewToMove?.SuperView is null || viewToMove == app?.Current || viewToMove?.SuperView == app?.Current) { - menuVisible = Application.Top?.MenuBar?.Visible == true; + menuVisible = app?.Current?.MenuBar?.Visible == true; } else { @@ -1207,7 +1209,7 @@ public partial class View // Layout APIs } } - if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) + if (viewToMove?.SuperView is null || viewToMove == app?.Current || viewToMove?.SuperView == app?.Current) { maxDimension = menuVisible ? 1 : 0; } @@ -1218,9 +1220,16 @@ public partial class View // Layout APIs ny = Math.Max (targetY, maxDimension); - if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) + if (viewToMove?.SuperView is null || viewToMove == app?.Current || viewToMove?.SuperView == app?.Current) { - maxDimension = statusVisible ? Application.Screen.Height - 1 : Application.Screen.Height; + if (app is { }) + { + maxDimension = statusVisible ? app.Screen.Height - 1 : app.Screen.Height; + } + else + { + maxDimension = 0; + } } else { @@ -1267,10 +1276,10 @@ public partial class View // Layout APIs /// /// flags set in their ViewportSettings. /// - public static List GetViewsUnderLocation (in Point screenLocation, ViewportSettingsFlags excludeViewportSettingsFlags) + public List GetViewsUnderLocation (in Point screenLocation, ViewportSettingsFlags excludeViewportSettingsFlags) { // PopoverHost - If visible, start with it instead of Top - if (Application.Popover?.GetActivePopover () is View { Visible: true } visiblePopover) + if (App?.Popover?.GetActivePopover () is View { Visible: true } visiblePopover) { // BUGBUG: We do not traverse all visible toplevels if there's an active popover. This may be a bug. List result = []; @@ -1286,9 +1295,9 @@ public partial class View // Layout APIs var checkedTop = false; // Traverse all visible toplevels, topmost first (reverse stack order) - if (Application.TopLevels.Count > 0) + if (App?.SessionStack.Count > 0) { - foreach (Toplevel toplevel in Application.TopLevels) + foreach (Toplevel toplevel in App.SessionStack) { if (toplevel.Visible && toplevel.Contains (screenLocation)) { @@ -1301,7 +1310,7 @@ public partial class View // Layout APIs } } - if (toplevel == Application.Top) + if (toplevel == App.Current) { checkedTop = true; } @@ -1309,7 +1318,7 @@ public partial class View // Layout APIs } // Fallback: If TopLevels is empty or Top is not in TopLevels, check Top directly (for test compatibility) - if (!checkedTop && Application.Top is { Visible: true } top) + if (!checkedTop && App?.Current is { Visible: true } top) { // For root toplevels, allow hit-testing even if location is outside bounds (for drag/move) List result = GetViewsUnderLocation (top, screenLocation, excludeViewportSettingsFlags); diff --git a/Terminal.Gui/ViewBase/View.Mouse.cs b/Terminal.Gui/ViewBase/View.Mouse.cs index 546017059..c899c2d2e 100644 --- a/Terminal.Gui/ViewBase/View.Mouse.cs +++ b/Terminal.Gui/ViewBase/View.Mouse.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.ComponentModel; +using System.ComponentModel; namespace Terminal.Gui.ViewBase; @@ -16,7 +15,7 @@ public partial class View // Mouse APIs private void SetupMouse () { - MouseHeldDown = new MouseHeldDown (this, Application.TimedEvents,Application.Mouse); + MouseHeldDown = new MouseHeldDown (this, App?.TimedEvents, App?.Mouse); MouseBindings = new (); // TODO: Should the default really work with any button or just button1? @@ -54,7 +53,7 @@ public partial class View // Mouse APIs #region MouseEnterLeave /// - /// INTERNAL Called by when the mouse moves over the View's + /// INTERNAL Called by when the mouse moves over the View's /// . /// will /// be raised when the mouse is no longer over the . If another View occludes this View, the @@ -151,7 +150,7 @@ public partial class View // Mouse APIs public event EventHandler? MouseEnter; /// - /// INTERNAL Called by when the mouse leaves , or is + /// INTERNAL Called by when the mouse leaves , or is /// occluded /// by another non-SubView. /// @@ -228,7 +227,7 @@ public partial class View // Mouse APIs public bool WantMousePositionReports { get; set; } /// - /// Processes a new . This method is called by when a + /// Processes a new . This method is called by when a /// mouse /// event occurs. /// @@ -375,7 +374,7 @@ public partial class View // Mouse APIs if (mouseEvent.IsReleased) { - if (Application.Mouse.MouseGrabView == this) + if (App?.Mouse.MouseGrabView == this) { //Logging.Debug ($"{Id} - {MouseState}"); MouseState &= ~MouseState.Pressed; @@ -407,9 +406,9 @@ public partial class View // Mouse APIs if (mouseEvent.IsPressed) { // The first time we get pressed event, grab the mouse and set focus - if (Application.Mouse.MouseGrabView != this) + if (App?.Mouse.MouseGrabView != this) { - Application.Mouse.GrabMouse (this); + App?.Mouse.GrabMouse (this); if (!HasFocus && CanFocus) { @@ -541,10 +540,10 @@ public partial class View // Mouse APIs { mouseEvent.Handled = false; - if (Application.Mouse.MouseGrabView == this && mouseEvent.IsSingleClicked) + if (App?.Mouse.MouseGrabView == this && mouseEvent.IsSingleClicked) { // We're grabbed. Clicked event comes after the last Release. This is our signal to ungrab - Application.Mouse.UngrabMouse (); + App?.Mouse.UngrabMouse (); // TODO: Prove we need to unset MouseState.Pressed and MouseState.PressedOutside here // TODO: There may be perf gains if we don't unset these flags here @@ -695,4 +694,4 @@ public partial class View // Mouse APIs #endregion MouseState Handling private void DisposeMouse () { } -} \ No newline at end of file +} diff --git a/Terminal.Gui/ViewBase/View.Navigation.cs b/Terminal.Gui/ViewBase/View.Navigation.cs index 516aef3c0..5ff60aa7e 100644 --- a/Terminal.Gui/ViewBase/View.Navigation.cs +++ b/Terminal.Gui/ViewBase/View.Navigation.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics; @@ -396,7 +395,7 @@ public partial class View // Focus and cross-view navigation management (TabStop public event EventHandler? FocusedChanged; /// Returns a value indicating if this View is currently on Top (Active) - public bool IsCurrentTop => Application.Top == this; + public bool IsCurrentTop => App?.Current == this; /// /// Returns the most focused SubView down the subview-hierarchy. @@ -520,7 +519,7 @@ public partial class View // Focus and cross-view navigation management (TabStop if (value) { // NOTE: If Application.Navigation is null, we pass null to FocusChanging. For unit tests. - (bool focusSet, bool _) = SetHasFocusTrue (Application.Navigation?.GetFocused ()); + (bool focusSet, bool _) = SetHasFocusTrue (App?.Navigation?.GetFocused ()); if (focusSet) { @@ -557,7 +556,7 @@ public partial class View // Focus and cross-view navigation management (TabStop /// if the focus changed; false otherwise. public bool SetFocus () { - (bool focusSet, bool _) = SetHasFocusTrue (Application.Navigation?.GetFocused ()); + (bool focusSet, bool _) = SetHasFocusTrue (App?.Navigation?.GetFocused ()); return focusSet; } @@ -722,17 +721,17 @@ public partial class View // Focus and cross-view navigation management (TabStop return true; } - View? appFocused = Application.Navigation?.GetFocused (); + View? appFocused = App?.Navigation?.GetFocused (); if (appFocused == currentFocused) { if (newFocused is { HasFocus: true }) { - Application.Navigation?.SetFocused (newFocused); + App?.Navigation?.SetFocused (newFocused); } else { - Application.Navigation?.SetFocused (null); + App?.Navigation?.SetFocused (null); } } @@ -835,7 +834,7 @@ public partial class View // Focus and cross-view navigation management (TabStop } // Application.Navigation.GetFocused? - View? applicationFocused = Application.Navigation?.GetFocused (); + View? applicationFocused = App?.Navigation?.GetFocused (); if (newFocusedView is null && applicationFocused != this && applicationFocused is { CanFocus: true }) { @@ -854,18 +853,18 @@ public partial class View // Focus and cross-view navigation management (TabStop } } - // Application.Top? - if (newFocusedView is null && Application.Top is { CanFocus: true, HasFocus: false }) + // Application.Current? + if (newFocusedView is null && App?.Current is { CanFocus: true, HasFocus: false }) { // Temporarily ensure this view can't get focus bool prevCanFocus = _canFocus; _canFocus = false; - bool restoredFocus = Application.Top.RestoreFocus (); + bool restoredFocus = App?.Current.RestoreFocus () ?? false; _canFocus = prevCanFocus; - if (Application.Top is { CanFocus: true, HasFocus: true }) + if (App?.Current is { CanFocus: true, HasFocus: true }) { - newFocusedView = Application.Top; + newFocusedView = App?.Current; } else if (restoredFocus) { @@ -952,7 +951,7 @@ public partial class View // Focus and cross-view navigation management (TabStop // If we are the most focused view, we need to set the focused view in Application.Navigation if (newHasFocus && focusedView?.Focused is null) { - Application.Navigation?.SetFocused (focusedView); + App?.Navigation?.SetFocused (focusedView); } // Call the virtual method diff --git a/Terminal.Gui/ViewBase/View.ScrollBars.cs b/Terminal.Gui/ViewBase/View.ScrollBars.cs index 4463299f9..fd5f6a327 100644 --- a/Terminal.Gui/ViewBase/View.ScrollBars.cs +++ b/Terminal.Gui/ViewBase/View.ScrollBars.cs @@ -1,5 +1,4 @@ -#nullable enable - + namespace Terminal.Gui.ViewBase; public partial class View diff --git a/Terminal.Gui/ViewBase/View.Text.cs b/Terminal.Gui/ViewBase/View.Text.cs index 32694d33f..b5b401f81 100644 --- a/Terminal.Gui/ViewBase/View.Text.cs +++ b/Terminal.Gui/ViewBase/View.Text.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.ViewBase; diff --git a/Terminal.Gui/ViewBase/View.cs b/Terminal.Gui/ViewBase/View.cs index 5aa8a6e1d..68325dfe9 100644 --- a/Terminal.Gui/ViewBase/View.cs +++ b/Terminal.Gui/ViewBase/View.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.ComponentModel; using System.Diagnostics; @@ -52,7 +51,7 @@ public partial class View : IDisposable, ISupportInitializeNotification /// Pretty prints the View /// - public override string ToString () { return $"{GetType ().Name}({Id}){Frame}"; } + public override string ToString () => $"{GetType ().Name}({Id}){Frame}"; /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// @@ -72,9 +71,9 @@ public partial class View : IDisposable, ISupportInitializeNotification DisposeAdornments (); DisposeScrollBars (); - if (Application.Mouse.MouseGrabView == this) + if (App?.Mouse.MouseGrabView == this) { - Application.Mouse.UngrabMouse (); + App.Mouse.UngrabMouse (); } for (int i = InternalSubViews.Count - 1; i >= 0; i--) @@ -109,6 +108,34 @@ public partial class View : IDisposable, ISupportInitializeNotification /// The id should be unique across all Views that share a SuperView. public string Id { get; set; } = ""; + private IApplication? _app; + + /// + /// Gets the instance this view is running in. If this view is at the top of the view + /// hierarchy, returns . + /// + /// + /// + /// If not explicitly set on an instance, this property will retrieve the value from the view at the top + /// of the View hierarchy (the top-most SuperView). + /// + /// + public IApplication? App + { + get => GetApp (); + internal set => _app = value; + } + + /// + /// Gets the instance this view is running in. Used internally to allow overrides by + /// . + /// + /// + /// If this view is at the top of the view hierarchy, and was not explicitly set, + /// returns . + /// + protected virtual IApplication? GetApp () => _app ?? SuperView?.App ?? null; + private IDriver? _driver; /// @@ -118,19 +145,21 @@ public partial class View : IDisposable, ISupportInitializeNotification /// internal IDriver? Driver { - get - { - if (_driver is { }) - { - return _driver; - } - - return Application.Driver; - } + get => GetDriver (); set => _driver = value; } - /// Gets the screen buffer contents. This is a convenience property for Views that need direct access to the screen buffer. + /// + /// Gets the instance for this view. Used internally to allow overrides by + /// . + /// + /// If this view is at the top of the view hierarchy, returns . + protected virtual IDriver? GetDriver () => _driver ?? App?.Driver ?? SuperView?.Driver /*?? ApplicationImpl.Instance.Driver*/; + + /// + /// Gets the screen buffer contents. This is a convenience property for Views that need direct access to the + /// screen buffer. + /// protected Cell [,]? ScreenContents => Driver?.Contents; /// Initializes a new instance of . @@ -387,7 +416,7 @@ public partial class View : IDisposable, ISupportInitializeNotification } /// Called when is changing. Can be cancelled by returning . - protected virtual bool OnVisibleChanging () { return false; } + protected virtual bool OnVisibleChanging () => false; /// /// Raised when the value is being changed. Can be cancelled by setting Cancel to diff --git a/Terminal.Gui/ViewBase/ViewDiagnosticFlags.cs b/Terminal.Gui/ViewBase/ViewDiagnosticFlags.cs index 345ec3c78..94959a874 100644 --- a/Terminal.Gui/ViewBase/ViewDiagnosticFlags.cs +++ b/Terminal.Gui/ViewBase/ViewDiagnosticFlags.cs @@ -1,5 +1,4 @@ -#nullable enable -namespace Terminal.Gui.ViewBase; +namespace Terminal.Gui.ViewBase; /// Enables diagnostic functions for . [Flags] diff --git a/Terminal.Gui/ViewBase/ViewEventArgs.cs b/Terminal.Gui/ViewBase/ViewEventArgs.cs index d2de59ec0..e5cc5e36c 100644 --- a/Terminal.Gui/ViewBase/ViewEventArgs.cs +++ b/Terminal.Gui/ViewBase/ViewEventArgs.cs @@ -13,4 +13,4 @@ public class ViewEventArgs : EventArgs /// child then sender may be the parent while is the child being added. /// public View View { get; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/Autocomplete/AppendAutocomplete.cs b/Terminal.Gui/Views/Autocomplete/AppendAutocomplete.cs index b53971624..f8452862e 100644 --- a/Terminal.Gui/Views/Autocomplete/AppendAutocomplete.cs +++ b/Terminal.Gui/Views/Autocomplete/AppendAutocomplete.cs @@ -1,3 +1,4 @@ +#nullable disable  namespace Terminal.Gui.Views; @@ -8,12 +9,12 @@ namespace Terminal.Gui.Views; public class AppendAutocomplete : AutocompleteBase { private bool _suspendSuggestions; - private TextField textField; + private TextField _textField; /// Creates a new instance of the class. public AppendAutocomplete (TextField textField) { - this.textField = textField; + _textField = textField; base.SelectionKey = KeyCode.Tab; Scheme = new Scheme @@ -35,15 +36,15 @@ public class AppendAutocomplete : AutocompleteBase /// public override View HostControl { - get => textField; - set => textField = (TextField)value; + get => _textField; + set => _textField = (TextField)value; } /// public override void ClearSuggestions () { base.ClearSuggestions (); - textField.SetNeedsDraw (); + _textField.SetNeedsDraw (); } /// @@ -107,19 +108,19 @@ public class AppendAutocomplete : AutocompleteBase } // draw it like it's selected, even though it's not - textField.SetAttribute ( + _textField.SetAttribute ( new Attribute ( Scheme.Normal.Foreground, - textField.GetAttributeForRole(VisualRole.Focus).Background, + _textField.GetAttributeForRole(VisualRole.Focus).Background, Scheme.Normal.Style ) ); - textField.Move (textField.Text.Length, 0); + _textField.Move (_textField.Text.Length, 0); Suggestion suggestion = Suggestions.ElementAt (SelectedIdx); string fragment = suggestion.Replacement.Substring (suggestion.Remove); - int spaceAvailable = textField.Viewport.Width - textField.Text.GetColumns (); + int spaceAvailable = _textField.Viewport.Width - _textField.Text.GetColumns (); int spaceRequired = fragment.EnumerateRunes ().Sum (c => c.GetColumns ()); if (spaceAvailable < spaceRequired) @@ -130,7 +131,7 @@ public class AppendAutocomplete : AutocompleteBase ); } - Application.Driver?.AddStr (fragment); + _textField.Driver?.AddStr (fragment); } /// @@ -143,12 +144,12 @@ public class AppendAutocomplete : AutocompleteBase if (MakingSuggestion ()) { Suggestion insert = Suggestions.ElementAt (SelectedIdx); - string newText = textField.Text; + string newText = _textField.Text; newText = newText.Substring (0, newText.Length - insert.Remove); newText += insert.Replacement; - textField.Text = newText; + _textField.Text = newText; - textField.MoveEnd (); + _textField.MoveEnd (); ClearSuggestions (); @@ -167,8 +168,8 @@ public class AppendAutocomplete : AutocompleteBase newText += Path.DirectorySeparatorChar; } - textField.Text = newText; - textField.MoveEnd (); + _textField.Text = newText; + _textField.MoveEnd (); } private bool CycleSuggestion (int direction) @@ -185,7 +186,7 @@ public class AppendAutocomplete : AutocompleteBase SelectedIdx = Suggestions.Count () - 1; } - textField.SetNeedsDraw (); + _textField.SetNeedsDraw (); return true; } @@ -195,5 +196,5 @@ public class AppendAutocomplete : AutocompleteBase /// to see auto-complete (i.e. focused and cursor in right place). /// /// - private bool MakingSuggestion () { return Suggestions.Any () && SelectedIdx != -1 && textField.HasFocus && textField.CursorIsAtEnd (); } + private bool MakingSuggestion () { return Suggestions.Any () && SelectedIdx != -1 && _textField.HasFocus && _textField.CursorIsAtEnd (); } } diff --git a/Terminal.Gui/Views/Autocomplete/AutocompleteBase.cs b/Terminal.Gui/Views/Autocomplete/AutocompleteBase.cs index f2122fa4b..d8abc050f 100644 --- a/Terminal.Gui/Views/Autocomplete/AutocompleteBase.cs +++ b/Terminal.Gui/Views/Autocomplete/AutocompleteBase.cs @@ -1,3 +1,4 @@ +#nullable disable using System.Collections.ObjectModel; namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/Autocomplete/AutocompleteContext.cs b/Terminal.Gui/Views/Autocomplete/AutocompleteContext.cs index d2e7e4d59..bd9f32712 100644 --- a/Terminal.Gui/Views/Autocomplete/AutocompleteContext.cs +++ b/Terminal.Gui/Views/Autocomplete/AutocompleteContext.cs @@ -1,4 +1,5 @@ +#nullable disable namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/Autocomplete/AutocompleteFilepathContext.cs b/Terminal.Gui/Views/Autocomplete/AutocompleteFilepathContext.cs index 6c3e12c34..80481a787 100644 --- a/Terminal.Gui/Views/Autocomplete/AutocompleteFilepathContext.cs +++ b/Terminal.Gui/Views/Autocomplete/AutocompleteFilepathContext.cs @@ -1,3 +1,4 @@ +#nullable disable using System.IO.Abstractions; using System.Runtime.InteropServices; diff --git a/Terminal.Gui/Views/Autocomplete/IAutocomplete.cs b/Terminal.Gui/Views/Autocomplete/IAutocomplete.cs index 717122e9d..ce15a641e 100644 --- a/Terminal.Gui/Views/Autocomplete/IAutocomplete.cs +++ b/Terminal.Gui/Views/Autocomplete/IAutocomplete.cs @@ -1,3 +1,4 @@ +#nullable disable using System.Collections.ObjectModel; namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/Autocomplete/ISuggestionGenerator.cs b/Terminal.Gui/Views/Autocomplete/ISuggestionGenerator.cs index 08fd17c9a..e8eedd2ee 100644 --- a/Terminal.Gui/Views/Autocomplete/ISuggestionGenerator.cs +++ b/Terminal.Gui/Views/Autocomplete/ISuggestionGenerator.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Generates autocomplete based on a given cursor location within a string diff --git a/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.PopUp.cs b/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.PopUp.cs index b7a67726e..18bd89b52 100644 --- a/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.PopUp.cs +++ b/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.PopUp.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs b/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs index 602a849d7..87f39fc2b 100644 --- a/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs +++ b/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs @@ -1,5 +1,4 @@ -using System.Diagnostics; - +#nullable disable namespace Terminal.Gui.Views; /// @@ -125,7 +124,7 @@ public abstract partial class PopupAutocomplete : AutocompleteBase { Visible = true; HostControl?.SetNeedsDraw (); - Application.Mouse.UngrabMouse (); + HostControl?.App?.Mouse.UngrabMouse (); return false; } @@ -137,7 +136,7 @@ public abstract partial class PopupAutocomplete : AutocompleteBase _closed = false; } - HostControl?.SetNeedsDraw (); + HostControl.SetNeedsDraw (); return false; } @@ -406,7 +405,7 @@ public abstract partial class PopupAutocomplete : AutocompleteBase string text = TextFormatter.ClipOrPad (toRender [i].Title, width); - Application.Driver?.AddStr (text); + _popup.App?.Driver?.AddStr (text); } } @@ -544,7 +543,6 @@ public abstract partial class PopupAutocomplete : AutocompleteBase /// protected abstract void SetCursorPosition (int column); -#nullable enable private Point? LastPopupPos { get; set; } #nullable restore diff --git a/Terminal.Gui/Views/Autocomplete/SingleWordSuggestionGenerator.cs b/Terminal.Gui/Views/Autocomplete/SingleWordSuggestionGenerator.cs index 1accda922..59bde624b 100644 --- a/Terminal.Gui/Views/Autocomplete/SingleWordSuggestionGenerator.cs +++ b/Terminal.Gui/Views/Autocomplete/SingleWordSuggestionGenerator.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/Autocomplete/Suggestion.cs b/Terminal.Gui/Views/Autocomplete/Suggestion.cs index 7b7bbd12a..a1facdc38 100644 --- a/Terminal.Gui/Views/Autocomplete/Suggestion.cs +++ b/Terminal.Gui/Views/Autocomplete/Suggestion.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// A replacement suggestion made by diff --git a/Terminal.Gui/Views/Bar.cs b/Terminal.Gui/Views/Bar.cs index 55e4a7914..3c04a16dc 100644 --- a/Terminal.Gui/Views/Bar.cs +++ b/Terminal.Gui/Views/Bar.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace Terminal.Gui.Views; /// @@ -33,13 +31,14 @@ public class Bar : View, IOrientation, IDesignable // Initialized += Bar_Initialized; MouseEvent += OnMouseEvent; - - if (shortcuts is { }) + if (shortcuts is null) { - foreach (View shortcut in shortcuts) - { - Add (shortcut); - } + return; + } + + foreach (View shortcut in shortcuts) + { + base.Add (shortcut); } } @@ -107,7 +106,7 @@ public class Bar : View, IOrientation, IDesignable public void OnOrientationChanged (Orientation newOrientation) { // BUGBUG: this should not be SuperView.GetContentSize - LayoutBarItems (SuperView?.GetContentSize () ?? Application.Screen.Size); + LayoutBarItems (SuperView?.GetContentSize () ?? App?.Screen.Size ?? Size.Empty); } #endregion @@ -245,7 +244,7 @@ public class Bar : View, IOrientation, IDesignable barItem.X = 0; scBarItem.MinimumKeyTextSize = minKeyWidth; scBarItem.Width = scBarItem.GetWidthDimAuto (); - barItem.Layout (Application.Screen.Size); + barItem.Layout (App?.Screen.Size ?? Size.Empty); maxBarItemWidth = Math.Max (maxBarItemWidth, barItem.Frame.Width); } diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index 942e97b11..626af1852 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -1,4 +1,5 @@ +#nullable disable namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/CharMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs index 02308ac1e..bf54252cf 100644 --- a/Terminal.Gui/Views/CharMap/CharMap.cs +++ b/Terminal.Gui/Views/CharMap/CharMap.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -384,12 +383,12 @@ public class CharMap : View, IDesignable try { decResponse = await client.GetCodepointDec (SelectedCodePoint).ConfigureAwait (false); - Application.Invoke (() => waitIndicator.RequestStop ()); + Application.Invoke ((_) => waitIndicator.RequestStop ()); } catch (HttpRequestException e) { getCodePointError = errorLabel.Text = e.Message; - Application.Invoke (() => waitIndicator.RequestStop ()); + Application.Invoke ((_) => waitIndicator.RequestStop ()); } }; Application.Run (waitIndicator); @@ -972,7 +971,7 @@ public class CharMap : View, IDesignable // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused // and the context menu is disposed when it is closed. - Application.Popover?.Register (contextMenu); + App!.Popover?.Register (contextMenu); contextMenu?.MakeVisible (ViewportToScreen (GetCursor (SelectedCodePoint))); diff --git a/Terminal.Gui/Views/CharMap/UcdApiClient.cs b/Terminal.Gui/Views/CharMap/UcdApiClient.cs index ae0af4838..a5abaf994 100644 --- a/Terminal.Gui/Views/CharMap/UcdApiClient.cs +++ b/Terminal.Gui/Views/CharMap/UcdApiClient.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/CharMap/UnicodeRange.cs b/Terminal.Gui/Views/CharMap/UnicodeRange.cs index 24f7378b2..4904d1751 100644 --- a/Terminal.Gui/Views/CharMap/UnicodeRange.cs +++ b/Terminal.Gui/Views/CharMap/UnicodeRange.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Reflection; using System.Text.Unicode; diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index d5801f9f8..2a0535469 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/CheckState.cs b/Terminal.Gui/Views/CheckState.cs index 2b001ddea..494df666b 100644 --- a/Terminal.Gui/Views/CheckState.cs +++ b/Terminal.Gui/Views/CheckState.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/CollectionNavigation/CollectionNavigator.cs b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigator.cs index a12d769c5..c0eb7a312 100644 --- a/Terminal.Gui/Views/CollectionNavigation/CollectionNavigator.cs +++ b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigator.cs @@ -1,3 +1,4 @@ +#nullable disable using System.Collections; namespace Terminal.Gui.Views; @@ -21,4 +22,4 @@ internal class CollectionNavigator : CollectionNavigatorBase, IListCollectionNav /// protected override int GetCollectionLength () { return Collection.Count; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs index 274d32622..39234b10b 100644 --- a/Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs +++ b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; @@ -202,4 +202,4 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator SearchString = ""; _lastKeystroke = DateTime.Now; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/CollectionNavigation/DefaultCollectionNavigatorMatcher.cs b/Terminal.Gui/Views/CollectionNavigation/DefaultCollectionNavigatorMatcher.cs index 20bee6809..ed910a828 100644 --- a/Terminal.Gui/Views/CollectionNavigation/DefaultCollectionNavigatorMatcher.cs +++ b/Terminal.Gui/Views/CollectionNavigation/DefaultCollectionNavigatorMatcher.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigator.cs b/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigator.cs index 85a68d300..5ea621646 100644 --- a/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigator.cs +++ b/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigator.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigatorMatcher.cs b/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigatorMatcher.cs index f45b59c0f..420c49674 100644 --- a/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigatorMatcher.cs +++ b/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigatorMatcher.cs @@ -1,3 +1,4 @@ +#nullable disable  namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/CollectionNavigation/IListCollectionNavigator.cs b/Terminal.Gui/Views/CollectionNavigation/IListCollectionNavigator.cs index f89e3f7c4..b382bc627 100644 --- a/Terminal.Gui/Views/CollectionNavigation/IListCollectionNavigator.cs +++ b/Terminal.Gui/Views/CollectionNavigation/IListCollectionNavigator.cs @@ -1,3 +1,4 @@ +#nullable disable using System.Collections; namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/CollectionNavigation/TableCollectionNavigator.cs b/Terminal.Gui/Views/CollectionNavigation/TableCollectionNavigator.cs index 69a817e50..21e3ce7d1 100644 --- a/Terminal.Gui/Views/CollectionNavigation/TableCollectionNavigator.cs +++ b/Terminal.Gui/Views/CollectionNavigation/TableCollectionNavigator.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Collection navigator for cycling selections in a . diff --git a/Terminal.Gui/Views/Color/BBar.cs b/Terminal.Gui/Views/Color/BBar.cs index b59b5eecb..11c5cc9df 100644 --- a/Terminal.Gui/Views/Color/BBar.cs +++ b/Terminal.Gui/Views/Color/BBar.cs @@ -1,4 +1,4 @@ -#nullable enable + using ColorHelper; diff --git a/Terminal.Gui/Views/Color/ColorBar.cs b/Terminal.Gui/Views/Color/ColorBar.cs index ad15e231f..2be624be5 100644 --- a/Terminal.Gui/Views/Color/ColorBar.cs +++ b/Terminal.Gui/Views/Color/ColorBar.cs @@ -1,4 +1,4 @@ -#nullable enable + using ColorHelper; diff --git a/Terminal.Gui/Views/Color/ColorModelStrategy.cs b/Terminal.Gui/Views/Color/ColorModelStrategy.cs index eb3f15608..0f803be18 100644 --- a/Terminal.Gui/Views/Color/ColorModelStrategy.cs +++ b/Terminal.Gui/Views/Color/ColorModelStrategy.cs @@ -1,4 +1,4 @@ -#nullable enable + using ColorHelper; using ColorConverter = ColorHelper.ColorConverter; diff --git a/Terminal.Gui/Views/Color/ColorPicker.16.cs b/Terminal.Gui/Views/Color/ColorPicker.16.cs index 8c76f2648..6c1eda4cb 100644 --- a/Terminal.Gui/Views/Color/ColorPicker.16.cs +++ b/Terminal.Gui/Views/Color/ColorPicker.16.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs b/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs index e6c29172c..400040e63 100644 --- a/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs +++ b/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs @@ -1,3 +1,4 @@ +#nullable disable  namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/Color/ColorPicker.Style.cs b/Terminal.Gui/Views/Color/ColorPicker.Style.cs index 616687156..afab89d5a 100644 --- a/Terminal.Gui/Views/Color/ColorPicker.Style.cs +++ b/Terminal.Gui/Views/Color/ColorPicker.Style.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/Color/ColorPicker.cs b/Terminal.Gui/Views/Color/ColorPicker.cs index 2a60e536b..f0cdc76b9 100644 --- a/Terminal.Gui/Views/Color/ColorPicker.cs +++ b/Terminal.Gui/Views/Color/ColorPicker.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/Color/GBar.cs b/Terminal.Gui/Views/Color/GBar.cs index ac9a7227f..b9dd5b435 100644 --- a/Terminal.Gui/Views/Color/GBar.cs +++ b/Terminal.Gui/Views/Color/GBar.cs @@ -1,4 +1,4 @@ -#nullable enable + using ColorHelper; diff --git a/Terminal.Gui/Views/Color/HueBar.cs b/Terminal.Gui/Views/Color/HueBar.cs index 9f7d29e45..a0b0b554d 100644 --- a/Terminal.Gui/Views/Color/HueBar.cs +++ b/Terminal.Gui/Views/Color/HueBar.cs @@ -1,4 +1,4 @@ -#nullable enable + using ColorHelper; using ColorConverter = ColorHelper.ColorConverter; diff --git a/Terminal.Gui/Views/Color/IColorBar.cs b/Terminal.Gui/Views/Color/IColorBar.cs index b8139b8d5..176edead9 100644 --- a/Terminal.Gui/Views/Color/IColorBar.cs +++ b/Terminal.Gui/Views/Color/IColorBar.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; internal interface IColorBar diff --git a/Terminal.Gui/Views/Color/LightnessBar.cs b/Terminal.Gui/Views/Color/LightnessBar.cs index 0f176a3f6..f3d1e3942 100644 --- a/Terminal.Gui/Views/Color/LightnessBar.cs +++ b/Terminal.Gui/Views/Color/LightnessBar.cs @@ -1,4 +1,4 @@ -#nullable enable + using ColorHelper; using ColorConverter = ColorHelper.ColorConverter; diff --git a/Terminal.Gui/Views/Color/RBar.cs b/Terminal.Gui/Views/Color/RBar.cs index 2610c66bb..e71dd9246 100644 --- a/Terminal.Gui/Views/Color/RBar.cs +++ b/Terminal.Gui/Views/Color/RBar.cs @@ -1,4 +1,4 @@ -#nullable enable + using ColorHelper; diff --git a/Terminal.Gui/Views/Color/SaturationBar.cs b/Terminal.Gui/Views/Color/SaturationBar.cs index 76fcd2029..9be7ab7f7 100644 --- a/Terminal.Gui/Views/Color/SaturationBar.cs +++ b/Terminal.Gui/Views/Color/SaturationBar.cs @@ -1,4 +1,4 @@ -#nullable enable + using ColorHelper; using ColorConverter = ColorHelper.ColorConverter; diff --git a/Terminal.Gui/Views/Color/ValueBar.cs b/Terminal.Gui/Views/Color/ValueBar.cs index 6352c7cab..5673ce8db 100644 --- a/Terminal.Gui/Views/Color/ValueBar.cs +++ b/Terminal.Gui/Views/Color/ValueBar.cs @@ -1,4 +1,4 @@ -#nullable enable + using ColorHelper; using ColorConverter = ColorHelper.ColorConverter; diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index 6da8acaea..86c4f4f79 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -1,3 +1,4 @@ +#nullable disable // // ComboBox.cs: ComboBox control // diff --git a/Terminal.Gui/Views/DatePicker.cs b/Terminal.Gui/Views/DatePicker.cs index a4f2791af..579607d3d 100644 --- a/Terminal.Gui/Views/DatePicker.cs +++ b/Terminal.Gui/Views/DatePicker.cs @@ -1,4 +1,4 @@ -#nullable enable + // // DatePicker.cs: DatePicker control diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index bf5379f36..4037d7312 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; /// @@ -8,7 +8,7 @@ namespace Terminal.Gui.Views; /// /// /// To run the modally, create the , and pass it to -/// . This will execute the dialog until +/// . This will execute the dialog until /// it terminates via the (`Esc` by default), /// or when one of the views or buttons added to the dialog calls /// . @@ -21,7 +21,7 @@ public class Dialog : Window /// /// By default, , , , and are /// set - /// such that the will be centered in, and no larger than 90% of , if + /// such that the will be centered in, and no larger than 90% of , if /// there is one. Otherwise, /// it will be bound by the screen dimensions. /// diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index 0519ecba6..644cc55d9 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -1 +1,2 @@ - \ No newline at end of file +#nullable disable + diff --git a/Terminal.Gui/Views/FileDialogs/AllowedType.cs b/Terminal.Gui/Views/FileDialogs/AllowedType.cs index 8460edadf..2bcdfbe4a 100644 --- a/Terminal.Gui/Views/FileDialogs/AllowedType.cs +++ b/Terminal.Gui/Views/FileDialogs/AllowedType.cs @@ -1,3 +1,4 @@ +#nullable disable  namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/FileDialogs/DefaultFileOperations.cs b/Terminal.Gui/Views/FileDialogs/DefaultFileOperations.cs index 5f0aefcad..467e8d74c 100644 --- a/Terminal.Gui/Views/FileDialogs/DefaultFileOperations.cs +++ b/Terminal.Gui/Views/FileDialogs/DefaultFileOperations.cs @@ -1,3 +1,4 @@ +#nullable disable using System.IO.Abstractions; namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.cs b/Terminal.Gui/Views/FileDialogs/FileDialog.cs index 1d2a3533f..4650730aa 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialog.cs @@ -1,4 +1,3 @@ -#nullable enable using System.IO.Abstractions; using System.Text.RegularExpressions; @@ -827,7 +826,7 @@ public class FileDialog : Dialog, IDesignable return _tableView.GetScheme (); } - Color color = Style.ColorProvider.GetColor (stats.FileSystemInfo) ?? new Color (Color.White); + Color color = Style.ColorProvider.GetColor (stats.FileSystemInfo!) ?? new Color (Color.White); var black = new Color (Color.Black); // TODO: Add some kind of cache for this @@ -1034,7 +1033,7 @@ public class FileDialog : Dialog, IDesignable private void New () { { - IFileSystemInfo created = FileOperationsHandler.New (_fileSystem, State!.Directory); + IFileSystemInfo created = FileOperationsHandler.New (_fileSystem!, State!.Directory); if (created is { }) { @@ -1175,7 +1174,7 @@ public class FileDialog : Dialog, IDesignable if (toRename?.Length == 1) { - IFileSystemInfo newNamed = FileOperationsHandler.Rename (_fileSystem, toRename.Single ()); + IFileSystemInfo newNamed = FileOperationsHandler.Rename (_fileSystem!, toRename.Single ()); if (newNamed is { }) { @@ -1233,7 +1232,7 @@ public class FileDialog : Dialog, IDesignable // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused // and the context menu is disposed when it is closed. - Application.Popover?.Register (contextMenu); + App!.Popover?.Register (contextMenu); contextMenu?.MakeVisible (e.ScreenPosition); } @@ -1261,7 +1260,7 @@ public class FileDialog : Dialog, IDesignable // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused // and the context menu is disposed when it is closed. - Application.Popover?.Register (contextMenu); + App!.Popover?.Register (contextMenu); contextMenu?.MakeVisible (e.ScreenPosition); } @@ -1569,7 +1568,7 @@ public class FileDialog : Dialog, IDesignable } } - if (Parent.SearchMatcher.IsMatch (f.FileSystemInfo)) + if (Parent.SearchMatcher.IsMatch (f.FileSystemInfo!)) { lock (_oLockFound) { @@ -1612,7 +1611,7 @@ public class FileDialog : Dialog, IDesignable UpdateChildrenToFound (); } - Application.Invoke (() => { Parent._spinnerView.Visible = false; }); + Application.Invoke ((_) => { Parent._spinnerView.Visible = false; }); } } @@ -1624,7 +1623,7 @@ public class FileDialog : Dialog, IDesignable } Application.Invoke ( - () => + (_) => { Parent._tbPath.Autocomplete.GenerateSuggestions ( new AutocompleteFilepathContext ( diff --git a/Terminal.Gui/Views/FileDialogs/FileDialogCollectionNavigator.cs b/Terminal.Gui/Views/FileDialogs/FileDialogCollectionNavigator.cs index d482c0a89..9e0590b83 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialogCollectionNavigator.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialogCollectionNavigator.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; internal class FileDialogCollectionNavigator (FileDialog fileDialog, TableView tableView) : CollectionNavigatorBase diff --git a/Terminal.Gui/Views/FileDialogs/FileDialogHistory.cs b/Terminal.Gui/Views/FileDialogs/FileDialogHistory.cs index 6558d40d9..535a19777 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialogHistory.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialogHistory.cs @@ -1,3 +1,4 @@ +#nullable disable using System.IO.Abstractions; namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/FileDialogs/FileDialogState.cs b/Terminal.Gui/Views/FileDialogs/FileDialogState.cs index 26cde6a62..aec681817 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialogState.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialogState.cs @@ -1,3 +1,4 @@ +#nullable disable using System.IO.Abstractions; namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/FileDialogs/FileDialogStyle.cs b/Terminal.Gui/Views/FileDialogs/FileDialogStyle.cs index 0ee7ac5aa..c72f24d8a 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialogStyle.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialogStyle.cs @@ -1,3 +1,4 @@ +#nullable disable using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO.Abstractions; diff --git a/Terminal.Gui/Views/FileDialogs/FileDialogTableSource.cs b/Terminal.Gui/Views/FileDialogs/FileDialogTableSource.cs index 14d37f097..bf15d3bac 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialogTableSource.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialogTableSource.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; internal class FileDialogTableSource ( diff --git a/Terminal.Gui/Views/FileDialogs/FilesSelectedEventArgs.cs b/Terminal.Gui/Views/FileDialogs/FilesSelectedEventArgs.cs index 56ba5f1fe..f8629e31a 100644 --- a/Terminal.Gui/Views/FileDialogs/FilesSelectedEventArgs.cs +++ b/Terminal.Gui/Views/FileDialogs/FilesSelectedEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable  namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/FileDialogs/OpenDialog.cs b/Terminal.Gui/Views/FileDialogs/OpenDialog.cs index 5f916b1de..0940436d2 100644 --- a/Terminal.Gui/Views/FileDialogs/OpenDialog.cs +++ b/Terminal.Gui/Views/FileDialogs/OpenDialog.cs @@ -1,3 +1,4 @@ +#nullable disable // // FileDialog.cs: File system dialogs for open and save // @@ -22,7 +23,7 @@ namespace Terminal.Gui.Views; /// /// /// To use, create an instance of , and pass it to -/// . This will run the dialog modally, and when this returns, +/// . This will run the dialog modally, and when this returns, /// the list of files will be available on the property. /// /// To select more than one file, users can use the spacebar, or control-t. diff --git a/Terminal.Gui/Views/FileDialogs/OpenMode.cs b/Terminal.Gui/Views/FileDialogs/OpenMode.cs index e50370ea2..e3e62d60f 100644 --- a/Terminal.Gui/Views/FileDialogs/OpenMode.cs +++ b/Terminal.Gui/Views/FileDialogs/OpenMode.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Determine which type to open. diff --git a/Terminal.Gui/Views/FileDialogs/SaveDialog.cs b/Terminal.Gui/Views/FileDialogs/SaveDialog.cs index 7646817f8..6e0da27e2 100644 --- a/Terminal.Gui/Views/FileDialogs/SaveDialog.cs +++ b/Terminal.Gui/Views/FileDialogs/SaveDialog.cs @@ -1,3 +1,4 @@ +#nullable disable // // FileDialog.cs: File system dialogs for open and save // @@ -17,7 +18,7 @@ namespace Terminal.Gui.Views; /// /// /// To use, create an instance of , and pass it to -/// . This will run the dialog modally, and when this returns, +/// . This will run the dialog modally, and when this returns, /// the property will contain the selected file name or null if the user canceled. /// /// diff --git a/Terminal.Gui/Views/FrameView.cs b/Terminal.Gui/Views/FrameView.cs index 4f8713c8b..cd99731b9 100644 --- a/Terminal.Gui/Views/FrameView.cs +++ b/Terminal.Gui/Views/FrameView.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/GraphView/Axis.cs b/Terminal.Gui/Views/GraphView/Axis.cs index 3160f3ee3..177500f9f 100644 --- a/Terminal.Gui/Views/GraphView/Axis.cs +++ b/Terminal.Gui/Views/GraphView/Axis.cs @@ -1,3 +1,4 @@ +#nullable disable  namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/GraphView/BarSeriesBar.cs b/Terminal.Gui/Views/GraphView/BarSeriesBar.cs index efe17b59c..29f401e85 100644 --- a/Terminal.Gui/Views/GraphView/BarSeriesBar.cs +++ b/Terminal.Gui/Views/GraphView/BarSeriesBar.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// A single bar in a diff --git a/Terminal.Gui/Views/GraphView/GraphCellToRender.cs b/Terminal.Gui/Views/GraphView/GraphCellToRender.cs index 8bf4d99e9..db1b56e5c 100644 --- a/Terminal.Gui/Views/GraphView/GraphCellToRender.cs +++ b/Terminal.Gui/Views/GraphView/GraphCellToRender.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/GraphView/GraphView.cs b/Terminal.Gui/Views/GraphView/GraphView.cs index 92b31a60f..69f708da8 100644 --- a/Terminal.Gui/Views/GraphView/GraphView.cs +++ b/Terminal.Gui/Views/GraphView/GraphView.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; @@ -8,6 +8,7 @@ public class GraphView : View, IDesignable /// Creates a new graph with a 1 to 1 graph space with absolute layout. public GraphView () { + App = ApplicationImpl.Instance; CanFocus = true; AxisX = new (); @@ -344,7 +345,7 @@ public class GraphView : View, IDesignable } /// - /// Sets the color attribute of to the (if defined) or + /// Sets the color attribute of to the (if defined) or /// otherwise. /// public void SetDriverColorToGraphColor () { SetAttribute (GraphColor ?? GetAttributeForRole (VisualRole.Normal)); } diff --git a/Terminal.Gui/Views/GraphView/IAnnotation.cs b/Terminal.Gui/Views/GraphView/IAnnotation.cs index 004339875..c396df59d 100644 --- a/Terminal.Gui/Views/GraphView/IAnnotation.cs +++ b/Terminal.Gui/Views/GraphView/IAnnotation.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/GraphView/LegendAnnotation.cs b/Terminal.Gui/Views/GraphView/LegendAnnotation.cs index 813228f7d..d8ac4be8b 100644 --- a/Terminal.Gui/Views/GraphView/LegendAnnotation.cs +++ b/Terminal.Gui/Views/GraphView/LegendAnnotation.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/GraphView/LineF.cs b/Terminal.Gui/Views/GraphView/LineF.cs index 67ef51cd1..462b03224 100644 --- a/Terminal.Gui/Views/GraphView/LineF.cs +++ b/Terminal.Gui/Views/GraphView/LineF.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Describes two points in graph space and a line between them diff --git a/Terminal.Gui/Views/GraphView/PathAnnotation.cs b/Terminal.Gui/Views/GraphView/PathAnnotation.cs index 4a623f7df..622ea83a8 100644 --- a/Terminal.Gui/Views/GraphView/PathAnnotation.cs +++ b/Terminal.Gui/Views/GraphView/PathAnnotation.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Sequence of lines to connect points e.g. of a diff --git a/Terminal.Gui/Views/GraphView/Series.cs b/Terminal.Gui/Views/GraphView/Series.cs index 187d6e6be..d417404ba 100644 --- a/Terminal.Gui/Views/GraphView/Series.cs +++ b/Terminal.Gui/Views/GraphView/Series.cs @@ -1,7 +1,6 @@ using System.Collections.ObjectModel; namespace Terminal.Gui.Views; -#nullable enable /// Describes a series of data that can be rendered into a > public interface ISeries { diff --git a/Terminal.Gui/Views/GraphView/TextAnnotation.cs b/Terminal.Gui/Views/GraphView/TextAnnotation.cs index 98f1fec3e..d1309fe9e 100644 --- a/Terminal.Gui/Views/GraphView/TextAnnotation.cs +++ b/Terminal.Gui/Views/GraphView/TextAnnotation.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Displays text at a given position (in screen space or graph space) diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs index cf645b03d..ee81eb832 100644 --- a/Terminal.Gui/Views/HexView.cs +++ b/Terminal.Gui/Views/HexView.cs @@ -1,4 +1,4 @@ -#nullable enable + // // HexView.cs: A hexadecimal viewer diff --git a/Terminal.Gui/Views/HexViewEventArgs.cs b/Terminal.Gui/Views/HexViewEventArgs.cs index 11e372115..2ee496b17 100644 --- a/Terminal.Gui/Views/HexViewEventArgs.cs +++ b/Terminal.Gui/Views/HexViewEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable // // HexView.cs: A hexadecimal viewer // diff --git a/Terminal.Gui/Views/IListDataSource.cs b/Terminal.Gui/Views/IListDataSource.cs index 37f1a63d6..76ab7e956 100644 --- a/Terminal.Gui/Views/IListDataSource.cs +++ b/Terminal.Gui/Views/IListDataSource.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable disable using System.Collections; using System.Collections.Specialized; diff --git a/Terminal.Gui/Views/Label.cs b/Terminal.Gui/Views/Label.cs index 5ea22dba9..c11347f9f 100644 --- a/Terminal.Gui/Views/Label.cs +++ b/Terminal.Gui/Views/Label.cs @@ -24,13 +24,13 @@ public class Label : View, IDesignable Width = Dim.Auto (DimAutoStyle.Text); // On HoKey, pass it to the next view - AddCommand (Command.HotKey, InvokeHotKeyOnNextPeer); + AddCommand (Command.HotKey, InvokeHotKeyOnNextPeer!); TitleChanged += Label_TitleChanged; MouseClick += Label_MouseClick; } - private void Label_MouseClick (object sender, MouseEventArgs e) + private void Label_MouseClick (object? sender, MouseEventArgs e) { if (!CanFocus) { @@ -38,7 +38,7 @@ public class Label : View, IDesignable } } - private void Label_TitleChanged (object sender, EventArgs e) + private void Label_TitleChanged (object? sender, EventArgs e) { base.Text = e.Value; TextFormatter.HotKeySpecifier = HotKeySpecifier; diff --git a/Terminal.Gui/Views/Line.cs b/Terminal.Gui/Views/Line.cs index 5b5a9c9a8..8da81b99d 100644 --- a/Terminal.Gui/Views/Line.cs +++ b/Terminal.Gui/Views/Line.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index b933af9e0..729e8066b 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -1,3 +1,4 @@ +#nullable disable using System.Collections; using System.Collections.ObjectModel; using System.Collections.Specialized; diff --git a/Terminal.Gui/Views/ListViewEventArgs.cs b/Terminal.Gui/Views/ListViewEventArgs.cs index fe83de580..7a718b536 100644 --- a/Terminal.Gui/Views/ListViewEventArgs.cs +++ b/Terminal.Gui/Views/ListViewEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// for events. diff --git a/Terminal.Gui/Views/Menu/MenuBarItemv2.cs b/Terminal.Gui/Views/Menu/MenuBarItemv2.cs index ca82d7fc3..8d04c3e2b 100644 --- a/Terminal.Gui/Views/Menu/MenuBarItemv2.cs +++ b/Terminal.Gui/Views/Menu/MenuBarItemv2.cs @@ -1,6 +1,7 @@ -#nullable enable +using System.Diagnostics; + namespace Terminal.Gui.Views; /// @@ -113,6 +114,8 @@ public class MenuBarItemv2 : MenuItemv2 if (_popoverMenu is { }) { + _popoverMenu.App = App; + PopoverMenuOpen = _popoverMenu.Visible; _popoverMenu.VisibleChanged += OnPopoverVisibleChanged; _popoverMenu.Accepted += OnPopoverMenuOnAccepted; diff --git a/Terminal.Gui/Views/Menu/MenuBarv2.cs b/Terminal.Gui/Views/Menu/MenuBarv2.cs index fdf1333a2..746cdb44d 100644 --- a/Terminal.Gui/Views/Menu/MenuBarv2.cs +++ b/Terminal.Gui/Views/Menu/MenuBarv2.cs @@ -1,4 +1,4 @@ -#nullable enable + using System.ComponentModel; using System.Diagnostics; @@ -321,7 +321,7 @@ public class MenuBarv2 : Menuv2, IDesignable // TODO: This needs to be done whenever a menuitem in any MenuBarItem changes foreach (MenuBarItemv2? mbi in SubViews.Select (s => s as MenuBarItemv2)) { - Application.Popover?.Register (mbi?.PopoverMenu); + App?.Popover?.Register (mbi?.PopoverMenu); } } @@ -396,11 +396,11 @@ public class MenuBarv2 : Menuv2, IDesignable } // If the active Application Popover is part of this MenuBar, hide it. - if (Application.Popover?.GetActivePopover () is PopoverMenu popoverMenu + if (App?.Popover?.GetActivePopover () is PopoverMenu popoverMenu && popoverMenu.Root?.SuperMenuItem?.SuperView == this) { - // Logging.Debug ($"{Title} - Calling Application.Popover?.Hide ({popoverMenu.Title})"); - Application.Popover.Hide (popoverMenu); + // Logging.Debug ($"{Title} - Calling App?.Popover?.Hide ({popoverMenu.Title})"); + App?.Popover.Hide (popoverMenu); } if (menuBarItem is null) @@ -420,7 +420,11 @@ public class MenuBarv2 : Menuv2, IDesignable } // Logging.Debug ($"{Title} - \"{menuBarItem.PopoverMenu?.Title}\".MakeVisible"); - menuBarItem.PopoverMenu?.MakeVisible (new Point (menuBarItem.FrameToScreen ().X, menuBarItem.FrameToScreen ().Bottom)); + if (menuBarItem.PopoverMenu is { }) + { + menuBarItem.PopoverMenu.App ??= App; + menuBarItem.PopoverMenu.MakeVisible (new Point (menuBarItem.FrameToScreen ().X, menuBarItem.FrameToScreen ().Bottom)); + } menuBarItem.Accepting += OnMenuItemAccepted; @@ -501,11 +505,16 @@ public class MenuBarv2 : Menuv2, IDesignable } /// - public bool EnableForDesign (ref TContext context) where TContext : notnull + public bool EnableForDesign (ref TContext targetView) where TContext : notnull { // Note: This menu is used by unit tests. If you modify it, you'll likely have to update // unit tests. + if (targetView is View target) + { + App ??= target.App; + } + Id = "DemoBar"; var bordersCb = new CheckBox @@ -552,10 +561,10 @@ public class MenuBarv2 : Menuv2, IDesignable new MenuBarItemv2 ( "_File", [ - new MenuItemv2 (context as View, Command.New), - new MenuItemv2 (context as View, Command.Open), - new MenuItemv2 (context as View, Command.Save), - new MenuItemv2 (context as View, Command.SaveAs), + new MenuItemv2 (targetView as View, Command.New), + new MenuItemv2 (targetView as View, Command.Open), + new MenuItemv2 (targetView as View, Command.Save), + new MenuItemv2 (targetView as View, Command.SaveAs), new Line (), new MenuItemv2 { @@ -576,7 +585,7 @@ public class MenuBarv2 : Menuv2, IDesignable Key = Key.W.WithCtrl, CommandView = enableOverwriteCb, Command = Command.EnableOverwrite, - TargetView = context as View + TargetView = targetView as View }, new () { @@ -622,7 +631,7 @@ public class MenuBarv2 : Menuv2, IDesignable new Line (), new MenuItemv2 { - TargetView = context as View, + TargetView = targetView as View, Key = Application.QuitKey, Command = Command.Quit } @@ -634,11 +643,11 @@ public class MenuBarv2 : Menuv2, IDesignable new MenuBarItemv2 ( "_Edit", [ - new MenuItemv2 (context as View, Command.Cut), - new MenuItemv2 (context as View, Command.Copy), - new MenuItemv2 (context as View, Command.Paste), + new MenuItemv2 (targetView as View, Command.Cut), + new MenuItemv2 (targetView as View, Command.Copy), + new MenuItemv2 (targetView as View, Command.Paste), new Line (), - new MenuItemv2 (context as View, Command.SelectAll), + new MenuItemv2 (targetView as View, Command.SelectAll), new Line (), new MenuItemv2 { diff --git a/Terminal.Gui/Views/Menu/MenuItemv2.cs b/Terminal.Gui/Views/Menu/MenuItemv2.cs index c5f472307..7533881cd 100644 --- a/Terminal.Gui/Views/Menu/MenuItemv2.cs +++ b/Terminal.Gui/Views/Menu/MenuItemv2.cs @@ -1,4 +1,3 @@ -#nullable enable using System.ComponentModel; @@ -136,7 +135,7 @@ public class MenuItemv2 : Shortcut { // Is this an Application-bound command? // Logging.Debug ($"{Title} - Application.InvokeCommandsBoundToKey ({Key})..."); - ret = Application.InvokeCommandsBoundToKey (Key); + ret = App?.Keyboard.InvokeCommandsBoundToKey (Key); } } @@ -186,6 +185,7 @@ public class MenuItemv2 : Shortcut if (_subMenu is { }) { + SubMenu!.App ??= App; SubMenu!.Visible = false; // TODO: This is a temporary hack - add a flag or something instead KeyView.Text = $"{Glyphs.RightArrow}"; diff --git a/Terminal.Gui/Views/Menu/Menuv2.cs b/Terminal.Gui/Views/Menu/Menuv2.cs index 1eb5f6cc4..2cd610f41 100644 --- a/Terminal.Gui/Views/Menu/Menuv2.cs +++ b/Terminal.Gui/Views/Menu/Menuv2.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; @@ -229,4 +229,4 @@ public class Menuv2 : Bar ConfigurationManager.Applied -= OnConfigurationManagerApplied; } } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/Menu/PopoverMenu.cs b/Terminal.Gui/Views/Menu/PopoverMenu.cs index dcaef66ea..c04041bf5 100644 --- a/Terminal.Gui/Views/Menu/PopoverMenu.cs +++ b/Terminal.Gui/Views/Menu/PopoverMenu.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; @@ -9,7 +9,7 @@ namespace Terminal.Gui.Views; /// /// /// -/// To use as a context menu, register the popover menu with and call +/// To use as a context menu, register the popover menu with and call /// . /// /// @@ -176,7 +176,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable UpdateKeyBindings (); SetPosition (idealScreenPosition); - Application.Popover?.Show (this); + App!.Popover?.Show (this); } /// @@ -188,7 +188,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable /// If , the current mouse position will be used. public void SetPosition (Point? idealScreenPosition = null) { - idealScreenPosition ??= Application.GetLastMousePosition (); + idealScreenPosition ??= App?.Mouse.LastMousePosition; if (idealScreenPosition is null || Root is null) { @@ -199,6 +199,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable if (!Root.IsInitialized) { + Root.App ??= App; Root.BeginInit (); Root.EndInit (); Root.Layout (); @@ -223,7 +224,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable else { HideAndRemoveSubMenu (_root); - Application.Popover?.Hide (this); + App?.Popover?.Hide (this); } } @@ -246,6 +247,11 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable _root = value; + if (_root is { }) + { + _root.App = App; + } + // TODO: This needs to be done whenever any MenuItem in the menu tree changes to support dynamic menus // TODO: And it needs to clear the old bindings first UpdateKeyBindings (); @@ -255,6 +261,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable foreach (Menuv2 menu in allMenus) { + menu.App = App; menu.Visible = false; menu.Accepting += MenuOnAccepting; menu.Accepted += MenuAccepted; @@ -279,7 +286,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable else { // No TargetView implies Application HotKey - key = Application.KeyBindings.GetFirstFromCommands (menuItem.Command); + key = App?.Keyboard.KeyBindings.GetFirstFromCommands (menuItem.Command); } if (key is not { IsValid: true }) @@ -439,6 +446,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable if (!menu!.IsInitialized) { + menu.App ??= App; menu.BeginInit (); menu.EndInit (); } @@ -626,27 +634,27 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable } /// - public bool EnableForDesign (ref TContext context) where TContext : notnull + public bool EnableForDesign (ref TContext targetView) where TContext : notnull { // Note: This menu is used by unit tests. If you modify it, you'll likely have to update // unit tests. Root = new ( [ - new MenuItemv2 (context as View, Command.Cut), - new MenuItemv2 (context as View, Command.Copy), - new MenuItemv2 (context as View, Command.Paste), + new MenuItemv2 (targetView as View, Command.Cut), + new MenuItemv2 (targetView as View, Command.Copy), + new MenuItemv2 (targetView as View, Command.Paste), new Line (), - new MenuItemv2 (context as View, Command.SelectAll), + new MenuItemv2 (targetView as View, Command.SelectAll), new Line (), - new MenuItemv2 (context as View, Command.Quit) + new MenuItemv2 (targetView as View, Command.Quit) ]) { Title = "Popover Demo Root" }; // NOTE: This is a workaround for the fact that the PopoverMenu is not visible in the designer - // NOTE: without being activated via Application.Popover. But we want it to be visible. + // NOTE: without being activated via App?.Popover. But we want it to be visible. // NOTE: If you use PopoverView.EnableForDesign for real Popover scenarios, change back to false // NOTE: after calling EnableForDesign. //Visible = true; diff --git a/Terminal.Gui/Views/Menuv1/Menu.cs b/Terminal.Gui/Views/Menuv1/Menu.cs index 7833e4196..f538af7dd 100644 --- a/Terminal.Gui/Views/Menuv1/Menu.cs +++ b/Terminal.Gui/Views/Menuv1/Menu.cs @@ -1,5 +1,4 @@ #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. -#nullable enable namespace Terminal.Gui.Views; @@ -13,10 +12,10 @@ internal sealed class Menu : View { public Menu () { - if (Application.Top is { }) + if (Application.Current is { }) { - Application.Top.DrawComplete += Top_DrawComplete; - Application.Top.SizeChanging += Current_TerminalResized; + Application.Current.DrawComplete += Top_DrawComplete; + Application.Current.SizeChanging += Current_TerminalResized; } Application.MouseEvent += Application_RootMouseEvent; @@ -232,10 +231,10 @@ internal sealed class Menu : View { RemoveKeyBindingsHotKey (_barItems); - if (Application.Top is { }) + if (Application.Current is { }) { - Application.Top.DrawComplete -= Top_DrawComplete; - Application.Top.SizeChanging -= Current_TerminalResized; + Application.Current.DrawComplete -= Top_DrawComplete; + Application.Current.SizeChanging -= Current_TerminalResized; } Application.MouseEvent -= Application_RootMouseEvent; @@ -968,11 +967,11 @@ internal sealed class Menu : View // The -3 is left/right border + one space (not sure what for) tf.Draw ( - ViewportToScreen (new Rectangle (1, i, Frame.Width - 3, 1)), - i == _currentChild ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal), - i == _currentChild ? GetAttributeForRole (VisualRole.HotFocus) : GetAttributeForRole (VisualRole.HotNormal), - SuperView?.ViewportToScreen (SuperView.Viewport) ?? Rectangle.Empty - ); + driver: Driver, + screen: ViewportToScreen (new Rectangle (1, i, Frame.Width - 3, 1)), + normalColor: i == _currentChild ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal), + hotColor: i == _currentChild ? GetAttributeForRole (VisualRole.HotFocus) : GetAttributeForRole (VisualRole.HotNormal), + maximum: SuperView?.ViewportToScreen (SuperView.Viewport) ?? Rectangle.Empty); } else { diff --git a/Terminal.Gui/Views/Menuv1/MenuBar.cs b/Terminal.Gui/Views/Menuv1/MenuBar.cs index d2ab03dc5..78d1c3a29 100644 --- a/Terminal.Gui/Views/Menuv1/MenuBar.cs +++ b/Terminal.Gui/Views/Menuv1/MenuBar.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Views; @@ -422,7 +421,7 @@ public class MenuBar : View, IDesignable _selected = 0; SetNeedsDraw (); - _previousFocused = (SuperView is null ? Application.Top?.Focused : SuperView.Focused)!; + _previousFocused = (SuperView is null ? Application.Current?.Focused : SuperView.Focused)!; OpenMenu (_selected); if (!SelectEnabledItem ( @@ -491,7 +490,7 @@ public class MenuBar : View, IDesignable if (_openMenu is null) { - _previousFocused = (SuperView is null ? Application.Top?.Focused ?? null : SuperView.Focused)!; + _previousFocused = (SuperView is null ? Application.Current?.Focused ?? null : SuperView.Focused)!; } OpenMenu (idx, sIdx, subMenu); @@ -703,7 +702,7 @@ public class MenuBar : View, IDesignable } Rectangle superViewFrame = SuperView?.Frame ?? Application.Screen; - View? sv = SuperView ?? Application.Top; + View? sv = SuperView ?? Application.Current; if (sv is null) { @@ -835,7 +834,7 @@ public class MenuBar : View, IDesignable { case null: // Open a submenu below a MenuBar - _lastFocused ??= SuperView is null ? Application.Top?.MostFocused : SuperView.MostFocused; + _lastFocused ??= SuperView is null ? Application.Current?.MostFocused : SuperView.MostFocused; if (_openSubMenu is { } && !CloseMenu (false, true)) { @@ -1680,9 +1679,9 @@ public class MenuBar : View, IDesignable /// - public bool EnableForDesign (ref TContext context) where TContext : notnull + public bool EnableForDesign (ref TContext targetView) where TContext : notnull { - if (context is not Func actionFn) + if (targetView is not Func actionFn) { actionFn = (_) => true; } diff --git a/Terminal.Gui/Views/Menuv1/MenuBarItem.cs b/Terminal.Gui/Views/Menuv1/MenuBarItem.cs index 8b3be2116..fe96c1589 100644 --- a/Terminal.Gui/Views/Menuv1/MenuBarItem.cs +++ b/Terminal.Gui/Views/Menuv1/MenuBarItem.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/Menuv1/MenuClosingEventArgs.cs b/Terminal.Gui/Views/Menuv1/MenuClosingEventArgs.cs index 0a830c2b8..e223893c8 100644 --- a/Terminal.Gui/Views/Menuv1/MenuClosingEventArgs.cs +++ b/Terminal.Gui/Views/Menuv1/MenuClosingEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; #pragma warning disable CS0618 // Type or member is obsolete @@ -30,4 +31,4 @@ public class MenuClosingEventArgs : EventArgs /// Indicates whether the current menu will reopen. public bool Reopen { get; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/Menuv1/MenuItem.cs b/Terminal.Gui/Views/Menuv1/MenuItem.cs index 002ea0a38..9d66c1c45 100644 --- a/Terminal.Gui/Views/Menuv1/MenuItem.cs +++ b/Terminal.Gui/Views/Menuv1/MenuItem.cs @@ -1,5 +1,4 @@ #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. -#nullable enable namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/Menuv1/MenuItemCheckStyle.cs b/Terminal.Gui/Views/Menuv1/MenuItemCheckStyle.cs index 87bc5168e..ad189e373 100644 --- a/Terminal.Gui/Views/Menuv1/MenuItemCheckStyle.cs +++ b/Terminal.Gui/Views/Menuv1/MenuItemCheckStyle.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Specifies how a shows selection state. @@ -12,4 +13,4 @@ public enum MenuItemCheckStyle /// The menu item is part of a menu radio group (see ) and will indicate selected state. Radio = 0b_0000_0010 -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/Menuv1/MenuOpenedEventArgs.cs b/Terminal.Gui/Views/Menuv1/MenuOpenedEventArgs.cs index ba6c4adcf..581b9b04c 100644 --- a/Terminal.Gui/Views/Menuv1/MenuOpenedEventArgs.cs +++ b/Terminal.Gui/Views/Menuv1/MenuOpenedEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; #pragma warning disable CS0618 // Type or member is obsolete @@ -18,4 +19,4 @@ public class MenuOpenedEventArgs : EventArgs /// The parent of . Will be null if menu opening is the root. public MenuBarItem Parent { get; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/Menuv1/MenuOpeningEventArgs.cs b/Terminal.Gui/Views/Menuv1/MenuOpeningEventArgs.cs index e19421caa..c2e10d6d4 100644 --- a/Terminal.Gui/Views/Menuv1/MenuOpeningEventArgs.cs +++ b/Terminal.Gui/Views/Menuv1/MenuOpeningEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; #pragma warning disable CS0618 // Type or member is obsolete @@ -23,4 +24,4 @@ public class MenuOpeningEventArgs : EventArgs /// The new to be replaced. public MenuBarItem NewMenuBarItem { get; set; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs index 478d79565..bdbf323a0 100644 --- a/Terminal.Gui/Views/MessageBox.cs +++ b/Terminal.Gui/Views/MessageBox.cs @@ -1,3 +1,4 @@ +#nullable disable  namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/NumericUpDown.cs b/Terminal.Gui/Views/NumericUpDown.cs index b0066c363..adb94d554 100644 --- a/Terminal.Gui/Views/NumericUpDown.cs +++ b/Terminal.Gui/Views/NumericUpDown.cs @@ -1,4 +1,3 @@ -#nullable enable using System.ComponentModel; using System.Numerics; diff --git a/Terminal.Gui/Views/ProgressBar.cs b/Terminal.Gui/Views/ProgressBar.cs index 3e27bd556..b7cf3a2e0 100644 --- a/Terminal.Gui/Views/ProgressBar.cs +++ b/Terminal.Gui/Views/ProgressBar.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; @@ -185,12 +185,12 @@ public class ProgressBar : View, IDesignable GetAttributeForRole (VisualRole.Active).Style); } - tf.Draw ( - ViewportToScreen (Viewport), - attr, - GetAttributeForRole (VisualRole.Normal), - SuperView?.ViewportToScreen (SuperView.Viewport) ?? default (Rectangle) - ); + tf.Draw ( + driver: Driver, + screen: ViewportToScreen (Viewport), + normalColor: attr, + hotColor: GetAttributeForRole (VisualRole.Normal), + maximum: SuperView?.ViewportToScreen (SuperView.Viewport) ?? default (Rectangle)); } return true; diff --git a/Terminal.Gui/Views/ReadOnlyCollectionExtensions.cs b/Terminal.Gui/Views/ReadOnlyCollectionExtensions.cs index 6eb572e29..0a615c92f 100644 --- a/Terminal.Gui/Views/ReadOnlyCollectionExtensions.cs +++ b/Terminal.Gui/Views/ReadOnlyCollectionExtensions.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/ScrollBar/ScrollBar.cs b/Terminal.Gui/Views/ScrollBar/ScrollBar.cs index e6b1eb590..4d7468434 100644 --- a/Terminal.Gui/Views/ScrollBar/ScrollBar.cs +++ b/Terminal.Gui/Views/ScrollBar/ScrollBar.cs @@ -1,4 +1,4 @@ -#nullable enable + using System.ComponentModel; diff --git a/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs b/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs index 920f88cce..d3d861ac2 100644 --- a/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs +++ b/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs @@ -1,4 +1,4 @@ -#nullable enable + using System.ComponentModel; diff --git a/Terminal.Gui/Views/SelectedItemChangedArgs.cs b/Terminal.Gui/Views/SelectedItemChangedArgs.cs index 691dea2f6..6d09f0d8f 100644 --- a/Terminal.Gui/Views/SelectedItemChangedArgs.cs +++ b/Terminal.Gui/Views/SelectedItemChangedArgs.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; /// Event arguments for the SelectedItemChanged event. diff --git a/Terminal.Gui/Views/Selectors/FlagSelector.cs b/Terminal.Gui/Views/Selectors/FlagSelector.cs index dfc45456a..c2c8c15f9 100644 --- a/Terminal.Gui/Views/Selectors/FlagSelector.cs +++ b/Terminal.Gui/Views/Selectors/FlagSelector.cs @@ -1,7 +1,3 @@ -#nullable enable - -using System.Collections.Immutable; - namespace Terminal.Gui.Views; // DoubleClick - Focus, Select (Toggle), and Accept the item under the mouse. diff --git a/Terminal.Gui/Views/Selectors/FlagSelectorTEnum.cs b/Terminal.Gui/Views/Selectors/FlagSelectorTEnum.cs index 9ec066e02..86aa5c9a8 100644 --- a/Terminal.Gui/Views/Selectors/FlagSelectorTEnum.cs +++ b/Terminal.Gui/Views/Selectors/FlagSelectorTEnum.cs @@ -1,4 +1,3 @@ -#nullable enable using System; namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/Selectors/OptionSelector.cs b/Terminal.Gui/Views/Selectors/OptionSelector.cs index 48a9d3503..ab4159cc3 100644 --- a/Terminal.Gui/Views/Selectors/OptionSelector.cs +++ b/Terminal.Gui/Views/Selectors/OptionSelector.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Immutable; using System.Diagnostics; diff --git a/Terminal.Gui/Views/Selectors/OptionSelectorTEnum.cs b/Terminal.Gui/Views/Selectors/OptionSelectorTEnum.cs index 604a6cf82..188d1fcc7 100644 --- a/Terminal.Gui/Views/Selectors/OptionSelectorTEnum.cs +++ b/Terminal.Gui/Views/Selectors/OptionSelectorTEnum.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/Selectors/SelectorBase.cs b/Terminal.Gui/Views/Selectors/SelectorBase.cs index 7bef55dd2..5747bf4b1 100644 --- a/Terminal.Gui/Views/Selectors/SelectorBase.cs +++ b/Terminal.Gui/Views/Selectors/SelectorBase.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Immutable; namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/Selectors/SelectorStyles.cs b/Terminal.Gui/Views/Selectors/SelectorStyles.cs index c1087001d..cf0fad51c 100644 --- a/Terminal.Gui/Views/Selectors/SelectorStyles.cs +++ b/Terminal.Gui/Views/Selectors/SelectorStyles.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index ccccc01bb..a0b7c5683 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.ComponentModel; +using System.Diagnostics; namespace Terminal.Gui.Views; @@ -25,7 +24,8 @@ namespace Terminal.Gui.Views; /// /// By default, a Shortcut displays the command text on the left side, the help text in the middle, and the key /// binding on the -/// right side. Set to to reverse the order. +/// right side. Set to to reverse the +/// order. /// /// /// The command text can be set by setting the 's Text property or by setting @@ -44,7 +44,7 @@ public class Shortcut : View, IOrientation, IDesignable /// /// Creates a new instance of . /// - public Shortcut () : this (Key.Empty, null, null, null) { } + public Shortcut () : this (Key.Empty, null, null) { } /// /// Creates a new instance of . @@ -60,7 +60,7 @@ public class Shortcut : View, IOrientation, IDesignable /// The help text to display. public Shortcut (Key key, string? commandText, Action? action, string? helpText = null) { - HighlightStates = ViewBase.MouseState.None; + HighlightStates = MouseState.None; CanFocus = true; if (Border is { }) @@ -88,10 +88,12 @@ public class Shortcut : View, IOrientation, IDesignable Title = commandText ?? string.Empty; HelpView.Id = "_helpView"; + //HelpView.CanFocus = false; HelpView.Text = helpText ?? string.Empty; KeyView.Id = "_keyView"; + //KeyView.CanFocus = false; key ??= Key.Empty; Key = key; @@ -101,13 +103,22 @@ public class Shortcut : View, IOrientation, IDesignable ShowHide (); } + /// + public override void EndInit () + { + base.EndInit (); + App ??= SuperView?.App; + Debug.Assert (App is { }); + UpdateKeyBindings (Key.Empty); + } + // Helper to set Width consistently internal Dim GetWidthDimAuto () { return Dim.Auto ( DimAutoStyle.Content, - minimumContentDim: Dim.Func (_ => _minimumNaturalWidth ?? 0), - maximumContentDim: Dim.Func (_ => _minimumNaturalWidth ?? 0))!; + Dim.Func (_ => _minimumNaturalWidth ?? 0), + Dim.Func (_ => _minimumNaturalWidth ?? 0))!; } private AlignmentModes _alignmentModes = AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast; @@ -168,24 +179,22 @@ public class Shortcut : View, IOrientation, IDesignable private void ForceCalculateNaturalWidth () { // Get the natural size of each subview - CommandView.SetRelativeLayout (Application.Screen.Size); - HelpView.SetRelativeLayout (Application.Screen.Size); - KeyView.SetRelativeLayout (Application.Screen.Size); + Size screenSize = App?.Screen.Size ?? new (2048, 2048); + CommandView.SetRelativeLayout (screenSize); + HelpView.SetRelativeLayout (screenSize); + KeyView.SetRelativeLayout (screenSize); _minimumNaturalWidth = PosAlign.CalculateMinDimension (0, SubViews, Dimension.Width); // Reset our relative layout - SetRelativeLayout (SuperView?.GetContentSize () ?? Application.Screen.Size); + SetRelativeLayout (SuperView?.GetContentSize () ?? screenSize); } // TODO: Enable setting of the margin thickness - private Thickness GetMarginThickness () - { - return new (1, 0, 1, 0); - } + private Thickness GetMarginThickness () => new (1, 0, 1, 0); // When layout starts, we need to adjust the layout of the HelpView and KeyView - /// + /// protected override void OnSubViewLayout (LayoutEventArgs e) { base.OnSubViewLayout (e); @@ -229,27 +238,30 @@ public class Shortcut : View, IOrientation, IDesignable } } - #region Accept/Select/HotKey Command Handling private void AddCommands () { // Accept (Enter key) - AddCommand (Command.Accept, DispatchCommand); + // Hotkey - AddCommand (Command.HotKey, DispatchCommand); + // Select (Space key or click) - AddCommand (Command.Select, DispatchCommand); } /// - /// Dispatches the Command in the (Raises Selected, then Accepting, then invoke the Action, if any). + /// Dispatches the Command in the (Raises Selected, then Accepting, then invoke the + /// Action, if any). /// Called when Command.Select, Accept, or HotKey has been invoked on this Shortcut. /// /// /// /// if no event was raised; input processing should continue. - /// if the event was raised and was not handled (or cancelled); input processing should continue. + /// if the event was raised and was not handled (or cancelled); input processing should + /// continue. /// if the event was raised and handled (or cancelled); input processing should stop. /// internal virtual bool? DispatchCommand (ICommandContext? commandContext) @@ -290,6 +302,7 @@ public class Shortcut : View, IOrientation, IDesignable { commandContext.Source = this; } + Logging.Debug ($"{Title} ({commandContext?.Source?.Title}) - Calling RaiseAccepting..."); cancel = RaiseAccepting (commandContext) is true; @@ -378,7 +391,7 @@ public class Shortcut : View, IOrientation, IDesignable /// /// /// This example illustrates how to add a to a that toggles the - /// property. + /// property. /// /// /// var force16ColorsShortcut = new Shortcut @@ -455,8 +468,8 @@ public class Shortcut : View, IOrientation, IDesignable void CommandViewOnSelecting (object? sender, CommandEventArgs e) { - if ((e.Context is CommandContext keyCommandContext && keyCommandContext.Binding.Data != this) || - e.Context is CommandContext) + if ((e.Context is CommandContext keyCommandContext && keyCommandContext.Binding.Data != this) + || e.Context is CommandContext) { // Forward command to ourselves InvokeCommand (Command.Select, new ([Command.Select], null, this)); @@ -472,6 +485,7 @@ public class Shortcut : View, IOrientation, IDesignable if (CommandView.Margin is { }) { CommandView.Margin!.Thickness = GetMarginThickness (); + // strip off ViewportSettings.TransparentMouse CommandView.Margin!.ViewportSettings &= ~ViewportSettingsFlags.TransparentMouse; } @@ -481,6 +495,7 @@ public class Shortcut : View, IOrientation, IDesignable CommandView.VerticalTextAlignment = Alignment.Center; CommandView.TextAlignment = Alignment.Start; CommandView.TextFormatter.WordWrap = false; + //CommandView.HighlightStates = HighlightStates.None; CommandView.GettingAttributeForRole += SubViewOnGettingAttributeForRole; } @@ -495,6 +510,7 @@ public class Shortcut : View, IOrientation, IDesignable e.Handled = true; e.Result = GetAttributeForRole (VisualRole.Focus); } + break; case VisualRole.HotNormal: @@ -503,17 +519,18 @@ public class Shortcut : View, IOrientation, IDesignable e.Handled = true; e.Result = GetAttributeForRole (VisualRole.HotFocus); } + break; } } - private void Shortcut_TitleChanged (object? sender, EventArgs e) { // If the Title changes, update the CommandView text. // This is a helper to make it easier to set the CommandView text. // CommandView is public and replaceable, but this is a convenience. _commandView.Text = Title; + //_commandView.Title = Title; } @@ -522,7 +539,7 @@ public class Shortcut : View, IOrientation, IDesignable #region Help // The maximum width of the HelpView. Calculated in OnLayoutStarted and used in HelpView.Width (Dim.Auto/Func). - private int _maxHelpWidth = 0; + private int _maxHelpWidth; /// /// The subview that displays the help text for the command. Internal for unit testing. @@ -534,20 +551,21 @@ public class Shortcut : View, IOrientation, IDesignable if (HelpView.Margin is { }) { HelpView.Margin!.Thickness = GetMarginThickness (); + // strip off ViewportSettings.TransparentMouse HelpView.Margin!.ViewportSettings &= ~ViewportSettingsFlags.TransparentMouse; } HelpView.X = Pos.Align (Alignment.End, AlignmentModes); _maxHelpWidth = HelpView.Text.GetColumns (); - HelpView.Width = Dim.Auto (DimAutoStyle.Text, maximumContentDim: Dim.Func ((_ => _maxHelpWidth))); + HelpView.Width = Dim.Auto (DimAutoStyle.Text, maximumContentDim: Dim.Func (_ => _maxHelpWidth)); HelpView.Height = Dim.Fill (); HelpView.Visible = true; HelpView.VerticalTextAlignment = Alignment.Center; HelpView.TextAlignment = Alignment.Start; HelpView.TextFormatter.WordWrap = false; - HelpView.HighlightStates = ViewBase.MouseState.None; + HelpView.HighlightStates = MouseState.None; HelpView.GettingAttributeForRole += SubViewOnGettingAttributeForRole; } @@ -605,16 +623,19 @@ public class Shortcut : View, IOrientation, IDesignable } } - private bool _bindKeyToApplication = false; + private bool _bindKeyToApplication; /// - /// Gets or sets whether is bound to via or . + /// Gets or sets whether is bound to via or + /// . /// public bool BindKeyToApplication { get => _bindKeyToApplication; set { + App ??= SuperView?.App; + if (value == _bindKeyToApplication) { return; @@ -622,7 +643,7 @@ public class Shortcut : View, IOrientation, IDesignable if (_bindKeyToApplication) { - Application.KeyBindings.Remove (Key); + App?.Keyboard.KeyBindings.Remove (Key); } else { @@ -661,18 +682,18 @@ public class Shortcut : View, IOrientation, IDesignable } } - private void SetKeyViewDefaultLayout () { if (KeyView.Margin is { }) { KeyView.Margin!.Thickness = GetMarginThickness (); + // strip off ViewportSettings.TransparentMouse KeyView.Margin!.ViewportSettings &= ~ViewportSettingsFlags.TransparentMouse; } KeyView.X = Pos.Align (Alignment.End, AlignmentModes); - KeyView.Width = Dim.Auto (DimAutoStyle.Text, minimumContentDim: Dim.Func (_ => MinimumKeyTextSize)); + KeyView.Width = Dim.Auto (DimAutoStyle.Text, Dim.Func (_ => MinimumKeyTextSize)); KeyView.Height = Dim.Fill (); KeyView.Visible = true; @@ -681,21 +702,23 @@ public class Shortcut : View, IOrientation, IDesignable KeyView.TextAlignment = Alignment.End; KeyView.VerticalTextAlignment = Alignment.Center; KeyView.KeyBindings.Clear (); - KeyView.HighlightStates = ViewBase.MouseState.None; + KeyView.HighlightStates = MouseState.None; KeyView.GettingAttributeForRole += (sender, args) => { if (args.Role == VisualRole.Normal) { - args.Result = SuperView?.GetAttributeForRole (HasFocus ? VisualRole.HotFocus : VisualRole.HotNormal) ?? Attribute.Default; + args.Result = SuperView?.GetAttributeForRole (HasFocus ? VisualRole.HotFocus : VisualRole.HotNormal) + ?? Attribute.Default; args.Handled = true; } }; + KeyView.ClearingViewport += (sender, args) => - { - // Do not clear; otherwise spaces will be printed with underlines - args.Cancel = true; - }; + { + // Do not clear; otherwise spaces will be printed with underlines + args.Cancel = true; + }; } private void UpdateKeyBindings (Key oldKey) @@ -709,11 +732,11 @@ public class Shortcut : View, IOrientation, IDesignable { if (oldKey != Key.Empty) { - Application.KeyBindings.Remove (oldKey); + App?.Keyboard.KeyBindings.Remove (oldKey); } - Application.KeyBindings.Remove (Key); - Application.KeyBindings.Add (Key, this, Command.HotKey); + App?.Keyboard.KeyBindings.Remove (Key); + App?.Keyboard.KeyBindings.Add (Key, this, Command.HotKey); } else { @@ -746,7 +769,7 @@ public class Shortcut : View, IOrientation, IDesignable } } - /// + /// protected override bool OnGettingAttributeForRole (in VisualRole role, ref Attribute currentAttribute) { if (!HasFocus) @@ -760,6 +783,7 @@ public class Shortcut : View, IOrientation, IDesignable return true; } + if (role == VisualRole.HotNormal) { currentAttribute = GetAttributeForRole (VisualRole.HotFocus); @@ -807,4 +831,4 @@ public class Shortcut : View, IOrientation, IDesignable base.Dispose (disposing); } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/Slider/Slider.cs b/Terminal.Gui/Views/Slider/Slider.cs index ff0308a56..3f379790a 100644 --- a/Terminal.Gui/Views/Slider/Slider.cs +++ b/Terminal.Gui/Views/Slider/Slider.cs @@ -1,3 +1,4 @@ +#nullable disable using System.Transactions; namespace Terminal.Gui.Views; @@ -1311,7 +1312,7 @@ public class Slider : View, IOrientation { _dragPosition = mouseEvent.Position; _moveRenderPosition = ClampMovePosition ((Point)_dragPosition); - Application.Mouse.GrabMouse (this); + App?.Mouse.GrabMouse (this); } SetNeedsDraw (); @@ -1357,7 +1358,7 @@ public class Slider : View, IOrientation || mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) { // End Drag - Application.Mouse.UngrabMouse (); + App?.Mouse.UngrabMouse (); _dragPosition = null; _moveRenderPosition = null; diff --git a/Terminal.Gui/Views/Slider/SliderAttributes.cs b/Terminal.Gui/Views/Slider/SliderAttributes.cs index db7f199dc..c57c8ab5a 100644 --- a/Terminal.Gui/Views/Slider/SliderAttributes.cs +++ b/Terminal.Gui/Views/Slider/SliderAttributes.cs @@ -11,4 +11,4 @@ public class SliderAttributes /// Attribute for when the respective Option is Set. public Attribute? SetAttribute { get; set; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/Slider/SliderConfiguration.cs b/Terminal.Gui/Views/Slider/SliderConfiguration.cs index 2e75ada0c..1b9354a48 100644 --- a/Terminal.Gui/Views/Slider/SliderConfiguration.cs +++ b/Terminal.Gui/Views/Slider/SliderConfiguration.cs @@ -1,4 +1,4 @@ - + namespace Terminal.Gui.Views; /// All configuration are grouped in this class. @@ -17,4 +17,4 @@ internal class SliderConfiguration internal int _startSpacing; internal SliderType _type = SliderType.Single; internal bool _useMinimumSize; -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/Slider/SliderEventArgs.cs b/Terminal.Gui/Views/Slider/SliderEventArgs.cs index b4f93ca12..0b55b4012 100644 --- a/Terminal.Gui/Views/Slider/SliderEventArgs.cs +++ b/Terminal.Gui/Views/Slider/SliderEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// for events. @@ -21,4 +22,4 @@ public class SliderEventArgs : EventArgs /// Gets/sets whether the option is set or not. public Dictionary> Options { get; set; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/Slider/SliderOption.cs b/Terminal.Gui/Views/Slider/SliderOption.cs index 95a929d6a..d5207c301 100644 --- a/Terminal.Gui/Views/Slider/SliderOption.cs +++ b/Terminal.Gui/Views/Slider/SliderOption.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Represents an option in a . @@ -47,4 +48,4 @@ public class SliderOption /// To Raise the event from the Slider. internal void OnUnSet () { UnSet?.Invoke (this, new (false)); } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/Slider/SliderOptionEventArgs.cs b/Terminal.Gui/Views/Slider/SliderOptionEventArgs.cs index 8014c2a77..5176f9601 100644 --- a/Terminal.Gui/Views/Slider/SliderOptionEventArgs.cs +++ b/Terminal.Gui/Views/Slider/SliderOptionEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// for events. @@ -9,4 +10,4 @@ public class SliderOptionEventArgs : EventArgs /// Gets whether the option is set or not. public bool IsSet { get; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/Slider/SliderStyle.cs b/Terminal.Gui/Views/Slider/SliderStyle.cs index 38c6570c3..aa5557116 100644 --- a/Terminal.Gui/Views/Slider/SliderStyle.cs +++ b/Terminal.Gui/Views/Slider/SliderStyle.cs @@ -33,4 +33,4 @@ public class SliderStyle /// The glyph and the attribute used for the start of ranges on the slider. public Cell StartRangeChar { get; set; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/Slider/SliderType.cs b/Terminal.Gui/Views/Slider/SliderType.cs index 7ad287a97..7715aa3c3 100644 --- a/Terminal.Gui/Views/Slider/SliderType.cs +++ b/Terminal.Gui/Views/Slider/SliderType.cs @@ -37,4 +37,4 @@ public enum SliderType /// /// Range -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/SpinnerView/SpinnerView.cs b/Terminal.Gui/Views/SpinnerView/SpinnerView.cs index 00f723fe6..be1337591 100644 --- a/Terminal.Gui/Views/SpinnerView/SpinnerView.cs +++ b/Terminal.Gui/Views/SpinnerView/SpinnerView.cs @@ -1,4 +1,4 @@ -#nullable enable + //------------------------------------------------------------------------------ // Windows Terminal supports Unicode and Emoji characters, but by default @@ -114,7 +114,7 @@ public class SpinnerView : View, IDesignable /// Advances the animation frame and notifies main loop that repainting needs to happen. Repeated calls are /// ignored based on . /// - /// Ensure this method is called on the main UI thread e.g. via + /// Ensure this method is called on the main UI thread e.g. via public void AdvanceAnimation (bool setNeedsDraw = true) { if (DateTime.Now - _lastRender > TimeSpan.FromMilliseconds (SpinDelay)) @@ -202,16 +202,16 @@ public class SpinnerView : View, IDesignable private void AddAutoSpinTimeout () { // Only add timeout if we are initialized and not already spinning - if (_timeout is { } || !Application.Initialized) + if (App is { } && (_timeout is { } || !App.Initialized)) { return; } - _timeout = Application.AddTimeout ( + _timeout = App?.AddTimeout ( TimeSpan.FromMilliseconds (SpinDelay), () => { - Application.Invoke (() => AdvanceAnimation ()); + App.Invoke ((_) => AdvanceAnimation ()); return true; } @@ -266,7 +266,7 @@ public class SpinnerView : View, IDesignable { if (_timeout is { }) { - Application.RemoveTimeout (_timeout); + App?.RemoveTimeout (_timeout); _timeout = null; } } diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs index 3d58a7afc..cd97abd06 100644 --- a/Terminal.Gui/Views/StatusBar.cs +++ b/Terminal.Gui/Views/StatusBar.cs @@ -1,6 +1,3 @@ -#nullable enable - - namespace Terminal.Gui.Views; /// @@ -137,12 +134,14 @@ public class StatusBar : Bar, IDesignable button1.Accepting += OnButtonClicked; Add (button1); - shortcut.Accepting += (s, e) => - { - button1.Visible = !button1.Visible; - button1.Enabled = button1.Visible; - e.Handled = false; - }; +#pragma warning disable TGUI001 + shortcut.Accepting += (_, e) => + { + button1.Visible = !button1.Visible; + button1.Enabled = button1.Visible; + e.Handled = false; + }; +#pragma warning restore TGUI001 Add (new Label { @@ -155,7 +154,7 @@ public class StatusBar : Bar, IDesignable { Text = "Or me!", }; - button2.Accepting += (s, e) => Application.RequestStop (); + button2.Accepting += (s, e) => App?.RequestStop (); Add (button2); diff --git a/Terminal.Gui/Views/TabView/Tab.cs b/Terminal.Gui/Views/TabView/Tab.cs index f0025b983..30562810d 100644 --- a/Terminal.Gui/Views/TabView/Tab.cs +++ b/Terminal.Gui/Views/TabView/Tab.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TabView/TabChangedEventArgs.cs b/Terminal.Gui/Views/TabView/TabChangedEventArgs.cs index acaa90fd6..4a769c688 100644 --- a/Terminal.Gui/Views/TabView/TabChangedEventArgs.cs +++ b/Terminal.Gui/Views/TabView/TabChangedEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Describes a change in diff --git a/Terminal.Gui/Views/TabView/TabMouseEventArgs.cs b/Terminal.Gui/Views/TabView/TabMouseEventArgs.cs index f52792549..94c55803a 100644 --- a/Terminal.Gui/Views/TabView/TabMouseEventArgs.cs +++ b/Terminal.Gui/Views/TabView/TabMouseEventArgs.cs @@ -1,4 +1,4 @@ -#nullable enable + using System.ComponentModel; namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TabView/TabRow.cs b/Terminal.Gui/Views/TabView/TabRow.cs index f85db683a..5534ac5be 100644 --- a/Terminal.Gui/Views/TabView/TabRow.cs +++ b/Terminal.Gui/Views/TabView/TabRow.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TabView/TabStyle.cs b/Terminal.Gui/Views/TabView/TabStyle.cs index 07ac12884..caf059ba0 100644 --- a/Terminal.Gui/Views/TabView/TabStyle.cs +++ b/Terminal.Gui/Views/TabView/TabStyle.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Describes render stylistic selections of a diff --git a/Terminal.Gui/Views/TabView/TabView.cs b/Terminal.Gui/Views/TabView/TabView.cs index 6c222d9f3..f401c500b 100644 --- a/Terminal.Gui/Views/TabView/TabView.cs +++ b/Terminal.Gui/Views/TabView/TabView.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TableView/CellActivatedEventArgs.cs b/Terminal.Gui/Views/TableView/CellActivatedEventArgs.cs index d5dc4eb3e..cb27a1b43 100644 --- a/Terminal.Gui/Views/TableView/CellActivatedEventArgs.cs +++ b/Terminal.Gui/Views/TableView/CellActivatedEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; // TOOD: SHould support Handled diff --git a/Terminal.Gui/Views/TableView/CellColorGetterArgs.cs b/Terminal.Gui/Views/TableView/CellColorGetterArgs.cs index 2dfcc069e..48a0b492e 100644 --- a/Terminal.Gui/Views/TableView/CellColorGetterArgs.cs +++ b/Terminal.Gui/Views/TableView/CellColorGetterArgs.cs @@ -1,3 +1,4 @@ +#nullable disable  namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TableView/CellToggledEventArgs.cs b/Terminal.Gui/Views/TableView/CellToggledEventArgs.cs index 281dbb6f9..f0a88aaae 100644 --- a/Terminal.Gui/Views/TableView/CellToggledEventArgs.cs +++ b/Terminal.Gui/Views/TableView/CellToggledEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Event args for the event. diff --git a/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs b/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs index c058b2557..5d0b41ed9 100644 --- a/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs +++ b/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs @@ -1,3 +1,4 @@ +#nullable disable  namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapperByIndex.cs b/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapperByIndex.cs index b82bebb13..7e1c4ab58 100644 --- a/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapperByIndex.cs +++ b/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapperByIndex.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Implementation of which records toggled rows by their row number. diff --git a/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapperByObject.cs b/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapperByObject.cs index faa746ced..87028346f 100644 --- a/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapperByObject.cs +++ b/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapperByObject.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/TableView/ColumnStyle.cs b/Terminal.Gui/Views/TableView/ColumnStyle.cs index e9afecaa7..8a1ca6526 100644 --- a/Terminal.Gui/Views/TableView/ColumnStyle.cs +++ b/Terminal.Gui/Views/TableView/ColumnStyle.cs @@ -1,3 +1,4 @@ +#nullable disable  namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TableView/DataTableSource.cs b/Terminal.Gui/Views/TableView/DataTableSource.cs index 551b38b05..e8ac64b2e 100644 --- a/Terminal.Gui/Views/TableView/DataTableSource.cs +++ b/Terminal.Gui/Views/TableView/DataTableSource.cs @@ -1,3 +1,4 @@ +#nullable disable using System.Data; namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TableView/EnumerableTableSource.cs b/Terminal.Gui/Views/TableView/EnumerableTableSource.cs index a771ca6fd..3499a63e1 100644 --- a/Terminal.Gui/Views/TableView/EnumerableTableSource.cs +++ b/Terminal.Gui/Views/TableView/EnumerableTableSource.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// implementation that wraps arbitrary data. diff --git a/Terminal.Gui/Views/TableView/IEnumerableTableSource.cs b/Terminal.Gui/Views/TableView/IEnumerableTableSource.cs index c21700836..7827b9c77 100644 --- a/Terminal.Gui/Views/TableView/IEnumerableTableSource.cs +++ b/Terminal.Gui/Views/TableView/IEnumerableTableSource.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/TableView/ITableSource.cs b/Terminal.Gui/Views/TableView/ITableSource.cs index 4848642da..c9728efc8 100644 --- a/Terminal.Gui/Views/TableView/ITableSource.cs +++ b/Terminal.Gui/Views/TableView/ITableSource.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Tabular matrix of data to be displayed in a . diff --git a/Terminal.Gui/Views/TableView/ListColumnStyle.cs b/Terminal.Gui/Views/TableView/ListColumnStyle.cs index cce3376b0..e805e6338 100644 --- a/Terminal.Gui/Views/TableView/ListColumnStyle.cs +++ b/Terminal.Gui/Views/TableView/ListColumnStyle.cs @@ -1,4 +1,4 @@ - + namespace Terminal.Gui.Views; /// Defines rendering options that affect how the view is displayed. diff --git a/Terminal.Gui/Views/TableView/ListTableSource.cs b/Terminal.Gui/Views/TableView/ListTableSource.cs index bc07d404f..7f535c300 100644 --- a/Terminal.Gui/Views/TableView/ListTableSource.cs +++ b/Terminal.Gui/Views/TableView/ListTableSource.cs @@ -1,3 +1,4 @@ +#nullable disable using System.Collections; using System.Data; diff --git a/Terminal.Gui/Views/TableView/RowColorGetterArgs.cs b/Terminal.Gui/Views/TableView/RowColorGetterArgs.cs index 6335a484d..c17e0c958 100644 --- a/Terminal.Gui/Views/TableView/RowColorGetterArgs.cs +++ b/Terminal.Gui/Views/TableView/RowColorGetterArgs.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/TableView/SelectedCellChangedEventArgs.cs b/Terminal.Gui/Views/TableView/SelectedCellChangedEventArgs.cs index 4aad9ccae..733f32b3d 100644 --- a/Terminal.Gui/Views/TableView/SelectedCellChangedEventArgs.cs +++ b/Terminal.Gui/Views/TableView/SelectedCellChangedEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Defines the event arguments for diff --git a/Terminal.Gui/Views/TableView/TableSelection.cs b/Terminal.Gui/Views/TableView/TableSelection.cs index f6217ef0b..2fe21f9cd 100644 --- a/Terminal.Gui/Views/TableView/TableSelection.cs +++ b/Terminal.Gui/Views/TableView/TableSelection.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Describes a selected region of the table diff --git a/Terminal.Gui/Views/TableView/TableStyle.cs b/Terminal.Gui/Views/TableView/TableStyle.cs index e52cddf1f..8640fa694 100644 --- a/Terminal.Gui/Views/TableView/TableStyle.cs +++ b/Terminal.Gui/Views/TableView/TableStyle.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index 272e7495d..f09532ed2 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -1,3 +1,4 @@ +#nullable disable using System.Data; using System.Globalization; diff --git a/Terminal.Gui/Views/TableView/TreeTableSource.cs b/Terminal.Gui/Views/TableView/TreeTableSource.cs index 6cbb5b0a7..8c6925399 100644 --- a/Terminal.Gui/Views/TableView/TreeTableSource.cs +++ b/Terminal.Gui/Views/TableView/TreeTableSource.cs @@ -1,4 +1,5 @@ +#nullable disable namespace Terminal.Gui.Views; /// An with expandable rows. diff --git a/Terminal.Gui/Views/TextInput/ContentsChangedEventArgs.cs b/Terminal.Gui/Views/TextInput/ContentsChangedEventArgs.cs index 23e1a5dab..58eac9160 100644 --- a/Terminal.Gui/Views/TextInput/ContentsChangedEventArgs.cs +++ b/Terminal.Gui/Views/TextInput/ContentsChangedEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable // TextView.cs: multi-line text editing namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TextInput/DateField.cs b/Terminal.Gui/Views/TextInput/DateField.cs index 2da26f9a0..74c48f5f3 100644 --- a/Terminal.Gui/Views/TextInput/DateField.cs +++ b/Terminal.Gui/Views/TextInput/DateField.cs @@ -1,4 +1,4 @@ -#nullable enable + // // DateField.cs: text entry for date diff --git a/Terminal.Gui/Views/TextInput/HistoryText.cs b/Terminal.Gui/Views/TextInput/HistoryText.cs index 62fd65c8b..7b04f1676 100644 --- a/Terminal.Gui/Views/TextInput/HistoryText.cs +++ b/Terminal.Gui/Views/TextInput/HistoryText.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TextInput/HistoryTextItemEventArgs.cs b/Terminal.Gui/Views/TextInput/HistoryTextItemEventArgs.cs index 875c039a3..bf67d14c2 100644 --- a/Terminal.Gui/Views/TextInput/HistoryTextItemEventArgs.cs +++ b/Terminal.Gui/Views/TextInput/HistoryTextItemEventArgs.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TextInput/ITextValidateProvider.cs b/Terminal.Gui/Views/TextInput/ITextValidateProvider.cs index e25306cd3..03c614da9 100644 --- a/Terminal.Gui/Views/TextInput/ITextValidateProvider.cs +++ b/Terminal.Gui/Views/TextInput/ITextValidateProvider.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TextInput/NetMaskedTextProvider.cs b/Terminal.Gui/Views/TextInput/NetMaskedTextProvider.cs index f082807c6..0ac0659cd 100644 --- a/Terminal.Gui/Views/TextInput/NetMaskedTextProvider.cs +++ b/Terminal.Gui/Views/TextInput/NetMaskedTextProvider.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.ComponentModel; +using System.ComponentModel; namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TextInput/TextEditingLineStatus.cs b/Terminal.Gui/Views/TextInput/TextEditingLineStatus.cs index e71f6de1d..0259b15ee 100644 --- a/Terminal.Gui/Views/TextInput/TextEditingLineStatus.cs +++ b/Terminal.Gui/Views/TextInput/TextEditingLineStatus.cs @@ -9,6 +9,7 @@ /// line. /// // ReSharper disable once CheckNamespace +#nullable disable public enum TextEditingLineStatus { /// diff --git a/Terminal.Gui/Views/TextInput/TextField.cs b/Terminal.Gui/Views/TextInput/TextField.cs index bc2390fb6..37a92062b 100644 --- a/Terminal.Gui/Views/TextInput/TextField.cs +++ b/Terminal.Gui/Views/TextInput/TextField.cs @@ -1,3 +1,4 @@ +#nullable disable using System.Globalization; namespace Terminal.Gui.Views; @@ -418,9 +419,6 @@ public class TextField : View, IDesignable KeyBindings.Remove (Key.Space); _currentCulture = Thread.CurrentThread.CurrentUICulture; - - CreateContextMenu (); - KeyBindings.Add (ContextMenu.Key, Command.Context); } /// @@ -864,16 +862,16 @@ public class TextField : View, IDesignable _isButtonReleased = false; PrepareSelection (x); - if (Application.Mouse.MouseGrabView is null) + if (App?.Mouse.MouseGrabView is null) { - Application.Mouse.GrabMouse (this); + App?.Mouse.GrabMouse (this); } } else if (ev.Flags == MouseFlags.Button1Released) { _isButtonReleased = true; _isButtonPressed = false; - Application.Mouse.UngrabMouse (); + App?.Mouse.UngrabMouse (); } else if (ev.Flags == MouseFlags.Button1DoubleClicked) { @@ -1016,13 +1014,10 @@ public class TextField : View, IDesignable /// protected override void OnHasFocusChanged (bool newHasFocus, View previousFocusedView, View view) { - if (Application.Mouse.MouseGrabView is { } && Application.Mouse.MouseGrabView == this) + if (App?.Mouse.MouseGrabView is { } && App?.Mouse.MouseGrabView == this) { - Application.Mouse.UngrabMouse (); + App?.Mouse.UngrabMouse (); } - - //if (SelectedLength != 0 && !(Application.Mouse.MouseGrabView is MenuBar)) - // ClearAllSelection (); } /// @@ -1254,6 +1249,7 @@ public class TextField : View, IDesignable menu.KeyChanged += ContextMenu_KeyChanged; ContextMenu = menu; + App?.Popover.Register (ContextMenu); } private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.Replace (e.OldKey.KeyCode, e.NewKey.KeyCode); } @@ -1730,10 +1726,7 @@ public class TextField : View, IDesignable GetAttributeForRole (VisualRole.Editable).Style | TextStyle.Underline); // Use TitleTextFormatter to render the caption with hotkey support - TitleTextFormatter.Draw ( - ViewportToScreen (new Rectangle (0, 0, Viewport.Width, 1)), - captionAttribute, - hotKeyAttribute); + TitleTextFormatter.Draw (driver: Driver, screen: ViewportToScreen (new Rectangle (0, 0, Viewport.Width, 1)), normalColor: captionAttribute, hotColor: hotKeyAttribute); } private void SetClipboard (IEnumerable text) @@ -1770,11 +1763,6 @@ public class TextField : View, IDesignable if (!Equals (_currentCulture, Thread.CurrentThread.CurrentUICulture)) { _currentCulture = Thread.CurrentThread.CurrentUICulture; - - if (ContextMenu is { }) - { - CreateContextMenu (); - } } if (keyboard) @@ -1817,6 +1805,10 @@ public class TextField : View, IDesignable Autocomplete.HostControl = this; Autocomplete.PopupInsideContainer = false; } + + CreateContextMenu (); + KeyBindings.Add (ContextMenu?.Key, Command.Context); + } private void DisposeContextMenu () diff --git a/Terminal.Gui/Views/TextInput/TextModel.cs b/Terminal.Gui/Views/TextInput/TextModel.cs index 275c8a908..81d07848a 100644 --- a/Terminal.Gui/Views/TextInput/TextModel.cs +++ b/Terminal.Gui/Views/TextInput/TextModel.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TextInput/TextRegexProvider.cs b/Terminal.Gui/Views/TextInput/TextRegexProvider.cs index 753c8c1bc..34ad5e6ff 100644 --- a/Terminal.Gui/Views/TextInput/TextRegexProvider.cs +++ b/Terminal.Gui/Views/TextInput/TextRegexProvider.cs @@ -1,4 +1,4 @@ -#nullable enable + using System.Text.RegularExpressions; namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TextInput/TextValidateField.cs b/Terminal.Gui/Views/TextInput/TextValidateField.cs index f7e897feb..db4cbbd54 100644 --- a/Terminal.Gui/Views/TextInput/TextValidateField.cs +++ b/Terminal.Gui/Views/TextInput/TextValidateField.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TextInput/TextView.cs b/Terminal.Gui/Views/TextInput/TextView.cs index f9e2031d9..8b5da5ef3 100644 --- a/Terminal.Gui/Views/TextInput/TextView.cs +++ b/Terminal.Gui/Views/TextInput/TextView.cs @@ -1,6 +1,3 @@ -#nullable enable - -// TextView.cs: multi-line text editing using System.Globalization; using System.Runtime.CompilerServices; @@ -116,7 +113,7 @@ public class TextView : View, IDesignable Used = true; // By default, disable hotkeys (in case someone sets Title) - HotKeySpecifier = new ('\xffff'); + base.HotKeySpecifier = new ('\xffff'); _model.LinesLoaded += Model_LinesLoaded!; _historyText.ChangeText += HistoryText_ChangeText!; @@ -629,9 +626,6 @@ public class TextView : View, IDesignable #endif _currentCulture = Thread.CurrentThread.CurrentUICulture; - - ContextMenu = CreateContextMenu (); - KeyBindings.Add (ContextMenu.Key, Command.Context); } // BUGBUG: AllowsReturn is mis-named. It should be EnterKeyAccepts. @@ -1676,15 +1670,15 @@ public class TextView : View, IDesignable _lastWasKill = false; _columnTrack = CurrentColumn; - if (Application.Mouse.MouseGrabView is null) + if (App?.Mouse.MouseGrabView is null) { - Application.Mouse.GrabMouse (this); + App?.Mouse.GrabMouse (this); } } else if (ev.Flags.HasFlag (MouseFlags.Button1Released)) { _isButtonReleased = true; - Application.Mouse.UngrabMouse (); + App?.Mouse.UngrabMouse (); } else if (ev.Flags.HasFlag (MouseFlags.Button1DoubleClicked)) { @@ -1886,9 +1880,9 @@ public class TextView : View, IDesignable /// protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? view) { - if (Application.Mouse.MouseGrabView is { } && Application.Mouse.MouseGrabView == this) + if (App?.Mouse.MouseGrabView is { } && App?.Mouse.MouseGrabView == this) { - Application.Mouse.UngrabMouse (); + App?.Mouse.UngrabMouse (); } } @@ -2027,12 +2021,12 @@ public class TextView : View, IDesignable { ProcessAutocomplete (); - if (!CanFocus || !Enabled || Application.Driver is null) + if (!CanFocus || !Enabled || Driver is null) { return null; } - if (Application.Mouse.MouseGrabView == this && IsSelecting) + if (App?.Mouse.MouseGrabView == this && IsSelecting) { // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. //var minRow = Math.Min (Math.Max (Math.Min (selectionStartRow, currentRow) - topRow, 0), Viewport.Height); @@ -4578,6 +4572,7 @@ public class TextView : View, IDesignable { mousePosition = ViewportToScreen (new Point (CursorPosition.X, CursorPosition.Y)); } + ContextMenu?.MakeVisible (mousePosition); } @@ -4653,6 +4648,11 @@ public class TextView : View, IDesignable Autocomplete.HostControl = this; } + + ContextMenu = CreateContextMenu (); + App?.Popover?.Register (ContextMenu); + KeyBindings.Add (ContextMenu.Key, Command.Context); + OnContentsChanged (); } @@ -4790,15 +4790,15 @@ public class TextView : View, IDesignable public class TextViewAutocomplete : PopupAutocomplete { /// - protected override void DeleteTextBackwards () { ((TextView)HostControl).DeleteCharLeft (); } + protected override void DeleteTextBackwards () { ((TextView)HostControl!).DeleteCharLeft (); } /// - protected override void InsertText (string accepted) { ((TextView)HostControl).InsertText (accepted); } + protected override void InsertText (string accepted) { ((TextView)HostControl!).InsertText (accepted); } /// protected override void SetCursorPosition (int column) { - ((TextView)HostControl).CursorPosition = + ((TextView)HostControl!).CursorPosition = new (column, ((TextView)HostControl).CurrentRow); } } diff --git a/Terminal.Gui/Views/TextInput/TimeField.cs b/Terminal.Gui/Views/TextInput/TimeField.cs index 59a906f63..e61e48d07 100644 --- a/Terminal.Gui/Views/TextInput/TimeField.cs +++ b/Terminal.Gui/Views/TextInput/TimeField.cs @@ -5,6 +5,7 @@ // // Licensed under the MIT license +#nullable disable using System.Globalization; namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TextInput/WordWrapManager.cs b/Terminal.Gui/Views/TextInput/WordWrapManager.cs index ced8c2a7c..705e07527 100644 --- a/Terminal.Gui/Views/TextInput/WordWrapManager.cs +++ b/Terminal.Gui/Views/TextInput/WordWrapManager.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index 926eb6aef..9f3854f7e 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace Terminal.Gui.Views; /// @@ -9,15 +7,15 @@ namespace Terminal.Gui.Views; /// /// /// Toplevel views can run as modal (popup) views, started by calling -/// . They return control to the caller when -/// has been called (which sets the +/// . They return control to the caller when +/// has been called (which sets the /// property to false). /// /// -/// A Toplevel is created when an application initializes Terminal.Gui by calling . -/// The application Toplevel can be accessed via . Additional Toplevels can be created +/// A Toplevel is created when an application initializes Terminal.Gui by calling . +/// The application Toplevel can be accessed via . Additional Toplevels can be created /// and run (e.g. s). To run a Toplevel, create the and call -/// . +/// . /// /// public partial class Toplevel : View @@ -84,12 +82,12 @@ public partial class Toplevel : View // TODO: IRunnable: Re-implement as a property on IRunnable /// Gets or sets whether the main loop for this is running or not. - /// Setting this property directly is discouraged. Use instead. + /// Setting this property directly is discouraged. Use instead. public bool Running { get; set; } // TODO: IRunnable: Re-implement in IRunnable /// - /// if was already loaded by the + /// if was already loaded by the /// , otherwise. /// public bool IsLoaded { get; private set; } @@ -102,12 +100,12 @@ public partial class Toplevel : View /// Invoked when the Toplevel ceases to be active. public event EventHandler? Deactivate; - /// Invoked when the Toplevel's is closed by . + /// Invoked when the Toplevel's is closed by . public event EventHandler? Closed; /// /// Invoked when the Toplevel's is being closed by - /// . + /// . /// public event EventHandler? Closing; @@ -118,7 +116,7 @@ public partial class Toplevel : View public event EventHandler? Loaded; /// - /// Called from before the redraws for the first + /// Called from before the redraws for the first /// time. /// /// @@ -143,23 +141,23 @@ public partial class Toplevel : View /// perform tasks when the has been laid out and focus has been set. changes. /// /// A Ready event handler is a good place to finalize initialization after calling - /// on this . + /// on this . /// /// public event EventHandler? Ready; /// /// Stops and closes this . If this Toplevel is the top-most Toplevel, - /// will be called, causing the application to exit. + /// will be called, causing the application to exit. /// public virtual void RequestStop () { - Application.RequestStop (Application.Top); + App?.RequestStop (App?.Current); } /// /// Invoked when the Toplevel has been unloaded. A Unloaded event handler is a good place - /// to dispose objects after calling . + /// to dispose objects after calling . /// public event EventHandler? Unloaded; @@ -191,7 +189,7 @@ public partial class Toplevel : View Ready?.Invoke (this, EventArgs.Empty); } - /// Called from before the is disposed. + /// Called from before the is disposed. internal virtual void OnUnloaded () { foreach (var view in SubViews.Where (v => v is Toplevel)) @@ -246,7 +244,7 @@ public partial class Toplevel : View } // BUGBUG: The && true is a temp hack - if ((superView != top || top?.SuperView is { } || (top != Application.Top && top!.Modal) || (top == Application.Top && top?.SuperView is null)) + if ((superView != top || top?.SuperView is { } || (top != App?.Current && top!.Modal) || (top == App?.Current && top?.SuperView is null)) && (top!.Frame.X + top.Frame.Width > maxWidth || ny > top.Frame.Y)) { diff --git a/Terminal.Gui/Views/ToplevelEventArgs.cs b/Terminal.Gui/Views/ToplevelEventArgs.cs index 0c8d15fc7..52e02d0c8 100644 --- a/Terminal.Gui/Views/ToplevelEventArgs.cs +++ b/Terminal.Gui/Views/ToplevelEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable  namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TreeView/AspectGetterDelegate.cs b/Terminal.Gui/Views/TreeView/AspectGetterDelegate.cs index 4382da300..df04b9047 100644 --- a/Terminal.Gui/Views/TreeView/AspectGetterDelegate.cs +++ b/Terminal.Gui/Views/TreeView/AspectGetterDelegate.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Delegates of this type are used to fetch string representations of user's model objects diff --git a/Terminal.Gui/Views/TreeView/Branch.cs b/Terminal.Gui/Views/TreeView/Branch.cs index 811824dbf..9c48a0a0d 100644 --- a/Terminal.Gui/Views/TreeView/Branch.cs +++ b/Terminal.Gui/Views/TreeView/Branch.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TreeView/DelegateTreeBuilder.cs b/Terminal.Gui/Views/TreeView/DelegateTreeBuilder.cs index 131dff2d6..7bc59d1b0 100644 --- a/Terminal.Gui/Views/TreeView/DelegateTreeBuilder.cs +++ b/Terminal.Gui/Views/TreeView/DelegateTreeBuilder.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Implementation of that uses user defined functions diff --git a/Terminal.Gui/Views/TreeView/DrawTreeViewLineEventArgs.cs b/Terminal.Gui/Views/TreeView/DrawTreeViewLineEventArgs.cs index 1ef543eeb..2b27697d0 100644 --- a/Terminal.Gui/Views/TreeView/DrawTreeViewLineEventArgs.cs +++ b/Terminal.Gui/Views/TreeView/DrawTreeViewLineEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable // This code is based on http://objectlistview.sourceforge.net (GPLv3 tree/list controls // by phillip.piper@gmail.com). Phillip has explicitly granted permission for his design // and code to be used in this library under the MIT license. diff --git a/Terminal.Gui/Views/TreeView/ITreeBuilder.cs b/Terminal.Gui/Views/TreeView/ITreeBuilder.cs index f03129525..c4c4be1dd 100644 --- a/Terminal.Gui/Views/TreeView/ITreeBuilder.cs +++ b/Terminal.Gui/Views/TreeView/ITreeBuilder.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/TreeView/ITreeViewFilter.cs b/Terminal.Gui/Views/TreeView/ITreeViewFilter.cs index dde9a3a98..3285d6ed1 100644 --- a/Terminal.Gui/Views/TreeView/ITreeViewFilter.cs +++ b/Terminal.Gui/Views/TreeView/ITreeViewFilter.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Provides filtering for a . diff --git a/Terminal.Gui/Views/TreeView/ObjectActivatedEventArgs.cs b/Terminal.Gui/Views/TreeView/ObjectActivatedEventArgs.cs index 195f62945..ac9685342 100644 --- a/Terminal.Gui/Views/TreeView/ObjectActivatedEventArgs.cs +++ b/Terminal.Gui/Views/TreeView/ObjectActivatedEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Event args for the event diff --git a/Terminal.Gui/Views/TreeView/SelectionChangedEventArgs.cs b/Terminal.Gui/Views/TreeView/SelectionChangedEventArgs.cs index 4b9eb93b9..d0efe0392 100644 --- a/Terminal.Gui/Views/TreeView/SelectionChangedEventArgs.cs +++ b/Terminal.Gui/Views/TreeView/SelectionChangedEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Event arguments describing a change in selected object in a tree view diff --git a/Terminal.Gui/Views/TreeView/TreeBuilder.cs b/Terminal.Gui/Views/TreeView/TreeBuilder.cs index afb7f5e18..f17e5a92c 100644 --- a/Terminal.Gui/Views/TreeView/TreeBuilder.cs +++ b/Terminal.Gui/Views/TreeView/TreeBuilder.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// Abstract implementation of . diff --git a/Terminal.Gui/Views/TreeView/TreeNode.cs b/Terminal.Gui/Views/TreeView/TreeNode.cs index 3377b92ac..750c1792b 100644 --- a/Terminal.Gui/Views/TreeView/TreeNode.cs +++ b/Terminal.Gui/Views/TreeView/TreeNode.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/TreeView/TreeNodeBuilder.cs b/Terminal.Gui/Views/TreeView/TreeNodeBuilder.cs index 08dc93108..f09775a53 100644 --- a/Terminal.Gui/Views/TreeView/TreeNodeBuilder.cs +++ b/Terminal.Gui/Views/TreeView/TreeNodeBuilder.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// implementation for objects diff --git a/Terminal.Gui/Views/TreeView/TreeStyle.cs b/Terminal.Gui/Views/TreeView/TreeStyle.cs index fe5eea7c4..14eb193f9 100644 --- a/Terminal.Gui/Views/TreeView/TreeStyle.cs +++ b/Terminal.Gui/Views/TreeView/TreeStyle.cs @@ -1,3 +1,4 @@ +#nullable disable  namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TreeView/TreeView.cs b/Terminal.Gui/Views/TreeView/TreeView.cs index c640b4e25..fd33ad0ff 100644 --- a/Terminal.Gui/Views/TreeView/TreeView.cs +++ b/Terminal.Gui/Views/TreeView/TreeView.cs @@ -2,6 +2,7 @@ // by phillip.piper@gmail.com). Phillip has explicitly granted permission for his design // and code to be used in this library under the MIT license. +#nullable disable using System.Collections.ObjectModel; namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/TreeView/TreeViewTextFilter.cs b/Terminal.Gui/Views/TreeView/TreeViewTextFilter.cs index 4be43e00a..f79e6e674 100644 --- a/Terminal.Gui/Views/TreeView/TreeViewTextFilter.cs +++ b/Terminal.Gui/Views/TreeView/TreeViewTextFilter.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/View.cs b/Terminal.Gui/Views/View.cs deleted file mode 100644 index 0519ecba6..000000000 --- a/Terminal.Gui/Views/View.cs +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Terminal.Gui/Views/Window.cs b/Terminal.Gui/Views/Window.cs index f2b51ce40..b374459a2 100644 --- a/Terminal.Gui/Views/Window.cs +++ b/Terminal.Gui/Views/Window.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/Wizard/Wizard.cs b/Terminal.Gui/Views/Wizard/Wizard.cs index a9b011451..fe3cfcc63 100644 --- a/Terminal.Gui/Views/Wizard/Wizard.cs +++ b/Terminal.Gui/Views/Wizard/Wizard.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace Terminal.Gui.Views; /// @@ -44,7 +42,7 @@ namespace Terminal.Gui.Views; /// Application.RequestStop(); /// }; /// -/// Application.Top.Add (wizard); +/// Application.Current.Add (wizard); /// Application.Run (); /// Application.Shutdown (); /// @@ -123,7 +121,7 @@ public class Wizard : Dialog /// /// /// If a non-Modal Wizard is added to the application after - /// has + /// has /// been called the first step must be explicitly set by setting to /// : /// @@ -374,7 +372,7 @@ public class Wizard : Dialog /// /// is derived from and Dialog causes Esc to call - /// , closing the Dialog. Wizard overrides + /// , closing the Dialog. Wizard overrides /// to instead fire the event when Wizard is being used as a /// non-modal (see ). /// diff --git a/Terminal.Gui/Views/Wizard/WizardEventArgs.cs b/Terminal.Gui/Views/Wizard/WizardEventArgs.cs index 85444fd30..4aac63162 100644 --- a/Terminal.Gui/Views/Wizard/WizardEventArgs.cs +++ b/Terminal.Gui/Views/Wizard/WizardEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable namespace Terminal.Gui.Views; /// for transition events. diff --git a/Terminal.Gui/Views/Wizard/WizardStep.cs b/Terminal.Gui/Views/Wizard/WizardStep.cs index 6e211b1e8..56dd09a94 100644 --- a/Terminal.Gui/Views/Wizard/WizardStep.cs +++ b/Terminal.Gui/Views/Wizard/WizardStep.cs @@ -1,4 +1,4 @@ -#nullable enable + namespace Terminal.Gui.Views; diff --git a/Terminal.sln b/Terminal.sln index 983bfdd29..050122e35 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -27,6 +27,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Settings", "Settings", "{B8 nuget.config = nuget.config Terminal.sln.DotSettings = Terminal.sln.DotSettings testenvironments.json = testenvironments.json + docfx\schemas\tui-config-schema.json = docfx\schemas\tui-config-schema.json EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{13BB2C46-B324-4B9C-92EB-CE6184D4736E}" @@ -45,11 +46,43 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{13BB2C EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{C7A51224-5E0F-4986-AB37-A6BF89966C12}" ProjectSection(SolutionItems) = preProject + docfx\docs\ansiparser.md = docfx\docs\ansiparser.md + docfx\docs\application.md = docfx\docs\application.md + docfx\docs\arrangement.md = docfx\docs\arrangement.md + docfx\docs\cancellable-work-pattern.md = docfx\docs\cancellable-work-pattern.md + docfx\docs\CharacterMap.md = docfx\docs\CharacterMap.md .github\CODEOWNERS = .github\CODEOWNERS CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md + docfx\docs\command.md = docfx\docs\command.md + docfx\docs\config.md = docfx\docs\config.md CONTRIBUTING.md = CONTRIBUTING.md .github\copilot-instructions.md = .github\copilot-instructions.md + docfx\docs\cursor.md = docfx\docs\cursor.md + docfx\docs\dimauto.md = docfx\docs\dimauto.md + docfx\docs\drawing.md = docfx\docs\drawing.md + docfx\docs\drivers.md = docfx\docs\drivers.md + docfx\docs\events.md = docfx\docs\events.md + docfx\docs\getting-started.md = docfx\docs\getting-started.md + docfx\docs\index.md = docfx\docs\index.md + docfx\docs\keyboard.md = docfx\docs\keyboard.md + docfx\docs\layout.md = docfx\docs\layout.md + docfx\docs\lexicon.md = docfx\docs\lexicon.md + docfx\docs\logging.md = docfx\docs\logging.md + docfx\docs\migratingfromv1.md = docfx\docs\migratingfromv1.md + docfx\docs\mouse.md = docfx\docs\mouse.md + docfx\docs\multitasking.md = docfx\docs\multitasking.md + docfx\docs\navigation.md = docfx\docs\navigation.md + docfx\docs\newinv2.md = docfx\docs\newinv2.md + docfx\docs\Popovers.md = docfx\docs\Popovers.md README.md = README.md + docfx\docs\scheme.md = docfx\docs\scheme.md + docfx\docs\scrolling.md = docfx\docs\scrolling.md + docfx\docs\showcase.md = docfx\docs\showcase.md + docfx\docs\tableview.md = docfx\docs\tableview.md + docfx\docs\toc.yml = docfx\docs\toc.yml + docfx\docs\treeview.md = docfx\docs\treeview.md + docfx\docs\View.md = docfx\docs\View.md + docfx\docs\views.md = docfx\docs\views.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SelfContained", "Examples\SelfContained\SelfContained.csproj", "{524DEA78-7E7C-474D-B42D-52ED4C04FF14}" @@ -163,10 +196,6 @@ Global {8C643A64-2A77-4432-987A-2E72BD9708E3}.Debug|Any CPU.Build.0 = Debug|Any CPU {8C643A64-2A77-4432-987A-2E72BD9708E3}.Release|Any CPU.ActiveCfg = Release|Any CPU {8C643A64-2A77-4432-987A-2E72BD9708E3}.Release|Any CPU.Build.0 = Release|Any CPU - {566AFB59-FF8C-FFF4-C1F4-049B6246E4A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {566AFB59-FF8C-FFF4-C1F4-049B6246E4A7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {566AFB59-FF8C-FFF4-C1F4-049B6246E4A7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {566AFB59-FF8C-FFF4-C1F4-049B6246E4A7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs index 770078bb3..6cc94ec45 100644 --- a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs @@ -191,7 +191,7 @@ public class FileDialogFluentTests SaveDialog? sd = null; MockFileSystem? fs = null; using var c = With.A (() => NewSaveDialog (out sd, out fs, modal: false), 100, 20, d) - .Then (() => sd!.Style.PreserveFilenameOnDirectoryChanges = true) + .Then ((_) => sd!.Style.PreserveFilenameOnDirectoryChanges = true) .ScreenShot ("Save dialog", _out) .AssertTrue (sd!.Canceled) .Focus (_ => true) @@ -230,7 +230,7 @@ public class FileDialogFluentTests SaveDialog? sd = null; MockFileSystem? fs = null; using var c = With.A (() => NewSaveDialog (out sd, out fs, modal: false), 100, 20, d) - .Then (() => sd!.Style.PreserveFilenameOnDirectoryChanges = false) + .Then ((_) => sd!.Style.PreserveFilenameOnDirectoryChanges = false) .ScreenShot ("Save dialog", _out) .AssertTrue (sd!.Canceled) .Focus (_ => true) @@ -267,7 +267,7 @@ public class FileDialogFluentTests SaveDialog? sd = null; MockFileSystem? fs = null; using var c = With.A (() => NewSaveDialog (out sd, out fs, modal: false), 100, 20, d) - .Then (() => sd!.Style.PreserveFilenameOnDirectoryChanges = preserve) + .Then ((_) => sd!.Style.PreserveFilenameOnDirectoryChanges = preserve) .ScreenShot ("Save dialog", _out) .AssertTrue (sd!.Canceled) .Focus (_ => true) diff --git a/Tests/IntegrationTests/FluentTests/GuiTestContextKeyEventTests.cs b/Tests/IntegrationTests/FluentTests/GuiTestContextKeyEventTests.cs index 9c7c9b0aa..51097c74c 100644 --- a/Tests/IntegrationTests/FluentTests/GuiTestContextKeyEventTests.cs +++ b/Tests/IntegrationTests/FluentTests/GuiTestContextKeyEventTests.cs @@ -16,10 +16,10 @@ public class GuiTestContextKeyEventTests (ITestOutputHelper outputHelper) public void QuitKey_ViaApplication_Stops (TestDriver d) { using GuiTestContext context = With.A (40, 10, d); - Assert.True (Application.Top!.Running); + Assert.True (context.App?.Current!.Running); - Toplevel top = Application.Top; - context.Then (() => Application.RaiseKeyDownEvent (Application.QuitKey)); + Toplevel? top = context.App?.Current; + context.Then ((_) => context!.App?.Keyboard.RaiseKeyDownEvent (Application.QuitKey)); Assert.False (top!.Running); } @@ -28,9 +28,9 @@ public class GuiTestContextKeyEventTests (ITestOutputHelper outputHelper) public void QuitKey_ViaEnqueueKey_Stops (TestDriver d) { using GuiTestContext context = With.A (40, 10, d, _out); - Assert.True (Application.Top!.Running); + Assert.True (context.App?.Current!.Running); - Toplevel top = Application.Top; + Toplevel? top = context.App?.Current; context.EnqueueKeyEvent (Application.QuitKey); Assert.False (top!.Running); @@ -46,7 +46,7 @@ public class GuiTestContextKeyEventTests (ITestOutputHelper outputHelper) using GuiTestContext context = With.A (40, 10, d, _out) .Add (view) - .Then (() => view.SetFocus ()) + .Then ((_) => view.SetFocus ()) .ResizeConsole (50, 20) .EnqueueKeyEvent (Key.A); @@ -62,7 +62,7 @@ public class GuiTestContextKeyEventTests (ITestOutputHelper outputHelper) using GuiTestContext context = With.A (40, 10, d, _out) .Add (textField) .Focus (textField) - .Then (() => textField.CursorPosition = textField.Text.Length) + .Then ((_) => textField.CursorPosition = textField.Text.Length) .EnqueueKeyEvent (Key.Backspace) .EnqueueKeyEvent (Key.Backspace); @@ -81,14 +81,14 @@ public class GuiTestContextKeyEventTests (ITestOutputHelper outputHelper) using GuiTestContext context = With.A (40, 10, d, _out) .Add (textField) .Add (button) - .Then (() => textField.SetFocus ()) + .Then ((_) => textField.SetFocus ()) .EnqueueKeyEvent (Key.T.WithShift) .EnqueueKeyEvent (Key.E) .EnqueueKeyEvent (Key.S) .EnqueueKeyEvent (Key.T) .AssertEqual ("Test", textField.Text) .EnqueueKeyEvent (Key.Tab) - .Then (() => Assert.True (button.HasFocus)) + .Then ((_) => Assert.True (button.HasFocus)) .EnqueueKeyEvent (Key.Enter) .AssertEqual (1, clickedCount); } @@ -110,7 +110,7 @@ public class GuiTestContextKeyEventTests (ITestOutputHelper outputHelper) using GuiTestContext context = With.A (40, 10, d, _out) .Add (view) - .Then (() => view.SetFocus ()) + .Then ((_) => view.SetFocus ()) .EnqueueKeyEvent (Key.A); Assert.True (keyReceived, "Key was not received by the view"); @@ -128,7 +128,7 @@ public class GuiTestContextKeyEventTests (ITestOutputHelper outputHelper) using GuiTestContext context = With.A (40, 10, d, _out) .Add (view) - .Then (() => view.SetFocus ()) + .Then ((_) => view.SetFocus ()) .EnqueueKeyEvent (Key.F1) .EnqueueKeyEvent (Key.F5) .EnqueueKeyEvent (Key.F12); @@ -150,7 +150,7 @@ public class GuiTestContextKeyEventTests (ITestOutputHelper outputHelper) using GuiTestContext context = With.A (40, 10, d, _out) .Add (view) - .Then (() => view.SetFocus ()) + .Then ((_) => view.SetFocus ()) .EnqueueKeyEvent (Key.A) .EnqueueKeyEvent (Key.B) .EnqueueKeyEvent (Key.C); @@ -171,7 +171,7 @@ public class GuiTestContextKeyEventTests (ITestOutputHelper outputHelper) using GuiTestContext context = With.A (40, 10, d, _out) .Add (view1) .Add (view2) - .Then (() => view1.SetFocus ()) + .Then ((_) => view1.SetFocus ()) .AssertTrue (view1.HasFocus) .AssertFalse (view2.HasFocus) .EnqueueKeyEvent (Key.Tab) @@ -190,7 +190,7 @@ public class GuiTestContextKeyEventTests (ITestOutputHelper outputHelper) using GuiTestContext context = With.A (40, 10, d, _out) .Add (textField) - .Then (() => textField.SetFocus ()) + .Then ((_) => textField.SetFocus ()) .EnqueueKeyEvent (Key.D1) .EnqueueKeyEvent (Key.D2) .EnqueueKeyEvent (Key.D3) @@ -210,7 +210,7 @@ public class GuiTestContextKeyEventTests (ITestOutputHelper outputHelper) using GuiTestContext context = With.A (40, 10, d, _out) .Add (view) - .Then (() => view.SetFocus ()); + .Then ((_) => view.SetFocus ()); // Send 10 keys rapidly for (var i = 0; i < 10; i++) @@ -237,7 +237,7 @@ public class GuiTestContextKeyEventTests (ITestOutputHelper outputHelper) using GuiTestContext context = With.A (40, 10, d, _out) .Add (view) - .Then (() => view.SetFocus ()) + .Then ((_) => view.SetFocus ()) .EnqueueKeyEvent (Key.Enter) .EnqueueKeyEvent (Key.Tab) .EnqueueKeyEvent (Key.CursorUp) @@ -266,7 +266,7 @@ public class GuiTestContextKeyEventTests (ITestOutputHelper outputHelper) using GuiTestContext context = With.A (40, 10, d, _out) .Add (listView) - .Then (() => listView.SetFocus ()) + .Then ((_) => listView.SetFocus ()) .AssertEqual (0, listView.SelectedItem) .EnqueueKeyEvent (Key.CursorDown) .AssertEqual (1, listView.SelectedItem) @@ -293,7 +293,7 @@ public class GuiTestContextKeyEventTests (ITestOutputHelper outputHelper) using GuiTestContext context = With.A (40, 10, d, _out) .Add (view) - .Then (() => view.SetFocus ()) + .Then ((_) => view.SetFocus ()) .EnqueueKeyEvent (Key.A.WithCtrl); Assert.True (keyReceived); diff --git a/Tests/IntegrationTests/FluentTests/GuiTestContextMouseEventTests.cs b/Tests/IntegrationTests/FluentTests/GuiTestContextMouseEventTests.cs index a68c17153..073852944 100644 --- a/Tests/IntegrationTests/FluentTests/GuiTestContextMouseEventTests.cs +++ b/Tests/IntegrationTests/FluentTests/GuiTestContextMouseEventTests.cs @@ -152,7 +152,7 @@ public class GuiTestContextMouseEventTests (ITestOutputHelper outputHelper) using GuiTestContext context = With.A (40, 10, d, _out) .Add (view1) .Add (view2) - .Then (() => view1.SetFocus ()) + .Then ((_) => view1.SetFocus ()) .AssertTrue (view1.HasFocus) .LeftClick (25, 7) // Click on view2 .AssertFalse (view1.HasFocus) diff --git a/Tests/IntegrationTests/FluentTests/GuiTestContextTests.cs b/Tests/IntegrationTests/FluentTests/GuiTestContextTests.cs index 91c0fb3dc..30ea80cd8 100644 --- a/Tests/IntegrationTests/FluentTests/GuiTestContextTests.cs +++ b/Tests/IntegrationTests/FluentTests/GuiTestContextTests.cs @@ -43,8 +43,24 @@ public class GuiTestContextTests (ITestOutputHelper outputHelper) public void With_New_A_Runs (TestDriver d) { using GuiTestContext context = With.A (40, 10, d, _out); - Assert.True (Application.Top!.Running); - Assert.NotEqual (Rectangle.Empty, Application.Screen); + Assert.True (context.App!.Current!.Running); + Assert.NotEqual (Rectangle.Empty, context.App!.Screen); + } + + [Theory] + [ClassData (typeof (TestDrivers))] + public void AnsiScreenShot_Renders_Ansi_Stream (TestDriver d) + { + using GuiTestContext context = With.A (10, 3, d, _out) + .Then ((app) => + { + app.Current!.BorderStyle = LineStyle.None; + app.Current!.Border!.Thickness = Thickness.Empty; + app.Current.Text = "hello"; + }) + .ScreenShot ("ScreenShot", _out) + .AnsiScreenShot ("AnsiScreenShot", _out) +; } [Theory] diff --git a/Tests/IntegrationTests/FluentTests/MenuBarv2Tests.cs b/Tests/IntegrationTests/FluentTests/MenuBarv2Tests.cs index 597214929..bc96b6f95 100644 --- a/Tests/IntegrationTests/FluentTests/MenuBarv2Tests.cs +++ b/Tests/IntegrationTests/FluentTests/MenuBarv2Tests.cs @@ -24,7 +24,7 @@ public class MenuBarv2Tests public void Initializes_WithNoItems (TestDriver d) { using GuiTestContext c = With.A (80, 25, d, _out) - .Then (() => + .Then ((_) => { // Create a menu bar with no items var menuBar = new MenuBarv2 (); @@ -42,7 +42,7 @@ public class MenuBarv2Tests MenuBarItemv2 [] menuItems = []; using GuiTestContext c = With.A (80, 25, d, _out) - .Then (() => + .Then ((_) => { // Create items for the menu bar menuItems = @@ -79,7 +79,7 @@ public class MenuBarv2Tests public void AddsItems_WithMenusProperty (TestDriver d) { using GuiTestContext c = With.A (80, 25, d, _out) - .Then (() => + .Then ((_) => { var menuBar = new MenuBarv2 (); @@ -100,7 +100,7 @@ public class MenuBarv2Tests public void ChangesKey_RaisesEvent (TestDriver d) { using GuiTestContext c = With.A (80, 25, d, _out) - .Then (() => + .Then ((_) => { var menuBar = new MenuBarv2 (); @@ -137,12 +137,13 @@ public class MenuBarv2Tests public void DefaultKey_Activates (TestDriver d) { MenuBarv2? menuBar = null; + Toplevel? top = null; using GuiTestContext c = With.A (50, 20, d, _out) - .Then (() => + .Then ((app) => { menuBar = new MenuBarv2 (); - Toplevel top = Application.Top!; + top = app.Current!; top.Add ( new View () @@ -152,16 +153,16 @@ public class MenuBarv2Tests }); menuBar.EnableForDesign (ref top); - Application.Top!.Add (menuBar); + app.Current!.Add (menuBar); }) .WaitIteration () - .AssertIsNotType (Application.Navigation!.GetFocused ()) + .AssertIsNotType (top?.App?.Navigation!.GetFocused ()) .ScreenShot ("MenuBar initial state", _out) .EnqueueKeyEvent (MenuBarv2.DefaultKey) .WaitIteration () .ScreenShot ($"After {MenuBarv2.DefaultKey}", _out) - .AssertEqual ("_New file", Application.Navigation!.GetFocused ()!.Title) - .AssertTrue (Application.Popover?.GetActivePopover () is PopoverMenu) + .AssertEqual ("_New file", top?.App?.Navigation!.GetFocused ()!.Title) + .AssertTrue (top?.App?.Popover?.GetActivePopover () is PopoverMenu) .AssertTrue (menuBar?.IsOpen ()); } @@ -171,12 +172,13 @@ public class MenuBarv2Tests public void DefaultKey_DeActivates (TestDriver d) { MenuBarv2? menuBar = null; - + IApplication? app = null; using GuiTestContext c = With.A (50, 20, d, _out) - .Then (() => + .Then ((a) => { + app = a; menuBar = new MenuBarv2 (); - Toplevel top = Application.Top!; + Toplevel top = app.Current!; top.Add ( new View () @@ -186,26 +188,28 @@ public class MenuBarv2Tests }); menuBar.EnableForDesign (ref top); - Application.Top!.Add (menuBar); + app.Current!.Add (menuBar); }) .WaitIteration () - .AssertIsNotType (Application.Navigation!.GetFocused ()) + .AssertIsNotType (app?.Navigation!.GetFocused ()) .ScreenShot ("MenuBar initial state", _out) .EnqueueKeyEvent (MenuBarv2.DefaultKey) .ScreenShot ($"After {MenuBarv2.DefaultKey}", _out) - .AssertEqual ("_New file", Application.Navigation!.GetFocused ()!.Title) + .AssertEqual ("_New file", app?.Navigation!.GetFocused ()!.Title) .EnqueueKeyEvent (MenuBarv2.DefaultKey) .ScreenShot ($"After {MenuBarv2.DefaultKey}", _out) - .AssertIsNotType (Application.Navigation!.GetFocused ()); + .AssertIsNotType (app?.Navigation!.GetFocused ()); } [Theory] [ClassData (typeof (TestDrivers))] public void ShowHidePopovers (TestDriver d) { + IApplication? app = null; using GuiTestContext c = With.A (80, 25, d, _out) - .Then (() => + .Then ((a) => { + app = a; // Create a menu bar with items that have submenus var fileMenuItem = new MenuBarItemv2 ( "_File", @@ -214,7 +218,7 @@ public class MenuBarv2Tests new MenuItemv2 ("_Save", string.Empty, null) ]); - var menuBar = new MenuBarv2 ([fileMenuItem]); + var menuBar = new MenuBarv2 ([fileMenuItem]) { App = app }; // Initially, no menu should be open Assert.False (menuBar.IsOpen ()); @@ -259,13 +263,13 @@ public class MenuBarv2Tests public void EnableForDesign_CreatesMenuItems (TestDriver d) { using GuiTestContext c = With.A (80, 25, d, _out) - .Then (() => + .Then ((app) => { var menuBar = new MenuBarv2 (); - Application.Top!.Add (menuBar); + app.Current!.Add (menuBar); // Call EnableForDesign - Toplevel top = Application.Top!; + Toplevel top = app.Current!; bool result = menuBar.EnableForDesign (ref top); // Should return true @@ -291,36 +295,38 @@ public class MenuBarv2Tests public void Navigation_Left_Right_Wraps (TestDriver d) { MenuBarv2? menuBar = null; + IApplication? app = null; using GuiTestContext c = With.A (50, 20, d, _out) - .Then (() => + .Then ((a) => { + app = a; menuBar = new MenuBarv2 (); - Toplevel top = Application.Top!; + Toplevel top = app.Current!; menuBar.EnableForDesign (ref top); - Application.Top!.Add (menuBar); + app.Current!.Add (menuBar); }) .WaitIteration () .ScreenShot ("MenuBar initial state", _out) .EnqueueKeyEvent (MenuBarv2.DefaultKey) - .AssertTrue (Application.Popover?.GetActivePopover () is PopoverMenu) + .AssertTrue (app?.Popover?.GetActivePopover () is PopoverMenu) .AssertTrue (menuBar?.IsOpen ()) - .AssertEqual ("_New file", Application.Navigation?.GetFocused ()!.Title) + .AssertEqual ("_New file", app?.Navigation?.GetFocused ()!.Title) .ScreenShot ($"After {MenuBarv2.DefaultKey}", _out) .EnqueueKeyEvent (Key.CursorRight) - .AssertTrue (Application.Popover?.GetActivePopover () is PopoverMenu) + .AssertTrue (app?.Popover?.GetActivePopover () is PopoverMenu) .ScreenShot ("After right arrow", _out) - .AssertEqual ("Cu_t", Application.Navigation?.GetFocused ()!.Title) + .AssertEqual ("Cu_t", app?.Navigation?.GetFocused ()!.Title) .EnqueueKeyEvent (Key.CursorRight) .ScreenShot ("After second right arrow", _out) - .AssertEqual ("_Online Help...", Application.Navigation?.GetFocused ()!.Title) + .AssertEqual ("_Online Help...", app?.Navigation?.GetFocused ()!.Title) .ScreenShot ("After third right arrow", _out) .EnqueueKeyEvent (Key.CursorRight) .ScreenShot ("After fourth right arrow", _out) - .AssertEqual ("_New file", Application.Navigation?.GetFocused ()!.Title) + .AssertEqual ("_New file", app?.Navigation?.GetFocused ()!.Title) .EnqueueKeyEvent (Key.CursorLeft) .ScreenShot ("After left arrow", _out) - .AssertEqual ("_Online Help...", Application.Navigation?.GetFocused ()!.Title); + .AssertEqual ("_Online Help...", app?.Navigation?.GetFocused ()!.Title); } @@ -329,12 +335,14 @@ public class MenuBarv2Tests public void MenuBarItem_With_QuitKey_Open_QuitKey_Restores_Focus_Correctly (TestDriver d) { MenuBarv2? menuBar = null; + IApplication? app = null; using GuiTestContext c = With.A (50, 20, d, _out) - .Then (() => + .Then ((a) => { + app = a; menuBar = new MenuBarv2 (); - Toplevel top = Application.Top!; + Toplevel top = app.Current!; top.Add ( new View () @@ -344,19 +352,19 @@ public class MenuBarv2Tests }); menuBar.EnableForDesign (ref top); - Application.Top!.Add (menuBar); + app.Current!.Add (menuBar); }) - .AssertIsNotType (Application.Navigation!.GetFocused ()) + .AssertIsNotType (app!.Navigation!.GetFocused ()) .ScreenShot ("MenuBar initial state", _out) .EnqueueKeyEvent (MenuBarv2.DefaultKey) - .AssertEqual ("_New file", Application.Navigation!.GetFocused ()!.Title) - .AssertTrue (Application.Popover?.GetActivePopover () is PopoverMenu) + .AssertEqual ("_New file", app.Navigation!.GetFocused ()!.Title) + .AssertTrue (app?.Popover?.GetActivePopover () is PopoverMenu) .AssertTrue (menuBar?.IsOpen ()) - .AssertEqual ("_New file", Application.Navigation?.GetFocused ()!.Title) + .AssertEqual ("_New file", app?.Navigation?.GetFocused ()!.Title) .ScreenShot ($"After {MenuBarv2.DefaultKey}", _out) .EnqueueKeyEvent (Application.QuitKey) - .AssertFalse (Application.Popover?.GetActivePopover () is PopoverMenu) - .AssertIsNotType (Application.Navigation!.GetFocused ()); + .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) + .AssertIsNotType (app!.Navigation!.GetFocused ()); } [Theory] @@ -364,6 +372,7 @@ public class MenuBarv2Tests public void MenuBarItem_Without_QuitKey_Open_QuitKey_Restores_Focus_Correctly (TestDriver d) { MenuBarv2? menuBar = null; + IApplication? app = null; using GuiTestContext c = With.A (50, 20, d, _out) .Add ( @@ -373,26 +382,27 @@ public class MenuBarv2Tests Id = "focusableView", }) - .Then (() => + .Then ((a) => { + app = a; menuBar = new MenuBarv2 (); - Toplevel? toplevel = Application.Top; + Toplevel? toplevel = app.Current; menuBar.EnableForDesign (ref toplevel!); - Application.Top!.Add (menuBar); + app.Current!.Add (menuBar); }) .WaitIteration () - .AssertIsNotType (Application.Navigation!.GetFocused ()) + .AssertIsNotType (app?.Navigation!.GetFocused ()) .ScreenShot ("MenuBar initial state", _out) .EnqueueKeyEvent (MenuBarv2.DefaultKey) .EnqueueKeyEvent (Key.CursorRight) - .AssertEqual ("Cu_t", Application.Navigation!.GetFocused ()!.Title) - .AssertTrue (Application.Popover?.GetActivePopover () is PopoverMenu) + .AssertEqual ("Cu_t", app?.Navigation!.GetFocused ()!.Title) + .AssertTrue (app?.Popover?.GetActivePopover () is PopoverMenu) .AssertTrue (menuBar?.IsOpen ()) - .AssertEqual ("Cu_t", Application.Navigation?.GetFocused ()!.Title) + .AssertEqual ("Cu_t", app?.Navigation?.GetFocused ()!.Title) .ScreenShot ($"After {MenuBarv2.DefaultKey}", _out) .EnqueueKeyEvent (Application.QuitKey) - .AssertFalse (Application.Popover?.GetActivePopover () is PopoverMenu) - .AssertIsNotType (Application.Navigation?.GetFocused ()); + .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) + .AssertIsNotType (app?.Navigation?.GetFocused ()); } [Theory] @@ -400,12 +410,14 @@ public class MenuBarv2Tests public void MenuBarItem_With_QuitKey_Open_QuitKey_Does_Not_Quit_App (TestDriver d) { MenuBarv2? menuBar = null; + IApplication? app = null; using GuiTestContext c = With.A (50, 20, d, _out) - .Then (() => + .Then ((a) => { + app = a; menuBar = new MenuBarv2 (); - Toplevel top = Application.Top!; + Toplevel top = app.Current!; top.Add ( new View () @@ -415,18 +427,18 @@ public class MenuBarv2Tests }); menuBar.EnableForDesign (ref top); - Application.Top!.Add (menuBar); + app.Current!.Add (menuBar); }) .WaitIteration () - .AssertIsNotType (Application.Navigation!.GetFocused ()) + .AssertIsNotType (app!.Navigation!.GetFocused ()) .ScreenShot ("MenuBar initial state", _out) .EnqueueKeyEvent (MenuBarv2.DefaultKey) - .AssertEqual ("_New file", Application.Navigation!.GetFocused ()!.Title) - .AssertTrue (Application.Top!.Running) + .AssertEqual ("_New file", app.Navigation!.GetFocused ()!.Title) + .AssertTrue (app?.Current!.Running) .ScreenShot ($"After {MenuBarv2.DefaultKey}", _out) .EnqueueKeyEvent (Application.QuitKey) - .AssertFalse (Application.Popover?.GetActivePopover () is PopoverMenu) - .AssertTrue (Application.Top!.Running); + .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) + .AssertTrue (app!.Current!.Running); } [Theory] @@ -434,12 +446,14 @@ public class MenuBarv2Tests public void MenuBarItem_Without_QuitKey_Open_QuitKey_Does_Not_Quit_MenuBar_SuperView (TestDriver d) { MenuBarv2? menuBar = null; + IApplication? app = null; using GuiTestContext c = With.A (50, 20, d, _out) - .Then (() => + .Then ((a) => { + app = a; menuBar = new MenuBarv2 (); - Toplevel top = Application.Top!; + Toplevel top = app.Current!; top.Add ( new View () @@ -456,17 +470,17 @@ public class MenuBarv2Tests item.Key = Key.Empty; } - Application.Top!.Add (menuBar); + app.Current!.Add (menuBar); }) .WaitIteration () - .AssertIsNotType (Application.Navigation!.GetFocused ()) + .AssertIsNotType (app?.Navigation!.GetFocused ()) .ScreenShot ("MenuBar initial state", _out) .EnqueueKeyEvent (MenuBarv2.DefaultKey) - .AssertEqual ("_New file", Application.Navigation!.GetFocused ()!.Title) + .AssertEqual ("_New file", app?.Navigation!.GetFocused ()!.Title) .ScreenShot ($"After {MenuBarv2.DefaultKey}", _out) .EnqueueKeyEvent (Application.QuitKey) - .AssertFalse (Application.Popover?.GetActivePopover () is PopoverMenu) - .AssertTrue (Application.Top!.Running); + .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) + .AssertTrue (app?.Current!.Running); } [Theory] @@ -489,12 +503,12 @@ public class MenuBarv2Tests }; using GuiTestContext c = With.A (50, 20, d, _out) - .Then (() => + .Then ((a) => { var menuBar = new MenuBarv2 (); - Toplevel top = Application.Top!; + Toplevel top = a.Current!; menuBar.EnableForDesign (ref top); - Application.Top!.Add (menuBar); + a.Current!.Add (menuBar); }) .Add (testView) .WaitIteration () @@ -523,12 +537,12 @@ public class MenuBarv2Tests }; using GuiTestContext c = With.A (50, 20, d, _out) - .Then (() => + .Then ((a) => { var menuBar = new MenuBarv2 (); - Toplevel top = Application.Top!; + Toplevel top = a.Current!; menuBar.EnableForDesign (ref top); - Application.Top!.Add (menuBar); + a.Current!.Add (menuBar); }) .Add (testView) .WaitIteration () diff --git a/Tests/IntegrationTests/FluentTests/NavigationTests.cs b/Tests/IntegrationTests/FluentTests/NavigationTests.cs index 96883edcc..fe74c59ea 100644 --- a/Tests/IntegrationTests/FluentTests/NavigationTests.cs +++ b/Tests/IntegrationTests/FluentTests/NavigationTests.cs @@ -21,7 +21,7 @@ public class NavigationTests (ITestOutputHelper outputHelper) var v6 = new View { Id = "v6", CanFocus = true }; using GuiTestContext c = With.A (50, 20, d, _out) - .Then (() => + .Then ((app) => { var w1 = new Window { Id = "w1" }; w1.Add (v1, v2); @@ -29,8 +29,8 @@ public class NavigationTests (ITestOutputHelper outputHelper) w2.Add (v3, v4); var w3 = new Window { Id = "w3" }; w3.Add (v5, v6); - Toplevel top = Application.Top!; - Application.Top!.Add (w1, w2, w3); + Toplevel top = app?.Current!; + app?.Current!.Add (w1, w2, w3); }) .AssertTrue (v5.HasFocus) .EnqueueKeyEvent (Key.F6) diff --git a/Tests/IntegrationTests/FluentTests/PopverMenuTests.cs b/Tests/IntegrationTests/FluentTests/PopverMenuTests.cs index d163a455b..83a173944 100644 --- a/Tests/IntegrationTests/FluentTests/PopverMenuTests.cs +++ b/Tests/IntegrationTests/FluentTests/PopverMenuTests.cs @@ -23,13 +23,13 @@ public class PopoverMenuTests public void EnableForDesign_CreatesMenuItems (TestDriver d) { using GuiTestContext c = With.A (80, 25, d) - .Then (() => + .Then ((app) => { PopoverMenu popoverMenu = new (); - Application.Top!.Add (popoverMenu); + app.Current!.Add (popoverMenu); // Call EnableForDesign - Toplevel top = Application.Top; + Toplevel top = app.Current; bool result = popoverMenu.EnableForDesign (ref top); // Should return true @@ -54,13 +54,18 @@ public class PopoverMenuTests { lock (o) { + IApplication? app = null; using GuiTestContext c = With.A (50, 20, d) - .Then (() => + .Then ((a) => { - PopoverMenu popoverMenu = new (); + app = a; + PopoverMenu popoverMenu = new () + { + App = app + }; // Call EnableForDesign - Toplevel top = Application.Top!; + Toplevel top = app.Current!; popoverMenu.EnableForDesign (ref top); var view = new View @@ -71,22 +76,22 @@ public class PopoverMenuTests Id = "focusableView", Text = "View" }; - Application.Top!.Add (view); + app.Current!.Add (view); // EnableForDesign sets to true; undo that popoverMenu.Visible = false; - Application.Popover!.Register (popoverMenu); + app?.Popover!.Register (popoverMenu); view.SetFocus (); }) - .AssertFalse (Application.Popover?.GetActivePopover () is PopoverMenu) - .AssertIsNotType (Application.Navigation!.GetFocused ()) + .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) + .AssertIsNotType (app?.Navigation!.GetFocused ()) .ScreenShot ("PopoverMenu initial state", _out) - .Then (() => Application.Popover!.Show (Application.Popover.Popovers.First ())) + .Then ((_) => app?.Popover!.Show (app?.Popover.Popovers.First ())) .ScreenShot ("After Show", _out) - .AssertTrue (Application.Popover?.GetActivePopover () is PopoverMenu) - .AssertEqual ("Cu_t", Application.Navigation!.GetFocused ()!.Title); + .AssertTrue (app?.Popover?.GetActivePopover () is PopoverMenu) + .AssertEqual ("Cu_t", app?.Navigation!.GetFocused ()!.Title); } } @@ -94,13 +99,18 @@ public class PopoverMenuTests [ClassData (typeof (TestDrivers))] public void QuitKey_Hides (TestDriver d) { + IApplication? app = null; using GuiTestContext c = With.A (50, 20, d) - .Then (() => + .Then ((a) => { - PopoverMenu popoverMenu = new (); + app = a; + PopoverMenu popoverMenu = new () + { + App = app + }; // Call EnableForDesign - Toplevel top = Application.Top!; + Toplevel top = app.Current!; bool result = popoverMenu.EnableForDesign (ref top); var view = new View @@ -111,38 +121,43 @@ public class PopoverMenuTests Id = "focusableView", Text = "View" }; - Application.Top!.Add (view); + app.Current!.Add (view); // EnableForDesign sets to true; undo that popoverMenu.Visible = false; - Application.Popover!.Register (popoverMenu); + app?.Popover!.Register (popoverMenu); view.SetFocus (); }) .ScreenShot ("PopoverMenu initial state", _out) - .AssertFalse (Application.Popover?.GetActivePopover () is PopoverMenu) - .Then (() => Application.Popover!.Show (Application.Popover.Popovers.First ())) + .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) + .Then ((_) => app?.Popover!.Show (app?.Popover.Popovers.First ())) .ScreenShot ("After Show", _out) - .AssertTrue (Application.Popover?.GetActivePopover () is PopoverMenu) + .AssertTrue (app?.Popover?.GetActivePopover () is PopoverMenu) .EnqueueKeyEvent (Application.QuitKey) .ScreenShot ($"After {Application.QuitKey}", _out) - .AssertFalse (Application.Popover!.Popovers.Cast ().FirstOrDefault ()!.Visible) - .AssertNull (Application.Popover!.GetActivePopover ()) - .AssertTrue (Application.Top!.Running); + .AssertFalse (app?.Popover!.Popovers.Cast ().FirstOrDefault ()!.Visible) + .AssertNull (app?.Popover!.GetActivePopover ()) + .AssertTrue (app?.Current!.Running); } [Theory] [ClassData (typeof (TestDrivers))] public void QuitKey_Restores_Focus_Correctly (TestDriver d) { + IApplication? app = null; using GuiTestContext c = With.A (50, 20, d) - .Then (() => + .Then ((a) => { - PopoverMenu popoverMenu = new (); + app = a; + PopoverMenu popoverMenu = new () + { + App = app + }; // Call EnableForDesign - Toplevel top = Application.Top!; + Toplevel top = app.Current!; bool result = popoverMenu.EnableForDesign (ref top); var view = new View @@ -153,39 +168,45 @@ public class PopoverMenuTests Id = "focusableView", Text = "View" }; - Application.Top!.Add (view); + app.Current!.Add (view); // EnableForDesign sets to true; undo that popoverMenu.Visible = false; - Application.Popover!.Register (popoverMenu); + app?.Popover!.Register (popoverMenu); view.SetFocus (); }) .ScreenShot ("PopoverMenu initial state", _out) - .AssertFalse (Application.Popover?.GetActivePopover () is PopoverMenu) - .AssertIsNotType (Application.Navigation!.GetFocused ()) - .Then (() => Application.Popover!.Show (Application.Popover.Popovers.First ())) + .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) + .AssertIsNotType (app?.Navigation!.GetFocused ()) + .Then ((_) => app?.Popover!.Show (app?.Popover.Popovers.First ())) .ScreenShot ("After Show", _out) - .AssertTrue (Application.Popover?.GetActivePopover () is PopoverMenu) - .AssertIsType (Application.Navigation!.GetFocused ()) + .AssertTrue (app?.Popover?.GetActivePopover () is PopoverMenu) + .AssertIsType (app?.Navigation!.GetFocused ()) .EnqueueKeyEvent (Application.QuitKey) .ScreenShot ($"After {Application.QuitKey}", _out) - .AssertFalse (Application.Popover?.GetActivePopover () is PopoverMenu) - .AssertIsNotType (Application.Navigation!.GetFocused ()); + .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) + .AssertIsNotType (app?.Navigation!.GetFocused ()); } [Theory] [ClassData (typeof (TestDrivers))] public void MenuBarItem_With_QuitKey_Open_QuitKey_Does_Not_Quit_App (TestDriver d) { + IApplication? app = null; + using GuiTestContext c = With.A (50, 20, d) - .Then (() => + .Then ((a) => { - PopoverMenu popoverMenu = new (); + app = a; + PopoverMenu popoverMenu = new () + { + App = app + }; // Call EnableForDesign - Toplevel top = Application.Top!; + Toplevel top = app.Current!; bool result = popoverMenu.EnableForDesign (ref top); var view = new View @@ -196,25 +217,25 @@ public class PopoverMenuTests Id = "focusableView", Text = "View" }; - Application.Top!.Add (view); + app.Current!.Add (view); // EnableForDesign sets to true; undo that popoverMenu.Visible = false; - Application.Popover!.Register (popoverMenu); + app?.Popover!.Register (popoverMenu); view.SetFocus (); }) - .AssertIsNotType (Application.Navigation!.GetFocused ()) + .AssertIsNotType (app?.Navigation!.GetFocused ()) .ScreenShot ("PopoverMenu initial state", _out) - .Then (() => Application.Popover!.Show (Application.Popover.Popovers.First ())) + .Then ((_) => app?.Popover!.Show (app?.Popover.Popovers.First ())) .ScreenShot ("PopoverMenu after Show", _out) - .AssertEqual ("Cu_t", Application.Navigation!.GetFocused ()!.Title) - .AssertTrue (Application.Top!.Running) + .AssertEqual ("Cu_t", app?.Navigation!.GetFocused ()!.Title) + .AssertTrue (app?.Current!.Running) .EnqueueKeyEvent (Application.QuitKey) .ScreenShot ($"After {Application.QuitKey}", _out) - .AssertFalse (Application.Popover?.GetActivePopover () is PopoverMenu) - .AssertTrue (Application.Top!.Running); + .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) + .AssertTrue (app?.Current!.Running); } [Theory] @@ -237,13 +258,18 @@ public class PopoverMenuTests } }; + IApplication? app = null; using GuiTestContext c = With.A (50, 20, d) - .Then (() => + .Then ((a) => { - PopoverMenu popoverMenu = new (); - Toplevel top = Application.Top!; + app = a; + PopoverMenu popoverMenu = new () + { + App = app + }; + Toplevel top = app.Current!; popoverMenu.EnableForDesign (ref top); - Application.Popover!.Register (popoverMenu); + app?.Popover!.Register (popoverMenu); }) .Add (testView) .Focus (testView) @@ -271,13 +297,19 @@ public class PopoverMenuTests } }; + IApplication? app = null; + using GuiTestContext c = With.A (50, 20, d) - .Then (() => + .Then ((a) => { - PopoverMenu popoverMenu = new (); - Toplevel top = Application.Top!; + app = a; + PopoverMenu popoverMenu = new () + { + App = app + }; + Toplevel top = app.Current!; popoverMenu.EnableForDesign (ref top); - Application.Popover!.Register (popoverMenu); + app?.Popover!.Register (popoverMenu); }) .Add (testView) .Focus (testView) @@ -305,13 +337,18 @@ public class PopoverMenuTests } }; + IApplication? app = null; using GuiTestContext c = With.A (50, 20, d) - .Then (() => + .Then ((a) => { - PopoverMenu popoverMenu = new (); - Toplevel top = Application.Top!; + app = a; + PopoverMenu popoverMenu = new () + { + App = app + }; + Toplevel top = app.Current!; popoverMenu.EnableForDesign (ref top); - Application.Popover!.Register (popoverMenu); + app?.Popover!.Register (popoverMenu); }) .Add (testView) .EnqueueKeyEvent (Application.QuitKey) @@ -326,24 +363,25 @@ public class PopoverMenuTests MenuItemv2 [] menuItems = [new ("_New File", string.Empty, () => { clicked = true; })]; + IApplication? app = null; using GuiTestContext c = With.A (40, 10, d, _out) - .WithContextMenu (new (menuItems)) + .Then ((a) => app = a) + .WithContextMenu (new (menuItems) { App = app }) .ScreenShot ("Before open menu", _out) // Click in main area inside border .RightClick (1, 1) - .Then (() => + .Then ((_) => { // Test depends on menu having a border - IPopover? popover = Application.Popover!.GetActivePopover (); + IPopover? popover = app?.Popover!.GetActivePopover (); Assert.NotNull (popover); var popoverMenu = popover as PopoverMenu; popoverMenu!.Root!.BorderStyle = LineStyle.Single; }) .ScreenShot ("After open menu", _out) .LeftClick (2, 2) - ; - Assert.True (clicked); + .AssertTrue(clicked); } [Theory] @@ -374,8 +412,11 @@ public class PopoverMenuTests new ("Six", "", null) ]; + IApplication? app = null; + using GuiTestContext c = With.A (40, 10, d) - .WithContextMenu (new (menuItems)) + .Then ((a) => app = a) + .WithContextMenu (new (menuItems) { App = app }) .ScreenShot ("Before open menu", _out) // Click in main area inside border diff --git a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs index 013bd32a5..01863f962 100644 --- a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs @@ -55,7 +55,7 @@ public class TreeViewFluentTests }) .AssertIsAssignableFrom (tv.SelectedObject) .Then ( - () => + (_) => { // Re order root.Children = [bike, car, lorry]; @@ -150,7 +150,7 @@ public class TreeViewFluentTests Assert.Equal (mrE, tv.GetObjectOnRow (8)); }) .Then ( - () => + (_) => { // Re order root.Children = [bike, car, lorry]; diff --git a/Tests/IntegrationTests/UICatalog/ScenarioTests.cs b/Tests/IntegrationTests/UICatalog/ScenarioTests.cs index a7943d4e5..69021ab15 100644 --- a/Tests/IntegrationTests/UICatalog/ScenarioTests.cs +++ b/Tests/IntegrationTests/UICatalog/ScenarioTests.cs @@ -20,8 +20,6 @@ public class ScenarioTests : TestsAllViews private readonly ITestOutputHelper _output; - private object? _timeoutLock; - /// /// This runs through all Scenarios defined in UI Catalog, calling Init, Setup, and Run. /// Should find any Scenarios which crash on load or do not respond to . @@ -37,18 +35,14 @@ public class ScenarioTests : TestsAllViews return; } - Assert.Null (_timeoutLock); - _timeoutLock = new (); - - ConfigurationManager.Disable (true); - - // If a previous test failed, this will ensure that the Application is in a clean state Application.ResetState (true); _output.WriteLine ($"Running Scenario '{scenarioType}'"); Scenario? scenario = null; var scenarioName = string.Empty; - object? timeout = null; + // Do not use Application.AddTimer for out-of-band watchdogs as + // they will be stopped by Shutdown/ResetState. + Timer? watchdogTimer = null; var timeoutFired = false; // Increase timeout for macOS - it's consistently slower @@ -90,14 +84,7 @@ public class ScenarioTests : TestsAllViews iterationHandlerRemoved = true; } - lock (_timeoutLock) - { - if (timeout is { }) - { - Application.RemoveTimeout (timeout); - timeout = null; - } - } + watchdogTimer?.Dispose (); scenario?.Dispose (); scenario = null; @@ -130,10 +117,8 @@ public class ScenarioTests : TestsAllViews Application.Iteration += OnApplicationOnIteration; initialized = true; - lock (_timeoutLock) - { - timeout = Application.AddTimeout (TimeSpan.FromMilliseconds (abortTime), ForceCloseCallback); - } + // Use a System.Threading.Timer for the watchdog to ensure it's not affected by Application.StopAllTimers + watchdogTimer = new Timer (_ => ForceCloseCallback (), null, (int)abortTime, System.Threading.Timeout.Infinite); } else { @@ -144,13 +129,9 @@ public class ScenarioTests : TestsAllViews } // If the scenario doesn't close within abortTime ms, this will force it to quit - bool ForceCloseCallback () + void ForceCloseCallback () { - lock (_timeoutLock) - { - timeoutFired = true; - timeout = null; - } + timeoutFired = true; _output.WriteLine ($"TIMEOUT FIRED for {scenarioName} after {abortTime}ms. Attempting graceful shutdown."); @@ -167,8 +148,6 @@ public class ScenarioTests : TestsAllViews { _output.WriteLine ($"Exception during timeout callback: {ex.Message}"); } - - return false; } void OnApplicationOnIteration (object? s, IterationEventArgs a) @@ -219,7 +198,7 @@ public class ScenarioTests : TestsAllViews List posNames = ["Percent", "AnchorEnd", "Center", "Absolute"]; List dimNames = ["Auto", "Percent", "Fill", "Absolute"]; - Application.Init (null, "fake"); + Application.Init ("fake"); var top = new Toplevel (); diff --git a/Tests/StressTests/ApplicationStressTests.cs b/Tests/StressTests/ApplicationStressTests.cs index d58b521c6..0feadf210 100644 --- a/Tests/StressTests/ApplicationStressTests.cs +++ b/Tests/StressTests/ApplicationStressTests.cs @@ -71,8 +71,8 @@ public class ApplicationStressTests { int tbNow = _tbCounter; - // Wait for Application.Top to be running to ensure timed events can be processed - while (Application.Top is null || Application.Top is { Running: false }) + // Wait for Application.Current to be running to ensure timed events can be processed + while (Application.Current is null || Application.Current is { Running: false }) { Thread.Sleep (1); } diff --git a/Tests/StressTests/ScenariosStressTests.cs b/Tests/StressTests/ScenariosStressTests.cs index 9e047f1ab..fead31b84 100644 --- a/Tests/StressTests/ScenariosStressTests.cs +++ b/Tests/StressTests/ScenariosStressTests.cs @@ -126,7 +126,7 @@ public class ScenariosStressTests void OnApplicationSessionBegun (object? sender, SessionTokenEventArgs e) { - // Get a list of all subviews under Application.Top (and their subviews, etc.) + // Get a list of all subviews under Application.Current (and their subviews, etc.) // and subscribe to their DrawComplete event void SubscribeAllSubViews (View view) { @@ -140,7 +140,7 @@ public class ScenariosStressTests } } - SubscribeAllSubViews (Application.Top!); + SubscribeAllSubViews (Application.Current!); } // If the scenario doesn't close within the abort time, this will force it to quit diff --git a/Tests/TerminalGuiFluentTesting/FakeDriver/FakeApplicationFactory.cs b/Tests/TerminalGuiFluentTesting/FakeDriver/FakeApplicationFactory.cs index 92f644bbb..7e51c3c74 100644 --- a/Tests/TerminalGuiFluentTesting/FakeDriver/FakeApplicationFactory.cs +++ b/Tests/TerminalGuiFluentTesting/FakeDriver/FakeApplicationFactory.cs @@ -21,17 +21,14 @@ public class FakeApplicationFactory FakeOutput output = new (); output.SetSize (80, 25); - IApplication origApp = ApplicationImpl.Instance; - SizeMonitorImpl sizeMonitor = new (output); ApplicationImpl impl = new (new FakeComponentFactory (fakeInput, output, sizeMonitor)); - - ApplicationImpl.ChangeInstance (impl); + ApplicationImpl.SetInstance (impl); // Initialize with a fake driver - impl.Init (null, "fake"); + impl.Init ("fake"); - return new FakeApplicationLifecycle (origApp, hardStopTokenSource); + return new FakeApplicationLifecycle (hardStopTokenSource); } } diff --git a/Tests/TerminalGuiFluentTesting/FakeDriver/FakeApplicationLifecycle.cs b/Tests/TerminalGuiFluentTesting/FakeDriver/FakeApplicationLifecycle.cs index 7bfbac632..5fced9338 100644 --- a/Tests/TerminalGuiFluentTesting/FakeDriver/FakeApplicationLifecycle.cs +++ b/Tests/TerminalGuiFluentTesting/FakeDriver/FakeApplicationLifecycle.cs @@ -5,17 +5,15 @@ namespace Terminal.Gui.Drivers; /// Implements a fake application lifecycle for testing purposes. Cleans up the application on dispose by cancelling /// the provided and shutting down the application. /// -/// /// -internal class FakeApplicationLifecycle (IApplication origApp, CancellationTokenSource hardStop) : IDisposable +internal class FakeApplicationLifecycle (CancellationTokenSource hardStop) : IDisposable { /// public void Dispose () { hardStop.Cancel (); - Application.Top?.Dispose (); + Application.Current?.Dispose (); Application.Shutdown (); - ApplicationImpl.ChangeInstance (origApp); } } diff --git a/Tests/TerminalGuiFluentTesting/GuiTestContext.ContextMenu.cs b/Tests/TerminalGuiFluentTesting/GuiTestContext.ContextMenu.cs index c6f45f510..359fd7a0a 100644 --- a/Tests/TerminalGuiFluentTesting/GuiTestContext.ContextMenu.cs +++ b/Tests/TerminalGuiFluentTesting/GuiTestContext.ContextMenu.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; + #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member namespace TerminalGuiFluentTesting; @@ -12,13 +14,17 @@ public partial class GuiTestContext /// public GuiTestContext WithContextMenu (PopoverMenu? contextMenu) { - LastView.MouseEvent += (s, e) => + if (contextMenu?.App is null) + { + Fail (@"PopoverMenu's must have their App property set."); + } + LastView.MouseEvent += (_, e) => { if (e.Flags.HasFlag (MouseFlags.Button3Clicked)) { // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused // and the context menu is disposed when it is closed. - Application.Popover?.Register (contextMenu); + App.Popover?.Register (contextMenu); contextMenu?.MakeVisible (e.ScreenPosition); } }; diff --git a/Tests/TerminalGuiFluentTesting/GuiTestContext.Input.cs b/Tests/TerminalGuiFluentTesting/GuiTestContext.Input.cs index 2d1021731..6a77c0a72 100644 --- a/Tests/TerminalGuiFluentTesting/GuiTestContext.Input.cs +++ b/Tests/TerminalGuiFluentTesting/GuiTestContext.Input.cs @@ -58,13 +58,13 @@ public partial class GuiTestContext private GuiTestContext EnqueueMouseEvent (MouseEventArgs mouseEvent) { // Enqueue the mouse event - WaitIteration (() => + WaitIteration ((app) => { - if (Application.Driver is { }) + if (app.Driver is { }) { mouseEvent.Position = mouseEvent.ScreenPosition; - Application.Driver.InputProcessor.EnqueueMouseEvent (mouseEvent); + app.Driver.InputProcessor.EnqueueMouseEvent (mouseEvent); } else { @@ -81,7 +81,7 @@ public partial class GuiTestContext { var screen = Point.Empty; - GuiTestContext ctx = WaitIteration (() => + GuiTestContext ctx = WaitIteration ((_) => { TView v = Find (evaluator); screen = v.ViewportToScreen (new Point (0, 0)); @@ -199,10 +199,10 @@ public partial class GuiTestContext // We do this by subscribing to the Driver.KeyDown event and waiting until it is raised. // This prevents the application from missing the key event if we enqueue it and immediately return. bool keyReceived = false; - if (_applicationImpl?.Driver is { }) + if (App?.Driver is { }) { - _applicationImpl.Driver.KeyDown += DriverOnKeyDown; - _applicationImpl.Driver.EnqueueKeyEvent (key); + App.Driver.KeyDown += DriverOnKeyDown; + App.Driver.EnqueueKeyEvent (key); WaitUntil (() => keyReceived); } else @@ -215,7 +215,7 @@ public partial class GuiTestContext void DriverOnKeyDown (object? sender, Key e) { - _applicationImpl.Driver.KeyDown -= DriverOnKeyDown; + App.Driver.KeyDown -= DriverOnKeyDown; keyReceived = true; } diff --git a/Tests/TerminalGuiFluentTesting/GuiTestContext.Navigation.cs b/Tests/TerminalGuiFluentTesting/GuiTestContext.Navigation.cs index 49bb1a9b5..9bd8f5bc9 100644 --- a/Tests/TerminalGuiFluentTesting/GuiTestContext.Navigation.cs +++ b/Tests/TerminalGuiFluentTesting/GuiTestContext.Navigation.cs @@ -40,13 +40,13 @@ public partial class GuiTestContext public GuiTestContext Focus (Func? evaluator = null) where T : View { evaluator ??= _ => true; - Toplevel? t = Application.Top; + Toplevel? t = App?.Current; HashSet seen = new (); if (t == null) { - Fail ("Application.Top was null when trying to set focus"); + Fail ("Application.Current was null when trying to set focus"); return this; } @@ -62,7 +62,7 @@ public partial class GuiTestContext } // No, try tab to the next (or first) - EnqueueKeyEvent (Application.NextTabKey); + EnqueueKeyEvent (Terminal.Gui.App.Application.NextTabKey); WaitIteration (); next = t.MostFocused; diff --git a/Tests/TerminalGuiFluentTesting/GuiTestContext.ViewBase.cs b/Tests/TerminalGuiFluentTesting/GuiTestContext.ViewBase.cs index d8848f080..74eafc77e 100644 --- a/Tests/TerminalGuiFluentTesting/GuiTestContext.ViewBase.cs +++ b/Tests/TerminalGuiFluentTesting/GuiTestContext.ViewBase.cs @@ -12,9 +12,9 @@ public partial class GuiTestContext /// public GuiTestContext Add (View v) { - WaitIteration (() => + WaitIteration ((app) => { - Toplevel top = Application.Top ?? throw new ("Top was null so could not add view"); + Toplevel top = app.Current ?? throw new ("Top was null so could not add view"); top.Add (v); top.Layout (); _lastView = v; @@ -28,15 +28,15 @@ public partial class GuiTestContext /// /// The last view added (e.g. with ) or the root/current top. /// - public View LastView => _lastView ?? Application.Top ?? throw new ("Could not determine which view to add to"); + public View LastView => _lastView ?? App.Current ?? throw new ("Could not determine which view to add to"); private T Find (Func evaluator) where T : View { - Toplevel? t = Application.Top; + Toplevel? t = App.Current; if (t == null) { - Fail ("Application.Top was null when attempting to find view"); + Fail ("App.Current was null when attempting to find view"); } T? f = FindRecursive (t!, evaluator); diff --git a/Tests/TerminalGuiFluentTesting/GuiTestContext.cs b/Tests/TerminalGuiFluentTesting/GuiTestContext.cs index de581408f..73960cd0a 100644 --- a/Tests/TerminalGuiFluentTesting/GuiTestContext.cs +++ b/Tests/TerminalGuiFluentTesting/GuiTestContext.cs @@ -1,5 +1,4 @@ -using System.Collections.Concurrent; -using System.Diagnostics; +using System.Diagnostics; using System.Drawing; using System.Text; using Microsoft.Extensions.Logging; @@ -32,10 +31,15 @@ public partial class GuiTestContext : IDisposable private IOutput? _output; private SizeMonitorImpl? _sizeMonitor; private ApplicationImpl? _applicationImpl; + + /// + /// The IApplication instance that was created. + /// + public IApplication? App => _applicationImpl; + private TestDriver _driverType; // ===== Application State Preservation (for restoration) ===== - private IApplication? _originalApplicationInstance; private ILogger? _originalLogger; // ===== Test Configuration ===== @@ -68,7 +72,7 @@ public partial class GuiTestContext : IDisposable _booting.Release (); // After Init, Application.Screen should be set by the driver - if (Application.Screen == Rectangle.Empty) + if (_applicationImpl?.Screen == Rectangle.Empty) { throw new InvalidOperationException ( "Driver bug: Application.Screen is empty after Init. The driver should set the screen size during Init."); @@ -121,11 +125,11 @@ public partial class GuiTestContext : IDisposable Toplevel t = topLevelBuilder (); t.Closed += (s, e) => { Finished = true; }; - Application.Run (t); // This will block, but it's on a background thread now + App?.Run (t); // This will block, but it's on a background thread now t.Dispose (); Logging.Trace ("Application.Run completed"); - Application.Shutdown (); + App?.Shutdown (); _runCancellationTokenSource.Cancel (); } catch (OperationCanceledException) @@ -163,9 +167,7 @@ public partial class GuiTestContext : IDisposable private void InitializeApplication () { - ApplicationImpl.ChangeInstance (_applicationImpl); - - _applicationImpl?.Init (null, GetDriverName ()); + App?.Init (GetDriverName ()); } @@ -175,7 +177,6 @@ public partial class GuiTestContext : IDisposable private void CommonInit (int width, int height, TestDriver driverType, TimeSpan? timeout) { _timeout = timeout ?? TimeSpan.FromSeconds (10); - _originalApplicationInstance = ApplicationImpl.Instance; _originalLogger = Logging.Logger; _logsSb = new (); _driverType = driverType; @@ -207,38 +208,36 @@ public partial class GuiTestContext : IDisposable // Remove frame limit Application.MaximumIterationsPerSecond = ushort.MaxValue; - //// Only set size if explicitly provided (width and height > 0) - //if (width > 0 && height > 0) - //{ - // _output.SetSize (width, height); - //} - IComponentFactory? cf = null; + _output = new FakeOutput (); + + // Only set size if explicitly provided (width and height > 0) + if (width > 0 && height > 0) + { + _output.SetSize (width, height); + } + // TODO: As each drivers' IInput/IOutput implementations are made testable (e.g. // TODO: safely injectable/mocked), we can expand this switch to use them. switch (driverType) { case TestDriver.DotNet: - _output = new FakeOutput (); _sizeMonitor = new (_output); cf = new FakeComponentFactory (_fakeInput, _output, _sizeMonitor); break; case TestDriver.Windows: - _output = new FakeOutput (); _sizeMonitor = new (_output); cf = new FakeComponentFactory (_fakeInput, _output, _sizeMonitor); break; case TestDriver.Unix: - _output = new FakeOutput (); _sizeMonitor = new (_output); cf = new FakeComponentFactory (_fakeInput, _output, _sizeMonitor); break; case TestDriver.Fake: - _output = new FakeOutput (); _sizeMonitor = new (_output); cf = new FakeComponentFactory (_fakeInput, _output, _sizeMonitor); @@ -278,7 +277,7 @@ public partial class GuiTestContext : IDisposable /// /// /// - public GuiTestContext Then (Action doAction) + public GuiTestContext Then (Action doAction) { try { @@ -302,7 +301,7 @@ public partial class GuiTestContext : IDisposable /// /// /// - public GuiTestContext WaitIteration (Action? action = null) + public GuiTestContext WaitIteration (Action? action = null) { // If application has already exited don't wait! if (Finished || _runCancellationTokenSource.Token.IsCancellationRequested || _fakeInput.ExternalCancellationTokenSource!.Token.IsCancellationRequested) @@ -312,31 +311,34 @@ public partial class GuiTestContext : IDisposable return this; } - if (Thread.CurrentThread.ManagedThreadId == Application.MainThreadId) + if (Thread.CurrentThread.ManagedThreadId == _applicationImpl?.MainThreadId) { throw new NotSupportedException ("Cannot WaitIteration during Invoke"); } Logging.Trace ($"WaitIteration started"); - action ??= () => { }; + if (action is null) + { + action = (app) => { }; + } CancellationTokenSource ctsActionCompleted = new (); - Application.Invoke (() => - { - try - { - action (); + App?.Invoke (app => + { + try + { + action (app); - //Logging.Trace ("Action completed"); - ctsActionCompleted.Cancel (); - } - catch (Exception e) - { - Logging.Warning ($"Action failed with exception: {e}"); - _backgroundException = e; - _fakeInput.ExternalCancellationTokenSource?.Cancel (); - } - }); + //Logging.Trace ("Action completed"); + ctsActionCompleted.Cancel (); + } + catch (Exception e) + { + Logging.Warning ($"Action failed with exception: {e}"); + _backgroundException = e; + _fakeInput.ExternalCancellationTokenSource?.Cancel (); + } + }); // Blocks until either the token or the hardStopToken is cancelled. // With linked tokens, we only need to wait on _runCancellationTokenSource and ctsLocal @@ -384,15 +386,27 @@ public partial class GuiTestContext : IDisposable /// new Width for the console. /// new Height for the console. /// - public GuiTestContext ResizeConsole (int width, int height) { return WaitIteration (() => { Application.Driver!.SetScreenSize (width, height); }); } + public GuiTestContext ResizeConsole (int width, int height) { return WaitIteration ((app) => { app.Driver!.SetScreenSize (width, height); }); } public GuiTestContext ScreenShot (string title, TextWriter? writer) { //Logging.Trace ($"{title}"); - return WaitIteration (() => + return WaitIteration ((app) => { writer?.WriteLine (title + ":"); - var text = Application.ToString (); + var text = app.Driver?.ToString (); + + writer?.WriteLine (text); + }); + } + + public GuiTestContext AnsiScreenShot (string title, TextWriter? writer) + { + //Logging.Trace ($"{title}"); + return WaitIteration ((app) => + { + writer?.WriteLine (title + ":"); + var text = app.Driver?.ToAnsi (); writer?.WriteLine (text); }); @@ -412,7 +426,7 @@ public partial class GuiTestContext : IDisposable { try { - Application.Shutdown (); + App?.Shutdown (); } catch { @@ -425,7 +439,7 @@ public partial class GuiTestContext : IDisposable return this; } - WaitIteration (() => { Application.RequestStop (); }); + WaitIteration ((app) => { app.RequestStop (); }); // Wait for the application to stop, but give it a 1-second timeout const int WAIT_TIMEOUT_MS = 1000; @@ -440,8 +454,8 @@ public partial class GuiTestContext : IDisposable // If this doesn't work there will be test failures as the main loop continues to run during next test. try { - Application.RequestStop (); - Application.Shutdown (); + App?.RequestStop (); + App?.Shutdown (); } catch (Exception ex) { @@ -516,9 +530,8 @@ public partial class GuiTestContext : IDisposable Logging.Trace ("CleanupApplication"); _fakeInput.ExternalCancellationTokenSource = null; - Application.ResetState (true); - ApplicationImpl.ChangeInstance (_originalApplicationInstance); - Logging.Logger = _originalLogger; + App?.ResetState (true); + Logging.Logger = _originalLogger!; Finished = true; Application.MaximumIterationsPerSecond = Application.DefaultMaximumIterationsPerSecond; diff --git a/Tests/UnitTests/Application/Application.NavigationTests.cs b/Tests/UnitTests/Application/Application.NavigationTests.cs index 93ad1ae91..21cafa132 100644 --- a/Tests/UnitTests/Application/Application.NavigationTests.cs +++ b/Tests/UnitTests/Application/Application.NavigationTests.cs @@ -61,9 +61,7 @@ public class ApplicationNavigationTests (ITestOutputHelper output) { var raised = false; - Application.Navigation = new (); - - Application.Navigation.FocusedChanged += ApplicationNavigationOnFocusedChanged; + Application.Navigation!.FocusedChanged += ApplicationNavigationOnFocusedChanged; Application.Navigation.SetFocused (new () { CanFocus = true, HasFocus = true }); @@ -74,8 +72,6 @@ public class ApplicationNavigationTests (ITestOutputHelper output) Application.Navigation.FocusedChanged -= ApplicationNavigationOnFocusedChanged; - Application.Navigation = null; - return; void ApplicationNavigationOnFocusedChanged (object sender, EventArgs e) { raised = true; } @@ -84,12 +80,13 @@ public class ApplicationNavigationTests (ITestOutputHelper output) [Fact] public void GetFocused_Returns_Focused_View () { - Application.Navigation = new (); + IApplication app = Application.Create (); - Application.Top = new () + app.Current = new () { Id = "top", - CanFocus = true + CanFocus = true, + App = app }; var subView1 = new View @@ -103,30 +100,28 @@ public class ApplicationNavigationTests (ITestOutputHelper output) Id = "subView2", CanFocus = true }; - Application.Top.Add (subView1, subView2); - Assert.False (Application.Top.HasFocus); - Application.Top.SetFocus (); + app.Current?.Add (subView1, subView2); + Assert.False (app.Current?.HasFocus); + + app.Current?.SetFocus (); Assert.True (subView1.HasFocus); - Assert.Equal (subView1, Application.Navigation.GetFocused ()); + Assert.Equal (subView1, app.Navigation?.GetFocused ()); - Application.Navigation.AdvanceFocus (NavigationDirection.Forward, null); - Assert.Equal (subView2, Application.Navigation.GetFocused ()); - - Application.Top.Dispose (); - Application.Top = null; - Application.Navigation = null; + app.Navigation?.AdvanceFocus (NavigationDirection.Forward, null); + Assert.Equal (subView2, app.Navigation?.GetFocused ()); } [Fact] public void GetFocused_Returns_Null_If_No_Focused_View () { - Application.Navigation = new (); + IApplication app = Application.Create (); - Application.Top = new () + app.Current = new () { Id = "top", - CanFocus = true + CanFocus = true, + App = app }; var subView1 = new View @@ -135,24 +130,21 @@ public class ApplicationNavigationTests (ITestOutputHelper output) CanFocus = true }; - Application.Top.Add (subView1); - Assert.False (Application.Top.HasFocus); + app!.Current.Add (subView1); + Assert.False (app.Current.HasFocus); - Application.Top.SetFocus (); + app.Current.SetFocus (); Assert.True (subView1.HasFocus); - Assert.Equal (subView1, Application.Navigation.GetFocused ()); + Assert.Equal (subView1, app.Navigation!.GetFocused ()); subView1.HasFocus = false; Assert.False (subView1.HasFocus); - Assert.True (Application.Top.HasFocus); - Assert.Equal (Application.Top, Application.Navigation.GetFocused ()); + Assert.True (app.Current.HasFocus); + Assert.Equal (app.Current, app.Navigation.GetFocused ()); - Application.Top.HasFocus = false; - Assert.False (Application.Top.HasFocus); - Assert.Null (Application.Navigation.GetFocused ()); + app.Current.HasFocus = false; + Assert.False (app.Current.HasFocus); + Assert.Null (app.Navigation.GetFocused ()); - Application.Top.Dispose (); - Application.Top = null; - Application.Navigation = null; } } diff --git a/Tests/UnitTests/Application/ApplicationImplBeginEndTests.cs b/Tests/UnitTests/Application/ApplicationImplBeginEndTests.cs new file mode 100644 index 000000000..da9faad9d --- /dev/null +++ b/Tests/UnitTests/Application/ApplicationImplBeginEndTests.cs @@ -0,0 +1,505 @@ +#nullable enable +using Xunit.Abstractions; + +namespace UnitTests.ApplicationTests; + +/// +/// Comprehensive tests for ApplicationImpl.Begin/End logic that manages Current and SessionStack. +/// These tests ensure the fragile state management logic is robust and catches regressions. +/// Tests work directly with ApplicationImpl instances to avoid global Application state issues. +/// +public class ApplicationImplBeginEndTests +{ + private readonly ITestOutputHelper _output; + + public ApplicationImplBeginEndTests (ITestOutputHelper output) { _output = output; } + + private IApplication NewApplicationImpl () + { + IApplication app = Application.Create (); + + return app; + } + + [Fact] + public void Begin_WithNullToplevel_ThrowsArgumentNullException () + { + IApplication app = NewApplicationImpl (); + + try + { + Assert.Throws (() => app.Begin (null!)); + } + finally + { + app.Shutdown (); + } + } + + [Fact] + public void Begin_SetsCurrent_WhenCurrentIsNull () + { + IApplication app = NewApplicationImpl (); + Toplevel? toplevel = null; + + try + { + toplevel = new (); + Assert.Null (app.Current); + + app.Begin (toplevel); + + Assert.NotNull (app.Current); + Assert.Same (toplevel, app.Current); + Assert.Single (app.SessionStack); + } + finally + { + toplevel?.Dispose (); + app.Shutdown (); + } + } + + [Fact] + public void Begin_PushesToSessionStack () + { + IApplication app = NewApplicationImpl (); + Toplevel? toplevel1 = null; + Toplevel? toplevel2 = null; + + try + { + toplevel1 = new() { Id = "1" }; + toplevel2 = new() { Id = "2" }; + + app.Begin (toplevel1); + Assert.Single (app.SessionStack); + Assert.Same (toplevel1, app.Current); + + app.Begin (toplevel2); + Assert.Equal (2, app.SessionStack.Count); + Assert.Same (toplevel2, app.Current); + } + finally + { + toplevel1?.Dispose (); + toplevel2?.Dispose (); + app.Shutdown (); + } + } + + [Fact] + public void Begin_SetsUniqueToplevelId_WhenIdIsEmpty () + { + IApplication app = NewApplicationImpl (); + Toplevel? toplevel1 = null; + Toplevel? toplevel2 = null; + Toplevel? toplevel3 = null; + + try + { + toplevel1 = new (); + toplevel2 = new (); + toplevel3 = new (); + + Assert.Empty (toplevel1.Id); + Assert.Empty (toplevel2.Id); + Assert.Empty (toplevel3.Id); + + app.Begin (toplevel1); + app.Begin (toplevel2); + app.Begin (toplevel3); + + Assert.NotEmpty (toplevel1.Id); + Assert.NotEmpty (toplevel2.Id); + Assert.NotEmpty (toplevel3.Id); + + // IDs should be unique + Assert.NotEqual (toplevel1.Id, toplevel2.Id); + Assert.NotEqual (toplevel2.Id, toplevel3.Id); + Assert.NotEqual (toplevel1.Id, toplevel3.Id); + } + finally + { + toplevel1?.Dispose (); + toplevel2?.Dispose (); + toplevel3?.Dispose (); + app.Shutdown (); + } + } + + [Fact] + public void End_WithNullSessionToken_ThrowsArgumentNullException () + { + IApplication app = NewApplicationImpl (); + + try + { + Assert.Throws (() => app.End (null!)); + } + finally + { + app.Shutdown (); + } + } + + [Fact] + public void End_PopsSessionStack () + { + IApplication app = NewApplicationImpl (); + Toplevel? toplevel1 = null; + Toplevel? toplevel2 = null; + + try + { + toplevel1 = new() { Id = "1" }; + toplevel2 = new() { Id = "2" }; + + SessionToken token1 = app.Begin (toplevel1); + SessionToken token2 = app.Begin (toplevel2); + + Assert.Equal (2, app.SessionStack.Count); + + app.End (token2); + + Assert.Single (app.SessionStack); + Assert.Same (toplevel1, app.Current); + + app.End (token1); + + Assert.Empty (app.SessionStack); + } + finally + { + toplevel1?.Dispose (); + toplevel2?.Dispose (); + app.Shutdown (); + } + } + + [Fact] + public void End_ThrowsArgumentException_WhenNotBalanced () + { + IApplication app = NewApplicationImpl (); + Toplevel? toplevel1 = null; + Toplevel? toplevel2 = null; + + try + { + toplevel1 = new() { Id = "1" }; + toplevel2 = new() { Id = "2" }; + + SessionToken token1 = app.Begin (toplevel1); + SessionToken token2 = app.Begin (toplevel2); + + // Trying to end token1 when token2 is on top should throw + // NOTE: This throws but has the side effect of popping token2 from the stack + Assert.Throws (() => app.End (token1)); + + // Don't try to clean up with more End calls - the state is now inconsistent + // Let Shutdown/ResetState handle cleanup + } + finally + { + // Dispose toplevels BEFORE Shutdown to satisfy DEBUG_IDISPOSABLE assertions + toplevel1?.Dispose (); + toplevel2?.Dispose (); + + // Shutdown will call ResetState which clears any remaining state + app.Shutdown (); + } + } + + [Fact] + public void End_RestoresCurrentToPreviousToplevel () + { + IApplication app = NewApplicationImpl (); + Toplevel? toplevel1 = null; + Toplevel? toplevel2 = null; + Toplevel? toplevel3 = null; + + try + { + toplevel1 = new() { Id = "1" }; + toplevel2 = new() { Id = "2" }; + toplevel3 = new() { Id = "3" }; + + SessionToken token1 = app.Begin (toplevel1); + SessionToken token2 = app.Begin (toplevel2); + SessionToken token3 = app.Begin (toplevel3); + + Assert.Same (toplevel3, app.Current); + + app.End (token3); + Assert.Same (toplevel2, app.Current); + + app.End (token2); + Assert.Same (toplevel1, app.Current); + + app.End (token1); + } + finally + { + toplevel1?.Dispose (); + toplevel2?.Dispose (); + toplevel3?.Dispose (); + app.Shutdown (); + } + } + + [Fact] + public void MultipleBeginEnd_MaintainsStackIntegrity () + { + IApplication app = NewApplicationImpl (); + List toplevels = new (); + List tokens = new (); + + try + { + // Begin multiple toplevels + for (var i = 0; i < 5; i++) + { + var toplevel = new Toplevel { Id = $"toplevel-{i}" }; + toplevels.Add (toplevel); + tokens.Add (app.Begin (toplevel)); + } + + Assert.Equal (5, app.SessionStack.Count); + Assert.Same (toplevels [4], app.Current); + + // End them in reverse order (LIFO) + for (var i = 4; i >= 0; i--) + { + app.End (tokens [i]); + + if (i > 0) + { + Assert.Equal (i, app.SessionStack.Count); + Assert.Same (toplevels [i - 1], app.Current); + } + else + { + Assert.Empty (app.SessionStack); + } + } + } + finally + { + foreach (Toplevel toplevel in toplevels) + { + toplevel.Dispose (); + } + + app.Shutdown (); + } + } + + [Fact] + public void End_UpdatesCachedSessionTokenToplevel () + { + IApplication app = NewApplicationImpl (); + Toplevel? toplevel = null; + + try + { + toplevel = new (); + + SessionToken token = app.Begin (toplevel); + Assert.Null (app.CachedSessionTokenToplevel); + + app.End (token); + + Assert.Same (toplevel, app.CachedSessionTokenToplevel); + } + finally + { + toplevel?.Dispose (); + app.Shutdown (); + } + } + + [Fact] + public void End_NullsSessionTokenToplevel () + { + IApplication app = NewApplicationImpl (); + Toplevel? toplevel = null; + + try + { + toplevel = new (); + + SessionToken token = app.Begin (toplevel); + Assert.Same (toplevel, token.Toplevel); + + app.End (token); + + Assert.Null (token.Toplevel); + } + finally + { + toplevel?.Dispose (); + app.Shutdown (); + } + } + + [Fact] + public void ResetState_ClearsSessionStack () + { + IApplication app = NewApplicationImpl (); + Toplevel? toplevel1 = null; + Toplevel? toplevel2 = null; + + try + { + toplevel1 = new() { Id = "1" }; + toplevel2 = new() { Id = "2" }; + + app.Begin (toplevel1); + app.Begin (toplevel2); + + Assert.Equal (2, app.SessionStack.Count); + Assert.NotNull (app.Current); + } + finally + { + // Dispose toplevels BEFORE Shutdown to satisfy DEBUG_IDISPOSABLE assertions + toplevel1?.Dispose (); + toplevel2?.Dispose (); + + // Shutdown calls ResetState, which will clear SessionStack and set Current to null + app.Shutdown (); + + // Verify cleanup happened + Assert.Empty (app.SessionStack); + Assert.Null (app.Current); + Assert.Null (app.CachedSessionTokenToplevel); + } + } + + [Fact] + public void ResetState_StopsAllRunningToplevels () + { + IApplication app = NewApplicationImpl (); + Toplevel? toplevel1 = null; + Toplevel? toplevel2 = null; + + try + { + toplevel1 = new() { Id = "1", Running = true }; + toplevel2 = new() { Id = "2", Running = true }; + + app.Begin (toplevel1); + app.Begin (toplevel2); + + Assert.True (toplevel1.Running); + Assert.True (toplevel2.Running); + } + finally + { + // Dispose toplevels BEFORE Shutdown to satisfy DEBUG_IDISPOSABLE assertions + toplevel1?.Dispose (); + toplevel2?.Dispose (); + + // Shutdown calls ResetState, which will stop all running toplevels + app.Shutdown (); + + // Verify toplevels were stopped + Assert.False (toplevel1!.Running); + Assert.False (toplevel2!.Running); + } + } + + [Fact] + public void Begin_ActivatesNewToplevel_WhenCurrentExists () + { + IApplication app = NewApplicationImpl (); + Toplevel? toplevel1 = null; + Toplevel? toplevel2 = null; + + try + { + toplevel1 = new() { Id = "1" }; + toplevel2 = new() { Id = "2" }; + + var toplevel1Deactivated = false; + var toplevel2Activated = false; + + toplevel1.Deactivate += (s, e) => toplevel1Deactivated = true; + toplevel2.Activate += (s, e) => toplevel2Activated = true; + + app.Begin (toplevel1); + app.Begin (toplevel2); + + Assert.True (toplevel1Deactivated); + Assert.True (toplevel2Activated); + Assert.Same (toplevel2, app.Current); + } + finally + { + toplevel1?.Dispose (); + toplevel2?.Dispose (); + app.Shutdown (); + } + } + + [Fact] + public void Begin_DoesNotDuplicateToplevel_WhenIdAlreadyExists () + { + IApplication app = NewApplicationImpl (); + Toplevel? toplevel = null; + + try + { + toplevel = new() { Id = "test-id" }; + + app.Begin (toplevel); + Assert.Single (app.SessionStack); + + // Calling Begin again with same toplevel should not duplicate + app.Begin (toplevel); + Assert.Single (app.SessionStack); + } + finally + { + toplevel?.Dispose (); + app.Shutdown (); + } + } + + [Fact] + public void SessionStack_ContainsAllBegunToplevels () + { + IApplication app = NewApplicationImpl (); + List toplevels = new (); + + try + { + for (var i = 0; i < 10; i++) + { + var toplevel = new Toplevel { Id = $"toplevel-{i}" }; + toplevels.Add (toplevel); + app.Begin (toplevel); + } + + // All toplevels should be in the stack + Assert.Equal (10, app.SessionStack.Count); + + // Verify stack contains all toplevels + List stackList = app.SessionStack.ToList (); + + foreach (Toplevel toplevel in toplevels) + { + Assert.Contains (toplevel, stackList); + } + } + finally + { + foreach (Toplevel toplevel in toplevels) + { + toplevel.Dispose (); + } + + app.Shutdown (); + } + } +} diff --git a/Tests/UnitTests/Application/ApplicationImplTests.cs b/Tests/UnitTests/Application/ApplicationImplTests.cs index d31a254d3..faa4cf7a8 100644 --- a/Tests/UnitTests/Application/ApplicationImplTests.cs +++ b/Tests/UnitTests/Application/ApplicationImplTests.cs @@ -1,7 +1,6 @@ #nullable enable using System.Collections.Concurrent; using Moq; -using TerminalGuiFluentTesting; namespace UnitTests.ApplicationTests; @@ -10,7 +9,7 @@ public class ApplicationImplTests /// /// Crates a new ApplicationImpl instance for testing. The input, output, and size monitor components are mocked. /// - private ApplicationImpl NewMockedApplicationImpl () + private IApplication? NewMockedApplicationImpl () { Mock netInput = new (); SetupRunInputMockMethodToBlock (netInput); @@ -21,132 +20,30 @@ public class ApplicationImplTests Mock consoleOutput = new (); var size = new Size (80, 25); + consoleOutput.Setup (o => o.SetSize (It.IsAny (), It.IsAny ())) - .Callback ((w, h) => size = new Size (w, h)); + .Callback ((w, h) => size = new (w, h)); consoleOutput.Setup (o => o.GetSize ()).Returns (() => size); m.Setup (f => f.CreateOutput ()).Returns (consoleOutput.Object); m.Setup (f => f.CreateSizeMonitor (It.IsAny (), It.IsAny ())).Returns (Mock.Of ()); - return new (m.Object); + return new ApplicationImpl (m.Object); } [Fact] public void Init_CreatesKeybindings () { - IApplication orig = ApplicationImpl.Instance; + IApplication? app = NewMockedApplicationImpl (); - ApplicationImpl v2 = NewMockedApplicationImpl (); - ApplicationImpl.ChangeInstance (v2); + app?.Keyboard.KeyBindings.Clear (); - Application.KeyBindings.Clear (); + Assert.Empty (app?.Keyboard?.KeyBindings.GetBindings ()!); - Assert.Empty (Application.KeyBindings.GetBindings ()); + app?.Init ("fake"); - v2.Init (null, "fake"); + Assert.NotEmpty (app?.Keyboard?.KeyBindings.GetBindings ()!); - Assert.NotEmpty (Application.KeyBindings.GetBindings ()); - - v2.Shutdown (); - - ApplicationImpl.ChangeInstance (orig); - } - - /* - [Fact] - public void Init_ExplicitlyRequestWin () - { - var orig = ApplicationImpl.Instance; - - Assert.Null (Application.Driver); - var netInput = new Mock (MockBehavior.Strict); - var netOutput = new Mock (MockBehavior.Strict); - var winInput = new Mock (MockBehavior.Strict); - var winOutput = new Mock (MockBehavior.Strict); - - winInput.Setup (i => i.Initialize (It.IsAny> ())) - .Verifiable (Times.Once); - SetupRunInputMockMethodToBlock (winInput); - winInput.Setup (i => i.Dispose ()) - .Verifiable (Times.Once); - winOutput.Setup (i => i.Dispose ()) - .Verifiable (Times.Once); - - var v2 = new ApplicationV2 ( - () => netInput.Object, - () => netOutput.Object, - () => winInput.Object, - () => winOutput.Object); - ApplicationImpl.ChangeInstance (v2); - - Assert.Null (Application.Driver); - v2.Init (null, "v2win"); - Assert.NotNull (Application.Driver); - - var type = Application.Driver.GetType (); - Assert.True (type.IsGenericType); - Assert.True (type.GetGenericTypeDefinition () == typeof (ConsoleDriverFacade<>)); - v2.Shutdown (); - - Assert.Null (Application.Driver); - - winInput.VerifyAll (); - - ApplicationImpl.ChangeInstance (orig); - } - - [Fact] - public void Init_ExplicitlyRequestNet () - { - var orig = ApplicationImpl.Instance; - - var netInput = new Mock (MockBehavior.Strict); - var netOutput = new Mock (MockBehavior.Strict); - var winInput = new Mock (MockBehavior.Strict); - var winOutput = new Mock (MockBehavior.Strict); - - netInput.Setup (i => i.Initialize (It.IsAny> ())) - .Verifiable (Times.Once); - SetupRunInputMockMethodToBlock (netInput); - netInput.Setup (i => i.Dispose ()) - .Verifiable (Times.Once); - netOutput.Setup (i => i.Dispose ()) - .Verifiable (Times.Once); - var v2 = new ApplicationV2 ( - () => netInput.Object, - () => netOutput.Object, - () => winInput.Object, - () => winOutput.Object); - ApplicationImpl.ChangeInstance (v2); - - Assert.Null (Application.Driver); - v2.Init (null, "v2net"); - Assert.NotNull (Application.Driver); - - var type = Application.Driver.GetType (); - Assert.True (type.IsGenericType); - Assert.True (type.GetGenericTypeDefinition () == typeof (ConsoleDriverFacade<>)); - v2.Shutdown (); - - Assert.Null (Application.Driver); - - netInput.VerifyAll (); - - ApplicationImpl.ChangeInstance (orig); - } -*/ - private void SetupRunInputMockMethodToBlock (Mock> winInput) - { - winInput.Setup (r => r.Run (It.IsAny ())) - .Callback (token => - { - // Simulate an infinite loop that checks for cancellation - while (!token.IsCancellationRequested) - { - // Perform the action that should repeat in the loop - // This could be some mock behavior or just an empty loop depending on the context - } - }) - .Verifiable (Times.Once); + app?.Shutdown (); } private void SetupRunInputMockMethodToBlock (Mock netInput) @@ -167,125 +64,105 @@ public class ApplicationImplTests [Fact] public void NoInitThrowOnRun () { - IApplication orig = ApplicationImpl.Instance; - - Assert.Null (Application.Driver); - ApplicationImpl app = NewMockedApplicationImpl (); - ApplicationImpl.ChangeInstance (app); - - var ex = Assert.Throws (() => app.Run (new Window ())); + IApplication? app = NewMockedApplicationImpl (); + var ex = Assert.Throws (() => app?.Run (new Window ())); Assert.Equal ("Run cannot be accessed before Initialization", ex.Message); - app.Shutdown (); - - ApplicationImpl.ChangeInstance (orig); + app?.Shutdown (); } [Fact] public void InitRunShutdown_Top_Set_To_Null_After_Shutdown () { - IApplication orig = ApplicationImpl.Instance; + IApplication? app = NewMockedApplicationImpl (); - ApplicationImpl v2 = NewMockedApplicationImpl (); - ApplicationImpl.ChangeInstance (v2); + app?.Init ("fake"); - v2.Init (null, "fake"); + object? timeoutToken = app?.AddTimeout ( + TimeSpan.FromMilliseconds (150), + () => + { + if (app.Current is { }) + { + app.RequestStop (); - object timeoutToken = v2.AddTimeout ( - TimeSpan.FromMilliseconds (150), - () => - { - if (Application.Top != null) - { - Application.RequestStop (); + return false; + } - return false; - } - - return false; - } - ); - Assert.Null (Application.Top); + return false; + } + ); + Assert.Null (app?.Current); // Blocks until the timeout call is hit - v2.Run (new Window ()); + app?.Run (new Window ()); // We returned false above, so we should not have to remove the timeout - Assert.False (v2.RemoveTimeout (timeoutToken)); + Assert.False (app?.RemoveTimeout (timeoutToken!)); - Assert.NotNull (Application.Top); - Application.Top?.Dispose (); - v2.Shutdown (); - Assert.Null (Application.Top); - - ApplicationImpl.ChangeInstance (orig); + Assert.NotNull (app?.Current); + app.Current?.Dispose (); + app.Shutdown (); + Assert.Null (app.Current); } [Fact] public void InitRunShutdown_Running_Set_To_False () { - IApplication orig = ApplicationImpl.Instance; + IApplication app = NewMockedApplicationImpl ()!; - ApplicationImpl v2 = NewMockedApplicationImpl (); - ApplicationImpl.ChangeInstance (v2); - - v2.Init (null, "fake"); + app.Init ("fake"); Toplevel top = new Window { Title = "InitRunShutdown_Running_Set_To_False" }; - object timeoutToken = v2.AddTimeout ( - TimeSpan.FromMilliseconds (150), - () => - { - Assert.True (top!.Running); + object timeoutToken = app.AddTimeout ( + TimeSpan.FromMilliseconds (150), + () => + { + Assert.True (top!.Running); - if (Application.Top != null) - { - Application.RequestStop (); + if (app.Current != null) + { + app.RequestStop (); - return false; - } + return false; + } - return false; - } - ); + return false; + } + ); Assert.False (top!.Running); // Blocks until the timeout call is hit - v2.Run (top); + app.Run (top); // We returned false above, so we should not have to remove the timeout - Assert.False (v2.RemoveTimeout (timeoutToken)); + Assert.False (app.RemoveTimeout (timeoutToken)); Assert.False (top!.Running); // BUGBUG: Shutdown sets Top to null, not End. - //Assert.Null (Application.Top); - Application.Top?.Dispose (); - v2.Shutdown (); - - ApplicationImpl.ChangeInstance (orig); + //Assert.Null (Application.Current); + app.Current?.Dispose (); + app.Shutdown (); } - [Fact] public void InitRunShutdown_StopAfterFirstIteration_Stops () { - IApplication orig = ApplicationImpl.Instance; + IApplication app = NewMockedApplicationImpl ()!; - ApplicationImpl v2 = NewMockedApplicationImpl (); - ApplicationImpl.ChangeInstance (v2); + Assert.Null (app.Current); + Assert.Null (app.Driver); - Assert.Null (Application.Top); - Assert.Null (Application.Driver); - - v2.Init (null, "fake"); + app.Init ("fake"); Toplevel top = new Window (); + app.Current = top; var closedCount = 0; @@ -297,45 +174,40 @@ public class ApplicationImplTests top.Unloaded += (_, a) => { unloadedCount++; }; - object timeoutToken = v2.AddTimeout ( - TimeSpan.FromMilliseconds (150), - () => - { - Assert.Fail (@"Didn't stop after first iteration."); - return false; - } - ); + object timeoutToken = app.AddTimeout ( + TimeSpan.FromMilliseconds (150), + () => + { + Assert.Fail (@"Didn't stop after first iteration."); + + return false; + } + ); Assert.Equal (0, closedCount); Assert.Equal (0, unloadedCount); - v2.StopAfterFirstIteration = true; - v2.Run (top); + app.StopAfterFirstIteration = true; + app.Run (top); Assert.Equal (1, closedCount); Assert.Equal (1, unloadedCount); - Application.Top?.Dispose (); - v2.Shutdown (); + app.Current?.Dispose (); + app.Shutdown (); Assert.Equal (1, closedCount); Assert.Equal (1, unloadedCount); - - ApplicationImpl.ChangeInstance (orig); } - [Fact] public void InitRunShutdown_End_Is_Called () { - IApplication orig = ApplicationImpl.Instance; + IApplication app = NewMockedApplicationImpl ()!; - ApplicationImpl v2 = NewMockedApplicationImpl (); - ApplicationImpl.ChangeInstance (v2); + Assert.Null (app.Current); + Assert.Null (app.Driver); - Assert.Null (Application.Top); - Assert.Null (Application.Driver); - - v2.Init (null, "fake"); + app.Init ("fake"); Toplevel top = new Window (); @@ -350,125 +222,110 @@ public class ApplicationImplTests top.Unloaded += (_, a) => { unloadedCount++; }; - object timeoutToken = v2.AddTimeout ( - TimeSpan.FromMilliseconds (150), - () => - { - Assert.True (top!.Running); + object timeoutToken = app.AddTimeout ( + TimeSpan.FromMilliseconds (150), + () => + { + Assert.True (top!.Running); - if (Application.Top != null) - { - Application.RequestStop (); + if (app.Current != null) + { + app.RequestStop (); - return false; - } + return false; + } - return false; - } - ); + return false; + } + ); Assert.Equal (0, closedCount); Assert.Equal (0, unloadedCount); // Blocks until the timeout call is hit - v2.Run (top); + app.Run (top); Assert.Equal (1, closedCount); Assert.Equal (1, unloadedCount); // We returned false above, so we should not have to remove the timeout - Assert.False (v2.RemoveTimeout (timeoutToken)); + Assert.False (app.RemoveTimeout (timeoutToken)); - Application.Top?.Dispose (); - v2.Shutdown (); + app.Current?.Dispose (); + app.Shutdown (); Assert.Equal (1, closedCount); Assert.Equal (1, unloadedCount); - - ApplicationImpl.ChangeInstance (orig); } [Fact] public void InitRunShutdown_QuitKey_Quits () { - IApplication orig = ApplicationImpl.Instance; + IApplication app = NewMockedApplicationImpl ()!; - ApplicationImpl v2 = NewMockedApplicationImpl (); - ApplicationImpl.ChangeInstance (v2); - - v2.Init (null, "fake"); + app.Init ("fake"); Toplevel top = new Window { Title = "InitRunShutdown_QuitKey_Quits" }; - object timeoutToken = v2.AddTimeout ( - TimeSpan.FromMilliseconds (150), - () => - { - Assert.True (top!.Running); + object timeoutToken = app.AddTimeout ( + TimeSpan.FromMilliseconds (150), + () => + { + Assert.True (top!.Running); - if (Application.Top != null) - { - Application.RaiseKeyDownEvent (Application.QuitKey); - } + if (app.Current != null) + { + app.Keyboard.RaiseKeyDownEvent (app.Keyboard.QuitKey); + } - return false; - } - ); + return false; + } + ); Assert.False (top!.Running); // Blocks until the timeout call is hit - v2.Run (top); + app.Run (top); // We returned false above, so we should not have to remove the timeout - Assert.False (v2.RemoveTimeout (timeoutToken)); + Assert.False (app.RemoveTimeout (timeoutToken)); Assert.False (top!.Running); - Assert.NotNull (Application.Top); + Assert.NotNull (app.Current); top.Dispose (); - v2.Shutdown (); - Assert.Null (Application.Top); - - ApplicationImpl.ChangeInstance (orig); + app.Shutdown (); + Assert.Null (app.Current); } [Fact] public void InitRunShutdown_Generic_IdleForExit () { - IApplication orig = ApplicationImpl.Instance; + IApplication app = NewMockedApplicationImpl ()!; - ApplicationImpl v2 = NewMockedApplicationImpl (); - ApplicationImpl.ChangeInstance (v2); + app.Init ("fake"); - v2.Init (null, "fake"); - - v2.AddTimeout (TimeSpan.Zero, IdleExit); - Assert.Null (Application.Top); + app.AddTimeout (TimeSpan.Zero, () => IdleExit (app)); + Assert.Null (app.Current); // Blocks until the timeout call is hit - v2.Run (); + app.Run (); - Assert.NotNull (Application.Top); - Application.Top?.Dispose (); - v2.Shutdown (); - Assert.Null (Application.Top); - - ApplicationImpl.ChangeInstance (orig); + Assert.NotNull (app.Current); + app.Current?.Dispose (); + app.Shutdown (); + Assert.Null (app.Current); } [Fact] public void Shutdown_Closing_Closed_Raised () { - IApplication orig = ApplicationImpl.Instance; + IApplication app = NewMockedApplicationImpl ()!; - ApplicationImpl v2 = NewMockedApplicationImpl (); - ApplicationImpl.ChangeInstance (v2); - - v2.Init (null, "fake"); + app.Init ("fake"); var closing = 0; var closed = 0; @@ -494,69 +351,37 @@ public class ApplicationImplTests Assert.Same (t, a.Toplevel); }; - v2.AddTimeout (TimeSpan.Zero, IdleExit); + app.AddTimeout (TimeSpan.Zero, () => IdleExit (app)); // Blocks until the timeout call is hit - v2.Run (t); + app.Run (t); - Application.Top?.Dispose (); - v2.Shutdown (); - - ApplicationImpl.ChangeInstance (orig); + app.Current?.Dispose (); + app.Shutdown (); Assert.Equal (2, closing); Assert.Equal (1, closed); } - private bool IdleExit () + private bool IdleExit (IApplication app) { - if (Application.Top != null) + if (app.Current != null) { - Application.RequestStop (); + app.RequestStop (); return true; } return true; } - /* - [Fact] - public void Shutdown_Called_Repeatedly_DoNotDuplicateDisposeOutput () - { - var orig = ApplicationImpl.Instance; - - var netInput = new Mock (); - SetupRunInputMockMethodToBlock (netInput); - Mock? outputMock = null; - - - var v2 = new ApplicationV2 ( - () => netInput.Object, - () => (outputMock = new Mock ()).Object, - Mock.Of, - Mock.Of); - ApplicationImpl.ChangeInstance (v2); - - v2.Init (null, "v2net"); - - - v2.Shutdown (); - outputMock!.Verify (o => o.Dispose (), Times.Once); - - ApplicationImpl.ChangeInstance (orig); - } - */ [Fact] public void Open_Calls_ContinueWith_On_UIThread () { - IApplication orig = ApplicationImpl.Instance; + IApplication app = NewMockedApplicationImpl ()!; - ApplicationImpl v2 = NewMockedApplicationImpl (); - ApplicationImpl.ChangeInstance (v2); - - v2.Init (null, "fake"); + app.Init ("fake"); var b = new Button (); var result = false; @@ -569,30 +394,30 @@ public class ApplicationImplTests (t, _) => { // no longer loading - Application.Invoke (() => - { - result = true; - Application.RequestStop (); - }); + app.Invoke (() => + { + result = true; + app.RequestStop (); + }); }, TaskScheduler.FromCurrentSynchronizationContext ()); }; - v2.AddTimeout ( - TimeSpan.FromMilliseconds (150), - () => - { - // Run asynchronous logic inside Task.Run - if (Application.Top != null) - { - b.NewKeyDownEvent (Key.Enter); - b.NewKeyUpEvent (Key.Enter); - } + app.AddTimeout ( + TimeSpan.FromMilliseconds (150), + () => + { + // Run asynchronous logic inside Task.Run + if (app.Current != null) + { + b.NewKeyDownEvent (Key.Enter); + b.NewKeyUpEvent (Key.Enter); + } - return false; - }); + return false; + }); - Assert.Null (Application.Top); + Assert.Null (app.Current); var w = new Window { @@ -601,14 +426,12 @@ public class ApplicationImplTests w.Add (b); // Blocks until the timeout call is hit - v2.Run (w); + app.Run (w); - Assert.NotNull (Application.Top); - Application.Top?.Dispose (); - v2.Shutdown (); - Assert.Null (Application.Top); - - ApplicationImpl.ChangeInstance (orig); + Assert.NotNull (app.Current); + app.Current?.Dispose (); + app.Shutdown (); + Assert.Null (app.Current); Assert.True (result); } @@ -617,47 +440,36 @@ public class ApplicationImplTests public void ApplicationImpl_UsesInstanceFields_NotStaticReferences () { // This test verifies that ApplicationImpl uses instance fields instead of static Application references - IApplication orig = ApplicationImpl.Instance; - - ApplicationImpl v2 = NewMockedApplicationImpl (); - ApplicationImpl.ChangeInstance (v2); + IApplication v2 = NewMockedApplicationImpl ()!; // Before Init, all fields should be null/default Assert.Null (v2.Driver); Assert.False (v2.Initialized); - Assert.Null (v2.Popover); - Assert.Null (v2.Navigation); - Assert.Null (v2.Top); - Assert.Empty (v2.TopLevels); + + //Assert.Null (v2.Popover); + //Assert.Null (v2.Navigation); + Assert.Null (v2.Current); + Assert.Empty (v2.SessionStack); // Init should populate instance fields - v2.Init (null, "fake"); + v2.Init ("fake"); // After Init, Driver, Navigation, and Popover should be populated Assert.NotNull (v2.Driver); Assert.True (v2.Initialized); Assert.NotNull (v2.Popover); Assert.NotNull (v2.Navigation); - Assert.Null (v2.Top); // Top is still null until Run - - // Verify that static Application properties delegate to instance - Assert.Equal (v2.Driver, Application.Driver); - Assert.Equal (v2.Initialized, Application.Initialized); - Assert.Equal (v2.Popover, Application.Popover); - Assert.Equal (v2.Navigation, Application.Navigation); - Assert.Equal (v2.Top, Application.Top); - Assert.Same (v2.TopLevels, Application.TopLevels); + Assert.Null (v2.Current); // Top is still null until Run // Shutdown should clean up instance fields v2.Shutdown (); Assert.Null (v2.Driver); Assert.False (v2.Initialized); - Assert.Null (v2.Popover); - Assert.Null (v2.Navigation); - Assert.Null (v2.Top); - Assert.Empty (v2.TopLevels); - ApplicationImpl.ChangeInstance (orig); + //Assert.Null (v2.Popover); + //Assert.Null (v2.Navigation); + Assert.Null (v2.Current); + Assert.Empty (v2.SessionStack); } } diff --git a/Tests/UnitTests/Application/ApplicationPopoverTests.cs b/Tests/UnitTests/Application/ApplicationPopoverTests.cs index 9cee1ed7c..1ca1944d0 100644 --- a/Tests/UnitTests/Application/ApplicationPopoverTests.cs +++ b/Tests/UnitTests/Application/ApplicationPopoverTests.cs @@ -9,8 +9,7 @@ public class ApplicationPopoverTests try { // Arrange - Assert.Null (Application.Popover); - Application.Init (null, "fake"); + Application.Init ("fake"); // Act Assert.NotNull (Application.Popover); @@ -27,8 +26,8 @@ public class ApplicationPopoverTests try { // Arrange - Assert.Null (Application.Popover); - Application.Init (null, "fake"); + + Application.Init ("fake"); // Act Assert.NotNull (Application.Popover); @@ -36,7 +35,6 @@ public class ApplicationPopoverTests Application.Shutdown (); // Test - Assert.Null (Application.Popover); } finally { @@ -52,12 +50,11 @@ public class ApplicationPopoverTests try { // Arrange - Assert.Null (Application.Popover); - Application.Init (null, "fake"); + Application.Init ("fake"); Assert.NotNull (Application.Popover); Application.StopAfterFirstIteration = true; - top = new Toplevel (); + top = new (); SessionToken rs = Application.Begin (top); // Act @@ -81,15 +78,15 @@ public class ApplicationPopoverTests try { // Arrange - Assert.Null (Application.Popover); - Application.Init (null, "fake"); + Application.Init ("fake"); Application.StopAfterFirstIteration = true; - top = new Toplevel (); + top = new (); SessionToken rs = Application.Begin (top); PopoverTestClass? popover = new (); + Application.Popover?.Register (popover); Application.Popover?.Show (popover); Assert.True (popover.Visible); @@ -116,8 +113,8 @@ public class ApplicationPopoverTests try { // Arrange - Assert.Null (Application.Popover); - Application.Init (null, "fake"); + + Application.Init ("fake"); PopoverTestClass? popover = new (); @@ -140,8 +137,8 @@ public class ApplicationPopoverTests try { // Arrange - Assert.Null (Application.Popover); - Application.Init (null, "fake"); + + Application.Init ("fake"); PopoverTestClass? popover = new (); @@ -169,11 +166,11 @@ public class ApplicationPopoverTests try { // Arrange - Assert.Null (Application.Popover); - Application.Init (null, "fake"); + + Application.Init ("fake"); PopoverTestClass? popover = new (); - + Application.Popover?.Register (popover); Application.Popover?.Show (popover); Application.Popover?.DeRegister (popover); @@ -198,16 +195,16 @@ public class ApplicationPopoverTests try { // Arrange - Assert.Null (Application.Popover); - Application.Init (null, "fake"); - Application.Top = new Toplevel (); + + Application.Init ("fake"); + Application.Current = new (); PopoverTestClass? popover = new (); // Act Application.Popover?.Register (popover); // Assert - Assert.Equal (Application.Top, popover.Toplevel); + Assert.Equal (Application.Current, popover.Current); } finally { @@ -221,23 +218,23 @@ public class ApplicationPopoverTests try { // Arrange - Assert.Null (Application.Popover); - Application.Init (null, "fake"); - Application.Top = new Toplevel () { Id = "initialTop" }; - PopoverTestClass? popover = new (); - int keyDownEvents = 0; + Application.Init ("fake"); + Application.Current = new() { Id = "initialTop" }; + PopoverTestClass? popover = new () { }; + var keyDownEvents = 0; + popover.KeyDown += (s, e) => - { - keyDownEvents++; - e.Handled = true; - }; // Ensure it handles the key + { + keyDownEvents++; + e.Handled = true; + }; // Ensure it handles the key Application.Popover?.Register (popover); // Act Application.RaiseKeyDownEvent (Key.A); // Goes to initialTop - Application.Top = new Toplevel () { Id = "secondaryTop" }; + Application.Current = new() { Id = "secondaryTop" }; Application.RaiseKeyDownEvent (Key.A); // Goes to secondaryTop // Test @@ -268,9 +265,9 @@ public class ApplicationPopoverTests try { // Arrange - Assert.Null (Application.Popover); - Application.Init (null, "fake"); - Application.Top = new () + Application.Init ("fake"); + + Application.Current = new () { Frame = new (0, 0, 10, 10), Id = "top" @@ -282,10 +279,10 @@ public class ApplicationPopoverTests X = 1, Y = 1, Width = 2, - Height = 2, + Height = 2 }; - Application.Top.Add (view); + Application.Current.Add (view); popover = new () { @@ -293,7 +290,7 @@ public class ApplicationPopoverTests X = 5, Y = 5, Width = 3, - Height = 3, + Height = 3 }; // at 5,5 to 8,8 (screen) View? popoverSubView = new () @@ -302,14 +299,15 @@ public class ApplicationPopoverTests X = 1, Y = 1, Width = 1, - Height = 1, + Height = 1 }; popover.Add (popoverSubView); + Application.Popover?.Register (popover); Application.Popover?.Show (popover); - List found = View.GetViewsUnderLocation (new (mouseX, mouseY), ViewportSettingsFlags.TransparentMouse); + List found = view.GetViewsUnderLocation (new (mouseX, mouseY), ViewportSettingsFlags.TransparentMouse); string [] foundIds = found.Select (v => v!.Id).ToArray (); @@ -318,7 +316,7 @@ public class ApplicationPopoverTests finally { popover?.Dispose (); - Application.Top?.Dispose (); + Application.Current?.Dispose (); Application.ResetState (true); } } @@ -361,4 +359,4 @@ public class ApplicationPopoverTests DisposedCount++; } } -} \ No newline at end of file +} diff --git a/Tests/UnitTests/Application/ApplicationScreenTests.cs b/Tests/UnitTests/Application/ApplicationScreenTests.cs index fd4f27be8..9d4185adf 100644 --- a/Tests/UnitTests/Application/ApplicationScreenTests.cs +++ b/Tests/UnitTests/Application/ApplicationScreenTests.cs @@ -15,7 +15,7 @@ public class ApplicationScreenTests { // Arrange Application.ResetState (true); - Application.Init (null, "fake"); + Application.Init ("fake"); // Act Application.ClearScreenNextIteration = true; @@ -46,35 +46,35 @@ public class ApplicationScreenTests Assert.Equal (0, clearedContentsRaised); // Act - Application.Top!.SetNeedsLayout (); + Application.Current!.SetNeedsLayout (); Application.LayoutAndDraw (); // Assert Assert.Equal (0, clearedContentsRaised); // Act - Application.Top.X = 1; + Application.Current.X = 1; Application.LayoutAndDraw (); // Assert Assert.Equal (1, clearedContentsRaised); // Act - Application.Top.Width = 10; + Application.Current.Width = 10; Application.LayoutAndDraw (); // Assert Assert.Equal (2, clearedContentsRaised); // Act - Application.Top.Y = 1; + Application.Current.Y = 1; Application.LayoutAndDraw (); // Assert Assert.Equal (3, clearedContentsRaised); // Act - Application.Top.Height = 10; + Application.Current.Height = 10; Application.LayoutAndDraw (); // Assert diff --git a/Tests/UnitTests/Application/ApplicationTests.cs b/Tests/UnitTests/Application/ApplicationTests.cs index a0909f551..f9230fccd 100644 --- a/Tests/UnitTests/Application/ApplicationTests.cs +++ b/Tests/UnitTests/Application/ApplicationTests.cs @@ -21,120 +21,41 @@ public class ApplicationTests private readonly ITestOutputHelper _output; - private object _timeoutLock; - - [Fact (Skip = "Hangs with SetupFakeApplication")] - [SetupFakeApplication] + [Fact] public void AddTimeout_Fires () { - Assert.Null (_timeoutLock); - _timeoutLock = new (); + IApplication app = Application.Create (); + app.Init ("fake"); - uint timeoutTime = 250; - var initialized = false; - var iteration = 0; - var shutdown = false; - object timeout = null; - var timeoutCount = 0; + uint timeoutTime = 100; + var timeoutFired = false; - Application.InitializedChanged += OnApplicationOnInitializedChanged; + // Setup a timeout that will fire + app.AddTimeout ( + TimeSpan.FromMilliseconds (timeoutTime), + () => + { + timeoutFired = true; - _output.WriteLine ("Application.Run ().Dispose ().."); - Application.Run ().Dispose (); - _output.WriteLine ("Back from Application.Run ().Dispose ()"); + // Return false so the timer does not repeat + return false; + } + ); - Assert.True (initialized); - Assert.False (shutdown); + // The timeout has not fired yet + Assert.False (timeoutFired); - Assert.Equal (1, timeoutCount); - Application.Shutdown (); + // Block the thread to prove the timeout does not fire on a background thread + Thread.Sleep ((int)timeoutTime * 2); + Assert.False (timeoutFired); - Application.InitializedChanged -= OnApplicationOnInitializedChanged; + app.StopAfterFirstIteration = true; + app.Run ().Dispose (); - lock (_timeoutLock) - { - if (timeout is { }) - { - Application.RemoveTimeout (timeout); - timeout = null; - } - } + // The timeout should have fired + Assert.True (timeoutFired); - Assert.True (initialized); - Assert.True (shutdown); - -#if DEBUG_IDISPOSABLE - Assert.Empty (View.Instances); -#endif - lock (_timeoutLock) - { - _timeoutLock = null; - } - - return; - - void OnApplicationOnInitializedChanged (object s, EventArgs a) - { - if (a.Value) - { - Application.Iteration += OnApplicationOnIteration; - initialized = true; - - lock (_timeoutLock) - { - _output.WriteLine ($"Setting timeout for {timeoutTime}ms"); - timeout = Application.AddTimeout (TimeSpan.FromMilliseconds (timeoutTime), TimeoutCallback); - } - } - else - { - Application.Iteration -= OnApplicationOnIteration; - shutdown = true; - } - } - - bool TimeoutCallback () - { - lock (_timeoutLock) - { - _output.WriteLine ($"TimeoutCallback. Count: {++timeoutCount}. Application Iteration: {iteration}"); - - if (timeout is { }) - { - _output.WriteLine (" Nulling timeout."); - timeout = null; - } - } - - // False means "don't re-do timer and remove it" - return false; - } - - void OnApplicationOnIteration (object s, IterationEventArgs a) - { - lock (_timeoutLock) - { - if (timeoutCount > 0) - { - _output.WriteLine ($"Iteration #{iteration} - Timeout fired. Calling Application.RequestStop."); - Application.RequestStop (); - - return; - } - } - - iteration++; - - // Simulate a delay - Thread.Sleep ((int)timeoutTime / 10); - - // Worst case scenario - something went wrong - if (Application.Initialized && iteration > 25) - { - _output.WriteLine ($"Too many iterations ({iteration}): Calling Application.RequestStop."); - Application.RequestStop (); - } - } + app.Shutdown (); } [Fact] @@ -149,13 +70,13 @@ public class ApplicationTests [SetupFakeApplication] public void Begin_Sets_Application_Top_To_Console_Size () { - Assert.Null (Application.Top); + Assert.Null (Application.Current); Application.Driver!.SetScreenSize (80, 25); Toplevel top = new (); Application.Begin (top); - Assert.Equal (new (0, 0, 80, 25), Application.Top!.Frame); + Assert.Equal (new (0, 0, 80, 25), Application.Current!.Frame); Application.Driver!.SetScreenSize (5, 5); - Assert.Equal (new (0, 0, 5, 5), Application.Top!.Frame); + Assert.Equal (new (0, 0, 5, 5), Application.Current!.Frame); top.Dispose (); } @@ -163,21 +84,21 @@ public class ApplicationTests [SetupFakeApplication] public void End_And_Shutdown_Should_Not_Dispose_ApplicationTop () { - Assert.Null (Application.Top); + Assert.Null (Application.Current); SessionToken rs = Application.Begin (new ()); - Application.Top!.Title = "End_And_Shutdown_Should_Not_Dispose_ApplicationTop"; - Assert.Equal (rs.Toplevel, Application.Top); + Application.Current!.Title = "End_And_Shutdown_Should_Not_Dispose_ApplicationTop"; + Assert.Equal (rs.Toplevel, Application.Current); Application.End (rs); #if DEBUG_IDISPOSABLE Assert.True (rs.WasDisposed); - Assert.False (Application.Top!.WasDisposed); // Is true because the rs.Toplevel is the same as Application.Top + Assert.False (Application.Current!.WasDisposed); // Is true because the rs.Toplevel is the same as Application.Current #endif Assert.Null (rs.Toplevel); - Toplevel top = Application.Top; + Toplevel top = Application.Current; #if DEBUG_IDISPOSABLE Exception exception = Record.Exception (Application.Shutdown); @@ -211,12 +132,12 @@ public class ApplicationTests Assert.NotNull (sessionToken); Assert.Equal (rs, sessionToken); - Assert.Equal (topLevel, Application.Top); + Assert.Equal (topLevel, Application.Current); Application.SessionBegun -= newSessionTokenFn; Application.End (sessionToken); - Assert.NotNull (Application.Top); + Assert.NotNull (Application.Current); Assert.NotNull (Application.Driver); topLevel.Dispose (); @@ -240,7 +161,7 @@ public class ApplicationTests Application.QuitKey = Key.Q; Assert.Equal (Key.Q, Application.QuitKey); - Application.Init (null, "fake"); + Application.Init ("fake"); Assert.Equal (Key.Q, Application.QuitKey); } @@ -313,8 +234,6 @@ public class ApplicationTests // Mouse Application.LastMousePosition = new Point (1, 1); - Application.Navigation = new (); - Application.ResetState (); CheckReset (); @@ -327,7 +246,7 @@ public class ApplicationTests // Check that all fields and properties are set to their default values // Public Properties - Assert.Null (Application.Top); + Assert.Null (Application.Current); Assert.Null (Application.Mouse.MouseGrabView); // Don't check Application.ForceDriver @@ -350,7 +269,7 @@ public class ApplicationTests Assert.Equal (Application.GetSupportedCultures (), Application.SupportedCultures); Assert.Equal (Application.GetAvailableCulturesFromEmbeddedResources (), Application.SupportedCultures); Assert.Null (Application.MainThreadId); - Assert.Empty (Application.TopLevels); + Assert.Empty (Application.SessionStack); Assert.Empty (Application.CachedViewsUnderMouse); // Mouse @@ -358,10 +277,10 @@ public class ApplicationTests //Assert.Null (Application._lastMousePosition); // Navigation - Assert.Null (Application.Navigation); + // Assert.Null (Application.Navigation); // Popover - Assert.Null (Application.Popover); + //Assert.Null (Application.Popover); // Events - Can't check //Assert.Null (GetEventSubscribers (typeof (Application), "InitializedChanged")); @@ -380,7 +299,7 @@ public class ApplicationTests // Verify initial state is per spec //Pre_Init_State (); - Application.Init (null, "fake"); + Application.Init ("fake"); // Verify post-Init state is correct //Post_Init_State (); @@ -436,7 +355,7 @@ public class ApplicationTests public void Init_Unbalanced_Throws () { Assert.Throws (() => - Application.Init (null, "fake") + Application.Init ("fake") ); } @@ -445,7 +364,7 @@ public class ApplicationTests public void Init_Unbalanced_Throws2 () { // Now try the other way - Assert.Throws (() => Application.Init (null, "fake")); + Assert.Throws (() => Application.Init ("fake")); } [Fact] @@ -456,7 +375,7 @@ public class ApplicationTests // NOTE: Run, when called after Init has been called behaves differently than // when called if Init has not been called. Toplevel topLevel = new (); - Application.Init (null, "fake"); + Application.Init ("fake"); SessionToken sessionToken = null; @@ -472,18 +391,18 @@ public class ApplicationTests Assert.NotNull (sessionToken); Assert.Equal (rs, sessionToken); - Assert.Equal (topLevel, Application.Top); + Assert.Equal (topLevel, Application.Current); Application.SessionBegun -= newSessionTokenFn; Application.End (sessionToken); - Assert.NotNull (Application.Top); + Assert.NotNull (Application.Current); Assert.NotNull (Application.Driver); topLevel.Dispose (); Application.Shutdown (); - Assert.Null (Application.Top); + Assert.Null (Application.Current); Assert.Null (Application.Driver); } @@ -492,11 +411,11 @@ public class ApplicationTests public void Internal_Properties_Correct () { Assert.True (Application.Initialized); - Assert.Null (Application.Top); + Assert.Null (Application.Current); SessionToken rs = Application.Begin (new ()); - Assert.Equal (Application.Top, rs.Toplevel); + Assert.Equal (Application.Current, rs.Toplevel); Assert.Null (Application.Mouse.MouseGrabView); // public - Application.Top!.Dispose (); + Application.Current!.Dispose (); } // Invoke Tests @@ -509,7 +428,7 @@ public class ApplicationTests SessionToken rs = Application.Begin (top); var actionCalled = 0; - Application.Invoke (() => { actionCalled++; }); + Application.Invoke ((_) => { actionCalled++; }); Application.TimedEvents!.RunTimers (); Assert.Equal (1, actionCalled); top.Dispose (); @@ -520,7 +439,7 @@ public class ApplicationTests { var iteration = 0; - Application.Init (null, "fake"); + Application.Init ("fake"); Application.Iteration += Application_Iteration; Application.Run ().Dispose (); @@ -592,9 +511,9 @@ public class ApplicationTests // Run when already initialized or not with a Driver will not throw (because Window is derived from Toplevel) // Using another type not derived from Toplevel will throws at compile time Application.Run (); - Assert.True (Application.Top is Window); + Assert.True (Application.Current is Window); - Application.Top!.Dispose (); + Application.Current!.Dispose (); } [Fact] @@ -605,15 +524,15 @@ public class ApplicationTests // Run when already initialized or not with a Driver will not throw (because Window is derived from Toplevel) // Using another type not derived from Toplevel will throws at compile time Application.Run (null, "fake"); - Assert.True (Application.Top is Window); + Assert.True (Application.Current is Window); - Application.Top!.Dispose (); + Application.Current!.Dispose (); // Run when already initialized or not with a Driver will not throw (because Dialog is derived from Toplevel) Application.Run (null, "fake"); - Assert.True (Application.Top is Dialog); + Assert.True (Application.Current is Dialog); - Application.Top!.Dispose (); + Application.Current!.Dispose (); Application.Shutdown (); } @@ -621,7 +540,7 @@ public class ApplicationTests [SetupFakeApplication] public void Run_T_After_Init_Does_Not_Disposes_Application_Top () { - // Init doesn't create a Toplevel and assigned it to Application.Top + // Init doesn't create a Toplevel and assigned it to Application.Current // but Begin does var initTop = new Toplevel (); @@ -635,13 +554,13 @@ public class ApplicationTests initTop.Dispose (); Assert.True (initTop.WasDisposed); #endif - Application.Top!.Dispose (); + Application.Current!.Dispose (); return; void OnApplicationOnIteration (object s, IterationEventArgs a) { - Assert.NotEqual (initTop, Application.Top); + Assert.NotEqual (initTop, Application.Current); #if DEBUG_IDISPOSABLE Assert.False (initTop.WasDisposed); #endif @@ -658,7 +577,7 @@ public class ApplicationTests // Init has been called and we're passing no driver to Run. This is ok. Application.Run (); - Application.Top!.Dispose (); + Application.Current!.Dispose (); } [Fact] @@ -670,7 +589,7 @@ public class ApplicationTests // Init has been called, selecting FakeDriver; we're passing no driver to Run. Should be fine. Application.Run (); - Application.Top!.Dispose (); + Application.Current!.Dispose (); } [Fact] @@ -701,7 +620,7 @@ public class ApplicationTests // Init has NOT been called and we're passing a valid driver to Run. This is ok. Application.Run (null, "fake"); - Application.Top!.Dispose (); + Application.Current!.Dispose (); } [Fact] @@ -825,9 +744,9 @@ public class ApplicationTests Assert.NotNull (w); Assert.Equal (string.Empty, w.Title); // Valid - w has not been disposed. The user may want to run it again - Assert.NotNull (Application.Top); - Assert.Equal (w, Application.Top); - Assert.NotEqual (top, Application.Top); + Assert.NotNull (Application.Current); + Assert.Equal (w, Application.Current); + Assert.NotEqual (top, Application.Current); Application.Run (w); // Valid - w has not been disposed. @@ -841,7 +760,7 @@ public class ApplicationTests //exception = Record.Exception ( // () => Application.Run ( - // w)); // Invalid - w has been disposed. Run it in debug mode will throw, otherwise the user may want to run it again + // w)); // Invalid - w has not been disposed. Run it in debug mode will throw, otherwise the user may want to run it again //Assert.NotNull (exception); // TODO: Re-enable this when we are done debug logging of ctx.Source.Title in RaiseSelecting @@ -855,14 +774,14 @@ public class ApplicationTests [Fact] public void Run_Creates_Top_Without_Init () { - Assert.Null (Application.Top); + Assert.Null (Application.Current); Application.StopAfterFirstIteration = true; Application.Iteration += OnApplicationOnIteration; Toplevel top = Application.Run (null, "fake"); Application.Iteration -= OnApplicationOnIteration; #if DEBUG_IDISPOSABLE - Assert.Equal (top, Application.Top); + Assert.Equal (top, Application.Current); Assert.False (top.WasDisposed); Exception exception = Record.Exception (Application.Shutdown); Assert.NotNull (exception); @@ -875,38 +794,38 @@ public class ApplicationTests #if DEBUG_IDISPOSABLE Assert.True (top.WasDisposed); #endif - Assert.NotNull (Application.Top); + Assert.NotNull (Application.Current); Application.Shutdown (); - Assert.Null (Application.Top); + Assert.Null (Application.Current); return; - void OnApplicationOnIteration (object s, IterationEventArgs e) { Assert.NotNull (Application.Top); } + void OnApplicationOnIteration (object s, IterationEventArgs e) { Assert.NotNull (Application.Current); } } [Fact] public void Run_T_Creates_Top_Without_Init () { - Assert.Null (Application.Top); + Assert.Null (Application.Current); Application.StopAfterFirstIteration = true; Application.Run (null, "fake"); #if DEBUG_IDISPOSABLE - Assert.False (Application.Top!.WasDisposed); + Assert.False (Application.Current!.WasDisposed); Exception exception = Record.Exception (Application.Shutdown); Assert.NotNull (exception); - Assert.False (Application.Top!.WasDisposed); + Assert.False (Application.Current!.WasDisposed); // It's up to caller to dispose it - Application.Top!.Dispose (); - Assert.True (Application.Top!.WasDisposed); + Application.Current!.Dispose (); + Assert.True (Application.Current!.WasDisposed); #endif - Assert.NotNull (Application.Top); + Assert.NotNull (Application.Current); Application.Shutdown (); - Assert.Null (Application.Top); + Assert.Null (Application.Current); } [Fact] @@ -920,35 +839,35 @@ public class ApplicationTests // the new(Toplevel) may be a derived class that is possible using Application static // properties that is only available after the Application.Init was called - Assert.Null (Application.Top); + Assert.Null (Application.Current); Assert.Throws (() => Application.Run (new Toplevel ())); - Application.Init (null, "fake"); + Application.Init ("fake"); - Application.Iteration += OnApplicationOnIteration; + Application.Iteration += OnApplication_OnIteration; Application.Run (new Toplevel ()); - Application.Iteration -= OnApplicationOnIteration; + Application.Iteration -= OnApplication_OnIteration; #if DEBUG_IDISPOSABLE - Assert.False (Application.Top!.WasDisposed); + Assert.False (Application.Current!.WasDisposed); Exception exception = Record.Exception (Application.Shutdown); Assert.NotNull (exception); - Assert.False (Application.Top!.WasDisposed); + Assert.False (Application.Current!.WasDisposed); // It's up to caller to dispose it - Application.Top!.Dispose (); - Assert.True (Application.Top!.WasDisposed); + Application.Current!.Dispose (); + Assert.True (Application.Current!.WasDisposed); #endif - Assert.NotNull (Application.Top); + Assert.NotNull (Application.Current); Application.Shutdown (); - Assert.Null (Application.Top); + Assert.Null (Application.Current); return; - void OnApplicationOnIteration (object s, IterationEventArgs e) + void OnApplication_OnIteration (object s, IterationEventArgs e) { - Assert.NotNull (Application.Top); + Assert.NotNull (Application.Current); Application.RequestStop (); } } @@ -960,7 +879,7 @@ public class ApplicationTests public void Run_T_With_V2_Driver_Does_Not_Call_ResetState_After_Init () { Assert.False (Application.Initialized); - Application.Init (null, "fake"); + Application.Init ("fake"); Assert.True (Application.Initialized); Task.Run (() => { Task.Delay (300).Wait (); }) @@ -968,14 +887,14 @@ public class ApplicationTests (t, _) => { // no longer loading - Application.Invoke (() => { Application.RequestStop (); }); + Application.Invoke ((app) => { app.RequestStop (); }); }, TaskScheduler.FromCurrentSynchronizationContext ()); Application.Run (); Assert.NotNull (Application.Driver); - Assert.NotNull (Application.Top); - Assert.False (Application.Top!.Running); - Application.Top!.Dispose (); + Assert.NotNull (Application.Current); + Assert.False (Application.Current!.Running); + Application.Current!.Dispose (); Application.Shutdown (); } diff --git a/Tests/UnitTests/Application/CursorTests.cs b/Tests/UnitTests/Application/CursorTests.cs index 69d472290..bf5cb1247 100644 --- a/Tests/UnitTests/Application/CursorTests.cs +++ b/Tests/UnitTests/Application/CursorTests.cs @@ -1,5 +1,4 @@ -using UnitTests; -using Xunit.Abstractions; +using Xunit.Abstractions; namespace UnitTests.ApplicationTests; @@ -7,22 +6,20 @@ public class CursorTests { private readonly ITestOutputHelper _output; - public CursorTests (ITestOutputHelper output) - { - _output = output; - } + public CursorTests (ITestOutputHelper output) { _output = output; } private class TestView : View { public Point? TestLocation { get; set; } - /// + /// public override Point? PositionCursor () { if (TestLocation.HasValue && HasFocus) { - Driver.SetCursorVisibility (CursorVisibility.Default); + Driver?.SetCursorVisibility (CursorVisibility.Default); } + return TestLocation; } } @@ -31,7 +28,6 @@ public class CursorTests [AutoInitShutdown] public void PositionCursor_No_Focus_Returns_False () { - Application.Navigation = new (); Application.Navigation.SetFocused (null); Assert.False (Application.PositionCursor ()); @@ -40,7 +36,7 @@ public class CursorTests { CanFocus = false, Width = 1, - Height = 1, + Height = 1 }; view.TestLocation = new Point (0, 0); Assert.False (Application.PositionCursor ()); @@ -50,12 +46,11 @@ public class CursorTests [AutoInitShutdown] public void PositionCursor_No_Position_Returns_False () { - Application.Navigation = new (); TestView view = new () { CanFocus = false, Width = 1, - Height = 1, + Height = 1 }; view.CanFocus = true; @@ -67,11 +62,10 @@ public class CursorTests [AutoInitShutdown] public void PositionCursor_No_IntersectSuperView_Returns_False () { - Application.Navigation = new (); View superView = new () { Width = 1, - Height = 1, + Height = 1 }; TestView view = new () @@ -80,7 +74,7 @@ public class CursorTests X = 1, Y = 1, Width = 1, - Height = 1, + Height = 1 }; superView.Add (view); @@ -94,11 +88,10 @@ public class CursorTests [AutoInitShutdown] public void PositionCursor_Position_OutSide_SuperView_Returns_False () { - Application.Navigation = new (); View superView = new () { Width = 1, - Height = 1, + Height = 1 }; TestView view = new () @@ -107,7 +100,7 @@ public class CursorTests X = 0, Y = 0, Width = 2, - Height = 2, + Height = 2 }; superView.Add (view); @@ -121,12 +114,12 @@ public class CursorTests [AutoInitShutdown] public void PositionCursor_Focused_With_Position_Returns_True () { - Application.Navigation = new (); TestView view = new () { CanFocus = false, Width = 1, Height = 1, + App = ApplicationImpl.Instance }; view.CanFocus = true; view.SetFocus (); @@ -138,12 +131,11 @@ public class CursorTests [AutoInitShutdown] public void PositionCursor_Defaults_Invisible () { - Application.Navigation = new (); View view = new () { CanFocus = true, Width = 1, - Height = 1, + Height = 1 }; view.SetFocus (); diff --git a/Tests/UnitTests/Application/MainLoopCoordinatorTests.cs b/Tests/UnitTests/Application/MainLoopCoordinatorTests.cs index 6c7f1d123..b0e1e5708 100644 --- a/Tests/UnitTests/Application/MainLoopCoordinatorTests.cs +++ b/Tests/UnitTests/Application/MainLoopCoordinatorTests.cs @@ -26,7 +26,7 @@ public class MainLoopCoordinatorTests // StartAsync boots the main loop and the input thread. But if the input class bombs // on startup it is important that the exception surface at the call site and not lost - var ex = await Assert.ThrowsAsync(c.StartInputTaskAsync); + var ex = await Assert.ThrowsAsync(() => c.StartInputTaskAsync (null)); Assert.Equal ("Crash on boot", ex.InnerExceptions [0].Message); diff --git a/Tests/UnitTests/Application/Mouse/ApplicationMouseEnterLeaveTests.cs b/Tests/UnitTests/Application/Mouse/ApplicationMouseEnterLeaveTests.cs index cbe800a98..06c3cc4d1 100644 --- a/Tests/UnitTests/Application/Mouse/ApplicationMouseEnterLeaveTests.cs +++ b/Tests/UnitTests/Application/Mouse/ApplicationMouseEnterLeaveTests.cs @@ -41,9 +41,9 @@ public class ApplicationMouseEnterLeaveTests public void RaiseMouseEnterLeaveEvents_MouseEntersView_CallsOnMouseEnter () { // Arrange - Application.Top = new () { Frame = new (0, 0, 10, 10) }; + Application.Current = new () { Frame = new (0, 0, 10, 10) }; var view = new TestView (); - Application.Top.Add (view); + Application.Current.Add (view); var mousePosition = new Point (1, 1); List currentViewsUnderMouse = new () { view }; @@ -66,7 +66,7 @@ public class ApplicationMouseEnterLeaveTests finally { // Cleanup - Application.Top?.Dispose (); + Application.Current?.Dispose (); Application.ResetState (); } } @@ -75,9 +75,9 @@ public class ApplicationMouseEnterLeaveTests public void RaiseMouseEnterLeaveEvents_MouseLeavesView_CallsOnMouseLeave () { // Arrange - Application.Top = new () { Frame = new (0, 0, 10, 10) }; + Application.Current = new () { Frame = new (0, 0, 10, 10) }; var view = new TestView (); - Application.Top.Add (view); + Application.Current.Add (view); var mousePosition = new Point (0, 0); List currentViewsUnderMouse = new (); var mouseEvent = new MouseEventArgs (); @@ -97,7 +97,7 @@ public class ApplicationMouseEnterLeaveTests finally { // Cleanup - Application.Top?.Dispose (); + Application.Current?.Dispose (); Application.ResetState (); } } @@ -106,7 +106,7 @@ public class ApplicationMouseEnterLeaveTests public void RaiseMouseEnterLeaveEvents_MouseMovesBetweenAdjacentViews_CallsOnMouseEnterAndLeave () { // Arrange - Application.Top = new () { Frame = new (0, 0, 10, 10) }; + Application.Current = new () { Frame = new (0, 0, 10, 10) }; var view1 = new TestView (); // at 1,1 to 2,2 var view2 = new TestView () // at 2,2 to 3,3 @@ -114,8 +114,8 @@ public class ApplicationMouseEnterLeaveTests X = 2, Y = 2 }; - Application.Top.Add (view1); - Application.Top.Add (view2); + Application.Current.Add (view1); + Application.Current.Add (view2); Application.CachedViewsUnderMouse.Clear (); @@ -126,7 +126,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (0, view1.OnMouseEnterCalled); @@ -139,7 +139,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -152,7 +152,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -165,7 +165,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -178,7 +178,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -189,7 +189,7 @@ public class ApplicationMouseEnterLeaveTests finally { // Cleanup - Application.Top?.Dispose (); + Application.Current?.Dispose (); Application.ResetState (); } } @@ -198,9 +198,9 @@ public class ApplicationMouseEnterLeaveTests public void RaiseMouseEnterLeaveEvents_NoViewsUnderMouse_DoesNotCallOnMouseEnterOrLeave () { // Arrange - Application.Top = new () { Frame = new (0, 0, 10, 10) }; + Application.Current = new () { Frame = new (0, 0, 10, 10) }; var view = new TestView (); - Application.Top.Add (view); + Application.Current.Add (view); var mousePosition = new Point (0, 0); List currentViewsUnderMouse = new (); var mouseEvent = new MouseEventArgs (); @@ -219,7 +219,7 @@ public class ApplicationMouseEnterLeaveTests finally { // Cleanup - Application.Top?.Dispose (); + Application.Current?.Dispose (); Application.ResetState (); } } @@ -228,7 +228,7 @@ public class ApplicationMouseEnterLeaveTests public void RaiseMouseEnterLeaveEvents_MouseMovesBetweenOverlappingPeerViews_CallsOnMouseEnterAndLeave () { // Arrange - Application.Top = new () { Frame = new (0, 0, 10, 10) }; + Application.Current = new () { Frame = new (0, 0, 10, 10) }; var view1 = new TestView { @@ -241,8 +241,8 @@ public class ApplicationMouseEnterLeaveTests X = 2, Y = 2 }; - Application.Top.Add (view1); - Application.Top.Add (view2); + Application.Current.Add (view1); + Application.Current.Add (view2); Application.CachedViewsUnderMouse.Clear (); @@ -253,7 +253,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (0, view1.OnMouseEnterCalled); @@ -266,7 +266,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -279,7 +279,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -292,7 +292,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -305,7 +305,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -318,7 +318,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -329,7 +329,7 @@ public class ApplicationMouseEnterLeaveTests finally { // Cleanup - Application.Top?.Dispose (); + Application.Current?.Dispose (); Application.ResetState (); } } @@ -338,7 +338,7 @@ public class ApplicationMouseEnterLeaveTests public void RaiseMouseEnterLeaveEvents_MouseMovesBetweenOverlappingSubViews_CallsOnMouseEnterAndLeave () { // Arrange - Application.Top = new () { Frame = new (0, 0, 10, 10) }; + Application.Current = new () { Frame = new (0, 0, 10, 10) }; var view1 = new TestView { @@ -358,7 +358,7 @@ public class ApplicationMouseEnterLeaveTests Arrangement = ViewArrangement.Overlapped }; // at 2,2 to 4,4 (screen) view1.Add (subView); - Application.Top.Add (view1); + Application.Current.Add (view1); Application.CachedViewsUnderMouse.Clear (); @@ -372,7 +372,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (0, view1.OnMouseEnterCalled); @@ -385,7 +385,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -398,7 +398,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -411,7 +411,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -424,7 +424,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (2, view1.OnMouseEnterCalled); @@ -437,7 +437,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (2, view1.OnMouseEnterCalled); @@ -450,7 +450,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (2, view1.OnMouseEnterCalled); @@ -463,7 +463,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - View.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (3, view1.OnMouseEnterCalled); @@ -474,7 +474,7 @@ public class ApplicationMouseEnterLeaveTests finally { // Cleanup - Application.Top?.Dispose (); + Application.Current?.Dispose (); Application.ResetState (); } } diff --git a/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs b/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs index 0451264ed..6116bcf63 100644 --- a/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs +++ b/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs @@ -202,15 +202,15 @@ public class ApplicationMouseTests var clicked = false; - Application.Top = new Toplevel () + Application.Current = new Toplevel () { Id = "top", }; - Application.Top.X = 0; - Application.Top.Y = 0; - Application.Top.Width = size.Width * 2; - Application.Top.Height = size.Height * 2; - Application.Top.BorderStyle = LineStyle.None; + Application.Current.X = 0; + Application.Current.Y = 0; + Application.Current.Width = size.Width * 2; + Application.Current.Height = size.Height * 2; + Application.Current.BorderStyle = LineStyle.None; var view = new View { Id = "view", X = pos.X, Y = pos.Y, Width = size.Width, Height = size.Height }; @@ -218,7 +218,7 @@ public class ApplicationMouseTests view.BorderStyle = LineStyle.Single; view.CanFocus = true; - Application.Top.Add (view); + Application.Current.Add (view); var mouseEvent = new MouseEventArgs { Position = new (clickX, clickY), ScreenPosition = new (clickX, clickY), Flags = MouseFlags.Button1Clicked }; @@ -231,7 +231,7 @@ public class ApplicationMouseTests Application.RaiseMouseEvent (mouseEvent); Assert.Equal (expectedClicked, clicked); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (ignoreDisposed: true); } diff --git a/Tests/UnitTests/Application/SessionTokenTests.cs b/Tests/UnitTests/Application/SessionTokenTests.cs index d01bc1c59..1e642d04d 100644 --- a/Tests/UnitTests/Application/SessionTokenTests.cs +++ b/Tests/UnitTests/Application/SessionTokenTests.cs @@ -1,4 +1,5 @@ +#nullable enable namespace UnitTests.ApplicationTests; /// These tests focus on Application.SessionToken and the various ways it can be changed. @@ -19,14 +20,16 @@ public class SessionTokenTests public void Begin_End_Cleans_Up_SessionToken () { // Test null Toplevel - Assert.Throws (() => Application.Begin (null)); + Assert.Throws (() => Application.Begin (null!)); - var top = new Toplevel (); + Assert.NotNull (Application.Driver); + + Toplevel top = new Toplevel (); SessionToken rs = Application.Begin (top); Assert.NotNull (rs); Application.End (rs); - Assert.NotNull (Application.Top); + Assert.NotNull (Application.Current); // v2 does not use main loop, it uses MainLoop and its internal //Assert.NotNull (Application.MainLoop); @@ -42,7 +45,7 @@ public class SessionTokenTests [Fact] public void Dispose_Cleans_Up_SessionToken () { - var rs = new SessionToken (null); + var rs = new SessionToken (null!); Assert.NotNull (rs); // Should not throw because Toplevel was null @@ -57,7 +60,7 @@ public class SessionTokenTests // Should throw because Toplevel was not cleaned up Assert.Throws (() => rs.Dispose ()); - rs.Toplevel.Dispose (); + rs.Toplevel?.Dispose (); rs.Toplevel = null; rs.Dispose (); #if DEBUG_IDISPOSABLE @@ -69,7 +72,7 @@ public class SessionTokenTests [Fact] public void New_Creates_SessionToken () { - var rs = new SessionToken (null); + var rs = new SessionToken (null!); Assert.Null (rs.Toplevel); var top = new Toplevel (); diff --git a/Tests/UnitTests/Application/SynchronizatonContextTests.cs b/Tests/UnitTests/Application/SynchronizatonContextTests.cs index 9f8d34b0e..915cb3024 100644 --- a/Tests/UnitTests/Application/SynchronizatonContextTests.cs +++ b/Tests/UnitTests/Application/SynchronizatonContextTests.cs @@ -9,7 +9,7 @@ public class SyncrhonizationContextTests [Fact] public void SynchronizationContext_CreateCopy () { - Application.Init (null, "fake"); + Application.Init ("fake"); SynchronizationContext context = SynchronizationContext.Current; Assert.NotNull (context); @@ -31,7 +31,7 @@ public class SyncrhonizationContextTests { lock (_lockPost) { - Application.Init (null, driverName: driverName); + Application.Init (driverName); SynchronizationContext context = SynchronizationContext.Current; @@ -39,7 +39,7 @@ public class SyncrhonizationContextTests Task.Run (() => { - while (Application.Top is null || Application.Top is { Running: false }) + while (Application.Current is null || Application.Current is { Running: false }) { Thread.Sleep (500); } @@ -56,7 +56,7 @@ public class SyncrhonizationContextTests null ); - if (Application.Top is { Running: true }) + if (Application.Current is { Running: true }) { Assert.False (success); } diff --git a/Tests/UnitTests/Application/TimedEventsTests.cs b/Tests/UnitTests/Application/TimedEventsTests.cs index 4877a6b2c..98d061159 100644 --- a/Tests/UnitTests/Application/TimedEventsTests.cs +++ b/Tests/UnitTests/Application/TimedEventsTests.cs @@ -134,4 +134,35 @@ public class TimedEventsTests Assert.Equal (expected, executeCount); } + + [Fact] + public void StopAll_Stops_All_Timeouts () + { + var timedEvents = new TimedEvents (); + var executeCount = 0; + var expected = 100; + + for (var i = 0; i < expected; i++) + { + timedEvents.Add ( + TimeSpan.Zero, + () => + { + Interlocked.Increment (ref executeCount); + + return false; + }); + } + + Assert.Equal (expected, timedEvents.Timeouts.Count); + + timedEvents.StopAll (); + + Assert.Empty (timedEvents.Timeouts); + + // Run timers once + timedEvents.RunTimers (); + + Assert.Equal (0, executeCount); + } } diff --git a/Tests/UnitTests/AutoInitShutdownAttribute.cs b/Tests/UnitTests/AutoInitShutdownAttribute.cs index 0b3d2af07..a0536503b 100644 --- a/Tests/UnitTests/AutoInitShutdownAttribute.cs +++ b/Tests/UnitTests/AutoInitShutdownAttribute.cs @@ -25,20 +25,16 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute /// be used when Application.Init is called. If not specified FakeDriver will be used. Only valid if /// is true. /// - /// If true and is true, the test will fail. public AutoInitShutdownAttribute ( bool autoInit = true, - string forceDriver = null, - bool verifyShutdown = false + string forceDriver = null ) { - AutoInit = autoInit; + _autoInit = autoInit; CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); _forceDriver = forceDriver; - _verifyShutdown = verifyShutdown; } - private readonly bool _verifyShutdown; private readonly string _forceDriver; private IDisposable _v2Cleanup; @@ -51,15 +47,10 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute _v2Cleanup?.Dispose (); - if (AutoInit) + if (_autoInit) { - // try + try { - if (!_verifyShutdown) - { - Application.ResetState (ignoreDisposed: true); - } - Application.Shutdown (); #if DEBUG_IDISPOSABLE if (View.Instances.Count == 0) @@ -74,14 +65,15 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute } //catch (Exception e) //{ - // Assert.Fail ($"Application.Shutdown threw an exception after the test exited: {e}"); + // Debug.WriteLine ($"Application.Shutdown threw an exception after the test exited: {e}"); //} - //finally + finally { #if DEBUG_IDISPOSABLE View.Instances.Clear (); Application.ResetState (true); #endif + ApplicationImpl.SetInstance (null); } } @@ -102,7 +94,7 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute //Debug.Assert(!CM.IsEnabled, "Some other test left ConfigurationManager enabled."); - if (AutoInit) + if (_autoInit) { #if DEBUG_IDISPOSABLE View.EnableDebugIDisposableAsserts = true; @@ -117,7 +109,7 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute View.Instances.Clear (); } #endif - if (string.IsNullOrEmpty(_forceDriver) || _forceDriver.ToLowerInvariant () == "fake") + if (string.IsNullOrEmpty (_forceDriver) || _forceDriver.ToLowerInvariant () == "fake") { var fa = new FakeApplicationFactory (); _v2Cleanup = fa.SetupFakeApplication (); @@ -131,7 +123,7 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute } } - private bool AutoInit { get; } + private bool _autoInit { get; } /// /// Runs a single iteration of the main loop (layout, draw, run timed events etc.) diff --git a/Tests/UnitTests/Configuration/ThemeScopeTests.cs b/Tests/UnitTests/Configuration/ThemeScopeTests.cs index 25c881d64..ca1e05aa1 100644 --- a/Tests/UnitTests/Configuration/ThemeScopeTests.cs +++ b/Tests/UnitTests/Configuration/ThemeScopeTests.cs @@ -86,6 +86,43 @@ public class ThemeScopeTests Disable (true); } + + [Fact] + public void DeSerialize_Themes_UpdateFrom_Updates () + { + Enable (ConfigLocations.HardCoded); + + IDictionary initial = ThemeManager.Themes!; + + string serialized = """ + { + "Default": { + "Button.DefaultShadow": "None" + } + } + """; + ConcurrentDictionary? deserialized = + JsonSerializer.Deserialize> (serialized, SerializerContext.Options); + + ShadowStyle initialShadowStyle = (ShadowStyle)(initial! ["Default"] ["Button.DefaultShadow"].PropertyValue!); + Assert.Equal (ShadowStyle.Opaque, initialShadowStyle); + + ShadowStyle deserializedShadowStyle = (ShadowStyle)(deserialized! ["Default"] ["Button.DefaultShadow"].PropertyValue!); + Assert.Equal (ShadowStyle.None, deserializedShadowStyle); + + initial ["Default"].UpdateFrom (deserialized ["Default"]); + initialShadowStyle = (ShadowStyle)(initial! ["Default"] ["Button.DefaultShadow"].PropertyValue!); + Assert.Equal (ShadowStyle.None, initialShadowStyle); + + Assert.Equal(ShadowStyle.Opaque, Button.DefaultShadow); + initial ["Default"].Apply (); + Assert.Equal (ShadowStyle.None, Button.DefaultShadow); + + Disable (true); + Assert.Equal (ShadowStyle.Opaque, Button.DefaultShadow); + + } + [Fact] public void Serialize_New_RoundTrip () { diff --git a/Tests/UnitTests/Dialogs/DialogTests.cs b/Tests/UnitTests/Dialogs/DialogTests.cs index b37f3225d..a4ba9591a 100644 --- a/Tests/UnitTests/Dialogs/DialogTests.cs +++ b/Tests/UnitTests/Dialogs/DialogTests.cs @@ -902,8 +902,8 @@ public class DialogTests (ITestOutputHelper output) #if DEBUG_IDISPOSABLE Assert.False (dlg.WasDisposed); - Assert.False (Application.Top!.WasDisposed); - Assert.Equal (dlg, Application.Top); + Assert.False (Application.Current!.WasDisposed); + Assert.Equal (dlg, Application.Current); #endif Assert.True (dlg.Canceled); @@ -925,8 +925,8 @@ public class DialogTests (ITestOutputHelper output) Application.Run (dlg2); Assert.True (dlg.WasDisposed); - Assert.False (Application.Top.WasDisposed); - Assert.Equal (dlg2, Application.Top); + Assert.False (Application.Current.WasDisposed); + Assert.Equal (dlg2, Application.Current); Assert.False (dlg2.WasDisposed); dlg2.Dispose (); @@ -937,10 +937,10 @@ public class DialogTests (ITestOutputHelper output) //Assert.NotNull (exception); //Assert.StartsWith ("Cannot access a disposed object.", exception.Message); - Assert.True (Application.Top.WasDisposed); + Assert.True (Application.Current.WasDisposed); Application.Shutdown (); Assert.True (dlg2.WasDisposed); - Assert.Null (Application.Top); + Assert.Null (Application.Current); #endif return; @@ -1174,8 +1174,8 @@ public class DialogTests (ITestOutputHelper output) switch (iterations) { case 0: - Application.Top!.SetNeedsLayout (); - Application.Top.SetNeedsDraw (); + Application.Current!.SetNeedsLayout (); + Application.Current.SetNeedsDraw (); break; @@ -1216,7 +1216,7 @@ public class DialogTests (ITestOutputHelper output) └───────────────────────┘", output); - Assert.False (Application.Top!.NewKeyDownEvent (Key.Enter)); + Assert.False (Application.Current!.NewKeyDownEvent (Key.Enter)); break; case 7: @@ -1410,9 +1410,9 @@ public class DialogTests (ITestOutputHelper output) #if DEBUG_IDISPOSABLE Assert.False (dlg.WasDisposed); - Assert.False (Application.Top!.WasDisposed); - Assert.NotEqual (top, Application.Top); - Assert.Equal (dlg, Application.Top); + Assert.False (Application.Current!.WasDisposed); + Assert.NotEqual (top, Application.Current); + Assert.Equal (dlg, Application.Current); #endif // dlg wasn't disposed yet and it's possible to access to his properties @@ -1426,11 +1426,11 @@ public class DialogTests (ITestOutputHelper output) top.Dispose (); #if DEBUG_IDISPOSABLE Assert.True (dlg.WasDisposed); - Assert.True (Application.Top.WasDisposed); - Assert.NotNull (Application.Top); + Assert.True (Application.Current.WasDisposed); + Assert.NotNull (Application.Current); #endif Application.Shutdown (); - Assert.Null (Application.Top); + Assert.Null (Application.Current); return; diff --git a/Tests/UnitTests/Dialogs/MessageBoxTests.cs b/Tests/UnitTests/Dialogs/MessageBoxTests.cs index 075067f5f..0699309b7 100644 --- a/Tests/UnitTests/Dialogs/MessageBoxTests.cs +++ b/Tests/UnitTests/Dialogs/MessageBoxTests.cs @@ -193,7 +193,7 @@ public class MessageBoxTests (ITestOutputHelper output) } else if (iterations == 1) { - mbFrame = Application.Top!.Frame; + mbFrame = Application.Current!.Frame; Application.RequestStop (); } } @@ -378,8 +378,8 @@ public class MessageBoxTests (ITestOutputHelper output) { AutoInitShutdownAttribute.RunIteration (); - Assert.IsType (Application.Top); - Assert.Equal (new (height, width), Application.Top.Frame.Size); + Assert.IsType (Application.Current); + Assert.Equal (new (height, width), Application.Current.Frame.Size); Application.RequestStop (); } @@ -415,8 +415,8 @@ public class MessageBoxTests (ITestOutputHelper output) { AutoInitShutdownAttribute.RunIteration (); - Assert.IsType (Application.Top); - Assert.Equal (new (height, width), Application.Top.Frame.Size); + Assert.IsType (Application.Current); + Assert.Equal (new (height, width), Application.Current.Frame.Size); Application.RequestStop (); } @@ -448,8 +448,8 @@ public class MessageBoxTests (ITestOutputHelper output) { AutoInitShutdownAttribute.RunIteration (); - Assert.IsType (Application.Top); - Assert.Equal (new (height, width), Application.Top.Frame.Size); + Assert.IsType (Application.Current); + Assert.Equal (new (height, width), Application.Current.Frame.Size); Application.RequestStop (); } diff --git a/Tests/UnitTests/DriverAssert.cs b/Tests/UnitTests/DriverAssert.cs index a1abc36b5..4e7795b13 100644 --- a/Tests/UnitTests/DriverAssert.cs +++ b/Tests/UnitTests/DriverAssert.cs @@ -60,7 +60,7 @@ internal partial class DriverAssert { case 0: output.WriteLine ( - $"{Application.ToString (driver)}\n" + $"{driver.ToString ()}\n" + $"Expected Attribute {val} at Contents[{line},{c}] {contents [line, c]} was not found.\n" + $" Expected: {string.Join (",", expectedAttributes.Select (attr => attr))}\n" + $" But Was: " @@ -79,7 +79,7 @@ internal partial class DriverAssert if (colorUsed != userExpected) { - output.WriteLine ($"{Application.ToString (driver)}"); + output.WriteLine ($"{driver.ToString ()}"); output.WriteLine ($"Unexpected Attribute at Contents[{line},{c}] = {contents [line, c]}."); output.WriteLine ($" Expected: {userExpected} ({expectedAttributes [int.Parse (userExpected.ToString ())]})"); output.WriteLine ($" But Was: {colorUsed} ({val})"); @@ -152,7 +152,9 @@ internal partial class DriverAssert ) { #pragma warning restore xUnit1013 // Public method should be marked as test - var actualLook = Application.ToString (driver ?? Application.Driver); + driver ??= Application.Driver!; + + var actualLook = driver.ToString (); if (string.Equals (expectedLook, actualLook)) { @@ -198,7 +200,7 @@ internal partial class DriverAssert { List> lines = []; var sb = new StringBuilder (); - driver ??= Application.Driver; + driver ??= Application.Driver!; int x = -1; int y = -1; diff --git a/Tests/UnitTests/Drivers/ClipRegionTests.cs b/Tests/UnitTests/Drivers/ClipRegionTests.cs index 8ede58565..013cdbe7a 100644 --- a/Tests/UnitTests/Drivers/ClipRegionTests.cs +++ b/Tests/UnitTests/Drivers/ClipRegionTests.cs @@ -12,7 +12,7 @@ public class ClipRegionTests (ITestOutputHelper output) [Fact] public void AddRune_Is_Clipped () { - Application.Init (null, "fake"); + Application.Init ("fake"); Application.Driver!.Move (0, 0); Application.Driver!.AddRune ('x'); @@ -41,7 +41,7 @@ public class ClipRegionTests (ITestOutputHelper output) [Fact] public void Clip_Set_To_Empty_AllInvalid () { - Application.Init (null, "fake"); + Application.Init ("fake"); // Define a clip rectangle Application.Driver!.Clip = new (Rectangle.Empty); @@ -64,7 +64,7 @@ public class ClipRegionTests (ITestOutputHelper output) [Fact] public void IsValidLocation () { - Application.Init (null, "fake"); + Application.Init ("fake"); Application.Driver!.Rows = 10; Application.Driver!.Cols = 10; diff --git a/Tests/UnitTests/Drivers/DriverTests.cs b/Tests/UnitTests/Drivers/DriverTests.cs index 558245c9e..202ec7208 100644 --- a/Tests/UnitTests/Drivers/DriverTests.cs +++ b/Tests/UnitTests/Drivers/DriverTests.cs @@ -1,5 +1,4 @@ -using System.Text; -using UnitTests.ViewsTests; +#nullable enable using Xunit.Abstractions; namespace UnitTests.DriverTests; @@ -8,16 +7,16 @@ public class DriverTests (ITestOutputHelper output) { private readonly ITestOutputHelper _output = output; - [Theory] [InlineData ("fake")] [InlineData ("windows")] [InlineData ("dotnet")] [InlineData ("unix")] - public void All_Drivers_Init_Shutdown_Cross_Platform (string driverName = null) + public void All_Drivers_Init_Shutdown_Cross_Platform (string driverName) { - Application.Init (null, driverName: driverName); - Application.Shutdown (); + IApplication? app = Application.Create (); + app.Init (driverName); + app.Shutdown (); } [Theory] @@ -25,12 +24,13 @@ public class DriverTests (ITestOutputHelper output) [InlineData ("windows")] [InlineData ("dotnet")] [InlineData ("unix")] - public void All_Drivers_Run_Cross_Platform (string driverName = null) + public void All_Drivers_Run_Cross_Platform (string driverName) { - Application.Init (null, driverName: driverName); - Application.StopAfterFirstIteration = true; - Application.Run ().Dispose (); - Application.Shutdown (); + IApplication? app = Application.Create (); + app.Init (driverName); + app.StopAfterFirstIteration = true; + app.Run ().Dispose (); + app.Shutdown (); } [Theory] @@ -38,22 +38,22 @@ public class DriverTests (ITestOutputHelper output) [InlineData ("windows")] [InlineData ("dotnet")] [InlineData ("unix")] - public void All_Drivers_LayoutAndDraw_Cross_Platform (string driverName = null) + public void All_Drivers_LayoutAndDraw_Cross_Platform (string driverName) { - Application.Init (null, driverName: driverName); - Application.StopAfterFirstIteration = true; - Application.Run ().Dispose (); + IApplication? app = Application.Create (); + app.Init (driverName); + app.StopAfterFirstIteration = true; + app.Run ().Dispose (); - DriverAssert.AssertDriverContentsWithFrameAre (expectedLook: driverName!, _output); - - Application.Shutdown (); + DriverAssert.AssertDriverContentsWithFrameAre (driverName!, _output, app.Driver); + app.Shutdown (); } } public class TestTop : Toplevel { - /// + /// public override void BeginInit () { Text = Driver!.GetName ()!; diff --git a/Tests/UnitTests/SetupFakeApplicationAttribute.cs b/Tests/UnitTests/SetupFakeApplicationAttribute.cs index 930e44c41..0b8633da7 100644 --- a/Tests/UnitTests/SetupFakeApplicationAttribute.cs +++ b/Tests/UnitTests/SetupFakeApplicationAttribute.cs @@ -32,6 +32,7 @@ public class SetupFakeApplicationAttribute : BeforeAfterTestAttribute _appDispose?.Dispose (); _appDispose = null; + ApplicationImpl.SetInstance (null); base.After (methodUnderTest); } diff --git a/Tests/UnitTests/View/Adornment/AdornmentSubViewTests.cs b/Tests/UnitTests/View/Adornment/AdornmentSubViewTests.cs index 52d5bc01e..7bfa4747a 100644 --- a/Tests/UnitTests/View/Adornment/AdornmentSubViewTests.cs +++ b/Tests/UnitTests/View/Adornment/AdornmentSubViewTests.cs @@ -14,14 +14,14 @@ public class AdornmentSubViewTests (ITestOutputHelper output) [InlineData (2, 1, true)] public void Adornment_WithSubView_Finds (int viewMargin, int subViewMargin, bool expectedFound) { - Application.Top = new Toplevel() + Application.Current = new Toplevel() { Width = 10, Height = 10 }; - Application.Top.Margin!.Thickness = new Thickness (viewMargin); + Application.Current.Margin!.Thickness = new Thickness (viewMargin); // Turn of TransparentMouse for the test - Application.Top.Margin!.ViewportSettings = ViewportSettingsFlags.None; + Application.Current.Margin!.ViewportSettings = ViewportSettingsFlags.None; var subView = new View () { @@ -34,26 +34,26 @@ public class AdornmentSubViewTests (ITestOutputHelper output) // Turn of TransparentMouse for the test subView.Margin!.ViewportSettings = ViewportSettingsFlags.None; - Application.Top.Margin!.Add (subView); - Application.Top.Layout (); + Application.Current.Margin!.Add (subView); + Application.Current.Layout (); - var foundView = View.GetViewsUnderLocation (new Point(0, 0), ViewportSettingsFlags.None).LastOrDefault (); + var foundView = Application.Current.GetViewsUnderLocation (new Point(0, 0), ViewportSettingsFlags.None).LastOrDefault (); bool found = foundView == subView || foundView == subView.Margin; Assert.Equal (expectedFound, found); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (ignoreDisposed: true); } [Fact] public void Adornment_WithNonVisibleSubView_Finds_Adornment () { - Application.Top = new Toplevel () + Application.Current = new Toplevel () { Width = 10, Height = 10 }; - Application.Top.Padding.Thickness = new Thickness (1); + Application.Current.Padding.Thickness = new Thickness (1); var subView = new View () { @@ -63,11 +63,11 @@ public class AdornmentSubViewTests (ITestOutputHelper output) Height = 1, Visible = false }; - Application.Top.Padding.Add (subView); - Application.Top.Layout (); + Application.Current.Padding.Add (subView); + Application.Current.Layout (); - Assert.Equal (Application.Top.Padding, View.GetViewsUnderLocation (new Point(0, 0), ViewportSettingsFlags.None).LastOrDefault ()); - Application.Top?.Dispose (); + Assert.Equal (Application.Current.Padding, Application.Current.GetViewsUnderLocation (new Point(0, 0), ViewportSettingsFlags.None).LastOrDefault ()); + Application.Current?.Dispose (); Application.ResetState (ignoreDisposed: true); } } diff --git a/Tests/UnitTests/View/Adornment/AdornmentTests.cs b/Tests/UnitTests/View/Adornment/AdornmentTests.cs index 176dbc719..3ce9f3b5e 100644 --- a/Tests/UnitTests/View/Adornment/AdornmentTests.cs +++ b/Tests/UnitTests/View/Adornment/AdornmentTests.cs @@ -9,7 +9,11 @@ public class AdornmentTests (ITestOutputHelper output) [SetupFakeApplication] public void Border_Is_Cleared_After_Margin_Thickness_Change () { - View view = new () { Text = "View", Width = 6, Height = 3, BorderStyle = LineStyle.Rounded }; + View view = new () + { + App = ApplicationImpl.Instance, + Text = "View", Width = 6, Height = 3, BorderStyle = LineStyle.Rounded + }; // Remove border bottom thickness view.Border!.Thickness = new (1, 1, 1, 0); @@ -59,7 +63,7 @@ public class AdornmentTests (ITestOutputHelper output) Assert.Equal (6, view.Width); Assert.Equal (3, view.Height); - View.SetClipToScreen (); + view.SetClipToScreen (); view.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( diff --git a/Tests/UnitTests/View/Adornment/BorderTests.cs b/Tests/UnitTests/View/Adornment/BorderTests.cs index 80a769258..e7f02752d 100644 --- a/Tests/UnitTests/View/Adornment/BorderTests.cs +++ b/Tests/UnitTests/View/Adornment/BorderTests.cs @@ -8,7 +8,11 @@ public class BorderTests (ITestOutputHelper output) [SetupFakeApplication] public void Border_Parent_HasFocus_Title_Uses_FocusAttribute () { - var superView = new View { Width = 10, Height = 10, CanFocus = true }; + var superView = new View + { + Driver = ApplicationImpl.Instance.Driver, + Width = 10, Height = 10, CanFocus = true + }; var otherView = new View { Width = 0, Height = 0, CanFocus = true }; superView.Add (otherView); @@ -39,7 +43,7 @@ public class BorderTests (ITestOutputHelper output) view.CanFocus = true; view.SetFocus (); - View.SetClipToScreen (); + view.SetClipToScreen (); view.Draw (); Assert.Equal (view.GetAttributeForRole (VisualRole.Focus), view.Border!.GetAttributeForRole (VisualRole.Focus)); Assert.Equal (view.GetScheme ().Focus.Foreground, view.Border!.GetAttributeForRole (VisualRole.Focus).Foreground); @@ -51,7 +55,11 @@ public class BorderTests (ITestOutputHelper output) [SetupFakeApplication] public void Border_Uses_Parent_Scheme () { - var view = new View { Title = "A", Height = 2, Width = 5 }; + var view = new View + { + Driver = ApplicationImpl.Instance.Driver, + Title = "A", Height = 2, Width = 5 + }; view.Border!.Thickness = new (0, 1, 0, 0); view.Border!.LineStyle = LineStyle.Single; @@ -839,6 +847,7 @@ public class BorderTests (ITestOutputHelper output) { var superView = new View { + Driver = ApplicationImpl.Instance.Driver, Id = "superView", Width = 5, Height = 5, @@ -902,6 +911,7 @@ public class BorderTests (ITestOutputHelper output) { var superView = new View { + Driver = ApplicationImpl.Instance.Driver, Id = "superView", Title = "A", Width = 11, diff --git a/Tests/UnitTests/View/Adornment/MarginTests.cs b/Tests/UnitTests/View/Adornment/MarginTests.cs index 82053e1ef..3a4bc20ba 100644 --- a/Tests/UnitTests/View/Adornment/MarginTests.cs +++ b/Tests/UnitTests/View/Adornment/MarginTests.cs @@ -15,27 +15,27 @@ public class MarginTests (ITestOutputHelper output) view.Margin!.Diagnostics = ViewDiagnosticFlags.Thickness; view.Margin.Thickness = new (1); - Application.Top = new Toplevel (); - Application.TopLevels.Push (Application.Top); + Application.Current = new Toplevel (); + Application.SessionStack.Push (Application.Current); - Application.Top.SetScheme (new() + Application.Current.SetScheme (new() { Normal = new (Color.Red, Color.Green), Focus = new (Color.Green, Color.Red) }); - Application.Top.Add (view); + Application.Current.Add (view); Assert.Equal (ColorName16.Red, view.Margin.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); - Assert.Equal (ColorName16.Red, Application.Top.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); + Assert.Equal (ColorName16.Red, Application.Current.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); - Application.Top.BeginInit (); - Application.Top.EndInit (); + Application.Current.BeginInit (); + Application.Current.EndInit (); Application.LayoutAndDraw(); DriverAssert.AssertDriverContentsAre ( @"", output ); - DriverAssert.AssertDriverAttributesAre ("0", output, null, Application.Top.GetAttributeForRole (VisualRole.Normal)); + DriverAssert.AssertDriverAttributesAre ("0", output, null, Application.Current.GetAttributeForRole (VisualRole.Normal)); Application.ResetState (true); } @@ -51,20 +51,20 @@ public class MarginTests (ITestOutputHelper output) view.Margin.Thickness = new (1); view.Margin.ViewportSettings = ViewportSettingsFlags.None; - Application.Top = new Toplevel (); - Application.TopLevels.Push (Application.Top); + Application.Current = new Toplevel (); + Application.SessionStack.Push (Application.Current); - Application.Top.SetScheme (new () + Application.Current.SetScheme (new () { Normal = new (Color.Red, Color.Green), Focus = new (Color.Green, Color.Red) }); - Application.Top.Add (view); + Application.Current.Add (view); Assert.Equal (ColorName16.Red, view.Margin.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); - Assert.Equal (ColorName16.Red, Application.Top.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); + Assert.Equal (ColorName16.Red, Application.Current.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); - Application.Top.BeginInit (); - Application.Top.EndInit (); + Application.Current.BeginInit (); + Application.Current.EndInit (); Application.LayoutAndDraw (); DriverAssert.AssertDriverContentsAre ( @@ -74,7 +74,7 @@ M M MMM", output ); - DriverAssert.AssertDriverAttributesAre ("0", output, null, Application.Top.GetAttributeForRole (VisualRole.Normal)); + DriverAssert.AssertDriverAttributesAre ("0", output, null, Application.Current.GetAttributeForRole (VisualRole.Normal)); Application.ResetState (true); } diff --git a/Tests/UnitTests/View/Adornment/PaddingTests.cs b/Tests/UnitTests/View/Adornment/PaddingTests.cs index d869a8d26..f48191350 100644 --- a/Tests/UnitTests/View/Adornment/PaddingTests.cs +++ b/Tests/UnitTests/View/Adornment/PaddingTests.cs @@ -9,8 +9,12 @@ public class PaddingTests (ITestOutputHelper output) [SetupFakeApplication] public void Padding_Uses_Parent_Scheme () { - Application.Driver!.SetScreenSize (5, 5); - var view = new View { Height = 3, Width = 3 }; + ApplicationImpl.Instance.Driver!.SetScreenSize (5, 5); + var view = new View + { + App = ApplicationImpl.Instance, + Height = 3, Width = 3 + }; view.Padding!.Thickness = new (1); view.Padding.Diagnostics = ViewDiagnosticFlags.Thickness; diff --git a/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs b/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs index d640a314c..48f24476c 100644 --- a/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs +++ b/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs @@ -48,6 +48,7 @@ public class ShadowStyleTests (ITestOutputHelper output) var superView = new Toplevel { + Driver = ApplicationImpl.Instance.Driver, Height = 3, Width = 3, Text = "012ABC!@#", @@ -65,7 +66,7 @@ public class ShadowStyleTests (ITestOutputHelper output) view.SetScheme (new (Attribute.Default)); superView.Add (view); - Application.TopLevels.Push (superView); + Application.SessionStack.Push (superView); Application.LayoutAndDraw (true); DriverAssert.AssertDriverAttributesAre (expectedAttrs, output, Application.Driver, attributes); Application.ResetState (true); @@ -104,6 +105,7 @@ public class ShadowStyleTests (ITestOutputHelper output) var superView = new Toplevel { + Driver = ApplicationImpl.Instance.Driver, Width = 4, Height = 4, Text = "!@#$".Repeat (4)! @@ -118,7 +120,7 @@ public class ShadowStyleTests (ITestOutputHelper output) }; view.ShadowStyle = style; superView.Add (view); - Application.TopLevels.Push (superView); + Application.SessionStack.Push (superView); Application.LayoutAndDraw (true); DriverAssert.AssertDriverContentsWithFrameAre (expected, output); @@ -136,7 +138,8 @@ public class ShadowStyleTests (ITestOutputHelper output) { var superView = new View { - Height = 10, Width = 10 + Height = 10, Width = 10, + App = ApplicationImpl.Instance }; View view = new () diff --git a/Tests/UnitTests/View/ArrangementTests.cs b/Tests/UnitTests/View/ArrangementTests.cs new file mode 100644 index 000000000..13e5d880a --- /dev/null +++ b/Tests/UnitTests/View/ArrangementTests.cs @@ -0,0 +1,217 @@ +using Xunit.Abstractions; + +namespace UnitTests.ViewTests; + +public class ArrangementTests (ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + + [Fact] + public void MouseGrabHandler_WorksWithMovableView_UsingNewMouseEvent () + { + // This test proves that MouseGrabHandler works correctly with concurrent unit tests + // using NewMouseEvent directly on views, without requiring Application.Init + + var superView = new View + { + Width = 80, + Height = 25 + }; + superView.App = Application.Create (); + + var movableView = new View + { + Arrangement = ViewArrangement.Movable, + BorderStyle = LineStyle.Single, + X = 10, + Y = 10, + Width = 20, + Height = 10 + }; + + superView.Add (movableView); + + // Verify initial state + Assert.NotNull (movableView.Border); + Assert.Null (Application.Mouse.MouseGrabView); + + // Simulate mouse press on the border to start dragging + var pressEvent = new MouseEventArgs + { + Position = new (1, 0), // Top border area + Flags = MouseFlags.Button1Pressed + }; + + bool? result = movableView.Border.NewMouseEvent (pressEvent); + + // The border should have grabbed the mouse + Assert.True (result); + Assert.Equal (movableView.Border, superView.App.Mouse.MouseGrabView); + + // Simulate mouse drag + var dragEvent = new MouseEventArgs + { + Position = new (5, 2), + Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition + }; + + result = movableView.Border.NewMouseEvent (dragEvent); + Assert.True (result); + + // Mouse should still be grabbed + Assert.Equal (movableView.Border, superView.App.Mouse.MouseGrabView); + + // Simulate mouse release to end dragging + var releaseEvent = new MouseEventArgs + { + Position = new (5, 2), + Flags = MouseFlags.Button1Released + }; + + result = movableView.Border.NewMouseEvent (releaseEvent); + Assert.True (result); + + // Mouse should be released + Assert.Null (superView.App.Mouse.MouseGrabView); + } + + [Fact] + public void MouseGrabHandler_WorksWithResizableView_UsingNewMouseEvent () + { + // This test proves MouseGrabHandler works for resizing operations + + var superView = new View + { + App = Application.Create (), + Width = 80, + Height = 25 + }; + + var resizableView = new View + { + Arrangement = ViewArrangement.RightResizable, + BorderStyle = LineStyle.Single, + X = 10, + Y = 10, + Width = 20, + Height = 10 + }; + + superView.Add (resizableView); + + // Verify initial state + Assert.NotNull (resizableView.Border); + Assert.Null (Application.Mouse.MouseGrabView); + + // Calculate position on right border (border is at right edge) + // Border.Frame.X is relative to parent, so we use coordinates relative to the border + var pressEvent = new MouseEventArgs + { + Position = new (resizableView.Border.Frame.Width - 1, 5), // Right border area + Flags = MouseFlags.Button1Pressed + }; + + bool? result = resizableView.Border.NewMouseEvent (pressEvent); + + // The border should have grabbed the mouse for resizing + Assert.True (result); + Assert.Equal (resizableView.Border, superView.App.Mouse.MouseGrabView); + + // Simulate dragging to resize + var dragEvent = new MouseEventArgs + { + Position = new (resizableView.Border.Frame.Width + 3, 5), + Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition + }; + + result = resizableView.Border.NewMouseEvent (dragEvent); + Assert.True (result); + Assert.Equal (resizableView.Border, superView.App.Mouse.MouseGrabView); + + // Simulate mouse release + var releaseEvent = new MouseEventArgs + { + Position = new (resizableView.Border.Frame.Width + 3, 5), + Flags = MouseFlags.Button1Released + }; + + result = resizableView.Border.NewMouseEvent (releaseEvent); + Assert.True (result); + + // Mouse should be released + Assert.Null (superView.App.Mouse.MouseGrabView); + } + + [Fact] + public void MouseGrabHandler_ReleasesOnMultipleViews () + { + // This test verifies MouseGrabHandler properly releases when switching between views + + var superView = new View { Width = 80, Height = 25 }; + superView.App = Application.Create (); + + var view1 = new View + { + Arrangement = ViewArrangement.Movable, + BorderStyle = LineStyle.Single, + X = 10, + Y = 10, + Width = 15, + Height = 8 + }; + + var view2 = new View + { + Arrangement = ViewArrangement.Movable, + BorderStyle = LineStyle.Single, + X = 30, + Y = 10, + Width = 15, + Height = 8 + }; + + superView.Add (view1, view2); + superView.BeginInit (); + superView.EndInit (); + + // Grab mouse on first view + var pressEvent1 = new MouseEventArgs + { + Position = new (1, 0), + Flags = MouseFlags.Button1Pressed + }; + + view1.Border!.NewMouseEvent (pressEvent1); + Assert.Equal (view1.Border, superView.App.Mouse.MouseGrabView); + + // Release on first view + var releaseEvent1 = new MouseEventArgs + { + Position = new (1, 0), + Flags = MouseFlags.Button1Released + }; + + view1.Border.NewMouseEvent (releaseEvent1); + Assert.Null (Application.Mouse.MouseGrabView); + + // Grab mouse on second view + var pressEvent2 = new MouseEventArgs + { + Position = new (1, 0), + Flags = MouseFlags.Button1Pressed + }; + + view2.Border!.NewMouseEvent (pressEvent2); + Assert.Equal (view2.Border, superView.App.Mouse.MouseGrabView); + + // Release on second view + var releaseEvent2 = new MouseEventArgs + { + Position = new (1, 0), + Flags = MouseFlags.Button1Released + }; + + view2.Border.NewMouseEvent (releaseEvent2); + Assert.Null (superView.App.Mouse.MouseGrabView); + } +} diff --git a/Tests/UnitTests/View/Draw/ClearViewportTests.cs b/Tests/UnitTests/View/Draw/ClearViewportTests.cs index 3cdcc9401..db64eb72d 100644 --- a/Tests/UnitTests/View/Draw/ClearViewportTests.cs +++ b/Tests/UnitTests/View/Draw/ClearViewportTests.cs @@ -101,7 +101,11 @@ public class ClearViewportTests (ITestOutputHelper output) [SetupFakeApplication] public void Clear_ClearsEntireViewport () { - var superView = new View { Width = Dim.Fill (), Height = Dim.Fill () }; + var superView = new View + { + App = ApplicationImpl.Instance, + Width = Dim.Fill (), Height = Dim.Fill () + }; var view = new View { @@ -133,7 +137,7 @@ public class ClearViewportTests (ITestOutputHelper output) └─┘", output); - View.SetClipToScreen (); + view.SetClipToScreen (); view.ClearViewport (); @@ -149,7 +153,11 @@ public class ClearViewportTests (ITestOutputHelper output) [SetupFakeApplication] public void Clear_WithClearVisibleContentOnly_ClearsVisibleContentOnly () { - var superView = new View { Width = Dim.Fill (), Height = Dim.Fill () }; + var superView = new View + { + App = ApplicationImpl.Instance, + Width = Dim.Fill (), Height = Dim.Fill () + }; var view = new View { @@ -172,7 +180,7 @@ public class ClearViewportTests (ITestOutputHelper output) │X│ └─┘", output); - View.SetClipToScreen (); + view.SetClipToScreen (); view.ClearViewport (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -203,7 +211,7 @@ public class ClearViewportTests (ITestOutputHelper output) } } - View.SetClip (savedClip); + view.SetClip (savedClip); e.Cancel = true; }; var top = new Toplevel (); @@ -268,7 +276,7 @@ public class ClearViewportTests (ITestOutputHelper output) } } - View.SetClip (savedClip); + view.SetClip (savedClip); e.Cancel = true; }; var top = new Toplevel (); diff --git a/Tests/UnitTests/View/Draw/ClipTests.cs b/Tests/UnitTests/View/Draw/ClipTests.cs index 44b120d76..4e3f62e60 100644 --- a/Tests/UnitTests/View/Draw/ClipTests.cs +++ b/Tests/UnitTests/View/Draw/ClipTests.cs @@ -14,6 +14,7 @@ public class ClipTests (ITestOutputHelper _output) { var view = new View { + App = ApplicationImpl.Instance, X = 1, Y = 1, Width = 3, Height = 3 @@ -36,6 +37,7 @@ public class ClipTests (ITestOutputHelper _output) { var view = new View { + App = ApplicationImpl.Instance, X = 1, Y = 1, Width = 3, Height = 3 @@ -67,7 +69,11 @@ public class ClipTests (ITestOutputHelper _output) [SetupFakeApplication] public void FillRect_Fills_HonorsClip (int x, int y, int width, int height) { - var superView = new View { Width = Dim.Fill (), Height = Dim.Fill () }; + var superView = new View + { + App = ApplicationImpl.Instance, + Width = Dim.Fill (), Height = Dim.Fill () + }; var view = new View { @@ -91,7 +97,7 @@ public class ClipTests (ITestOutputHelper _output) _output); Rectangle toFill = new (x, y, width, height); - View.SetClipToScreen (); + superView.SetClipToScreen (); view.FillRect (toFill); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -133,7 +139,7 @@ public class ClipTests (ITestOutputHelper _output) _output); toFill = new (-1, -1, width + 1, height + 1); - View.SetClipToScreen (); + superView.SetClipToScreen (); view.FillRect (toFill); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -154,7 +160,7 @@ public class ClipTests (ITestOutputHelper _output) └─┘", _output); toFill = new (0, 0, width * 2, height * 2); - View.SetClipToScreen (); + superView.SetClipToScreen (); view.FillRect (toFill); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -175,6 +181,7 @@ public class ClipTests (ITestOutputHelper _output) var top = new View { + App = ApplicationImpl.Instance, Id = "top", Width = Dim.Fill (), Height = Dim.Fill () @@ -193,7 +200,7 @@ public class ClipTests (ITestOutputHelper _output) frameView.Border!.Thickness = new (1, 0, 0, 0); top.Add (frameView); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Layout (); top.Draw (); @@ -217,7 +224,7 @@ public class ClipTests (ITestOutputHelper _output) top.Add (view); top.Layout (); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); // 012345678901234567890123456789012345678 @@ -252,19 +259,20 @@ public class ClipTests (ITestOutputHelper _output) { Width = Dim.Fill (), Height = Dim.Fill (), - ViewportSettings = ViewportSettingsFlags.ClipContentOnly + ViewportSettings = ViewportSettingsFlags.ClipContentOnly, + App = ApplicationImpl.Instance }; view.SetContentSize (new Size (10, 10)); view.Border!.Thickness = new (1); view.BeginInit (); view.EndInit (); - Assert.Equal (view.Frame, View.GetClip ()!.GetBounds ()); + Assert.Equal (view.Frame, view.GetClip ()!.GetBounds ()); // Act view.AddViewportToClip (); // Assert - Assert.Equal (expectedClip, View.GetClip ()!.GetBounds ()); + Assert.Equal (expectedClip, view.GetClip ()!.GetBounds ()); view.Dispose (); } @@ -286,20 +294,21 @@ public class ClipTests (ITestOutputHelper _output) var view = new View { Width = Dim.Fill (), - Height = Dim.Fill () + Height = Dim.Fill (), + App = ApplicationImpl.Instance }; view.SetContentSize (new Size (10, 10)); view.Border!.Thickness = new (1); view.BeginInit (); view.EndInit (); - Assert.Equal (view.Frame, View.GetClip ()!.GetBounds ()); + Assert.Equal (view.Frame, view.GetClip ()!.GetBounds ()); view.Viewport = view.Viewport with { X = 1, Y = 1 }; // Act view.AddViewportToClip (); // Assert - Assert.Equal (expectedClip, View.GetClip ()!.GetBounds ()); + Assert.Equal (expectedClip, view.GetClip ()!.GetBounds ()); view.Dispose (); } } diff --git a/Tests/UnitTests/View/Draw/DrawTests.cs b/Tests/UnitTests/View/Draw/DrawTests.cs index 2526c2178..0e84371d5 100644 --- a/Tests/UnitTests/View/Draw/DrawTests.cs +++ b/Tests/UnitTests/View/Draw/DrawTests.cs @@ -114,7 +114,11 @@ public class DrawTests (ITestOutputHelper output) [SetupFakeApplication] public void Draw_Minimum_Full_Border_With_Empty_Viewport () { - var view = new View { Width = 2, Height = 2, BorderStyle = LineStyle.Single }; + var view = new View + { + App = ApplicationImpl.Instance, + Width = 2, Height = 2, BorderStyle = LineStyle.Single + }; Assert.True (view.NeedsLayout); Assert.True (view.NeedsDraw); view.Layout (); @@ -139,7 +143,11 @@ public class DrawTests (ITestOutputHelper output) [SetupFakeApplication] public void Draw_Minimum_Full_Border_With_Empty_Viewport_Without_Bottom () { - var view = new View { Width = 2, Height = 1, BorderStyle = LineStyle.Single }; + var view = new View + { + App = ApplicationImpl.Instance, + Width = 2, Height = 1, BorderStyle = LineStyle.Single + }; view.Border!.Thickness = new (1, 1, 1, 0); view.BeginInit (); view.EndInit (); @@ -157,7 +165,11 @@ public class DrawTests (ITestOutputHelper output) [SetupFakeApplication] public void Draw_Minimum_Full_Border_With_Empty_Viewport_Without_Left () { - var view = new View { Width = 1, Height = 2, BorderStyle = LineStyle.Single }; + var view = new View + { + App = ApplicationImpl.Instance, + Width = 1, Height = 2, BorderStyle = LineStyle.Single + }; view.Border!.Thickness = new (0, 1, 1, 1); view.BeginInit (); view.EndInit (); @@ -182,7 +194,11 @@ public class DrawTests (ITestOutputHelper output) [SetupFakeApplication] public void Draw_Minimum_Full_Border_With_Empty_Viewport_Without_Right () { - var view = new View { Width = 1, Height = 2, BorderStyle = LineStyle.Single }; + var view = new View + { + App = ApplicationImpl.Instance, + Width = 1, Height = 2, BorderStyle = LineStyle.Single + }; view.Border!.Thickness = new (1, 1, 0, 1); view.BeginInit (); view.EndInit (); @@ -207,7 +223,11 @@ public class DrawTests (ITestOutputHelper output) [SetupFakeApplication] public void Draw_Minimum_Full_Border_With_Empty_Viewport_Without_Top () { - var view = new View { Width = 2, Height = 1, BorderStyle = LineStyle.Single }; + var view = new View + { + App = ApplicationImpl.Instance, + Width = 2, Height = 1, BorderStyle = LineStyle.Single + }; view.Border!.Thickness = new (1, 0, 1, 1); view.BeginInit (); @@ -587,7 +607,11 @@ public class DrawTests (ITestOutputHelper output) [InlineData ("a𐐀b")] public void DrawHotString_NonBmp (string expected) { - var view = new View { Width = 10, Height = 1 }; + var view = new View + { + App = ApplicationImpl.Instance, + Width = 10, Height = 1 + }; view.DrawHotString (expected, Attribute.Default, Attribute.Default); DriverAssert.AssertDriverContentsWithFrameAre (expected, output); @@ -760,7 +784,7 @@ At 0,0 Assert.Equal (new (3, 3, 10, 1), view.Frame); Assert.Equal (new (0, 0, 10, 1), view.Viewport); Assert.Equal (new (0, 0, 10, 1), view.NeedsDrawRect); - View.SetClipToScreen (); + view.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -859,7 +883,7 @@ At 0,0 Assert.Equal (new (1, 1, 10, 1), view.Frame); Assert.Equal (new (0, 0, 10, 1), view.Viewport); Assert.Equal (new (0, 0, 10, 1), view.NeedsDrawRect); - View.SetClipToScreen (); + view.SetClipToScreen (); top.Draw (); diff --git a/Tests/UnitTests/View/Draw/TransparentTests.cs b/Tests/UnitTests/View/Draw/TransparentTests.cs index 6a38f91e7..050ae18cb 100644 --- a/Tests/UnitTests/View/Draw/TransparentTests.cs +++ b/Tests/UnitTests/View/Draw/TransparentTests.cs @@ -14,6 +14,7 @@ public class TransparentTests (ITestOutputHelper output) { var super = new View { + App = ApplicationImpl.Instance, Id = "super", Width = 20, Height = 5, @@ -58,6 +59,7 @@ public class TransparentTests (ITestOutputHelper output) { var super = new View { + App = ApplicationImpl.Instance, Id = "super", Width = 20, Height = 5, diff --git a/Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs b/Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs index 74bfebcbe..236ba0600 100644 --- a/Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs +++ b/Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs @@ -160,14 +160,14 @@ public class KeyBindingsTests () var hotKeyRaised = false; var acceptRaised = false; var selectRaised = false; - Application.Top = new Toplevel (); + Application.Current = new Toplevel (); var view = new View { CanFocus = true, HotKeySpecifier = new Rune ('_'), Title = "_Test" }; - Application.Top.Add (view); + Application.Current.Add (view); view.HandlingHotKey += (s, e) => hotKeyRaised = true; view.Accepting += (s, e) => acceptRaised = true; view.Selecting += (s, e) => selectRaised = true; @@ -191,7 +191,7 @@ public class KeyBindingsTests () Assert.False (acceptRaised); Assert.False (selectRaised); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } // tests that test KeyBindingScope.Focus and KeyBindingScope.HotKey (tests for KeyBindingScope.Application are in Application/KeyboardTests.cs) diff --git a/Tests/UnitTests/View/Layout/GetViewsUnderLocationTests.cs b/Tests/UnitTests/View/Layout/GetViewsUnderLocationTests.cs index ec1fcdd08..97a4fc19d 100644 --- a/Tests/UnitTests/View/Layout/GetViewsUnderLocationTests.cs +++ b/Tests/UnitTests/View/Layout/GetViewsUnderLocationTests.cs @@ -70,22 +70,22 @@ public class GetViewsUnderLocationTests ) { // Arrange - Application.Top = new () + Application.Current = new () { Id = "Top", Frame = new (frameX, frameY, 10, 10) }; - Application.Top.Margin!.Thickness = new (marginThickness); - Application.Top.Margin!.Id = "Margin"; - Application.Top.Border!.Thickness = new (borderThickness); - Application.Top.Border!.Id = "Border"; - Application.Top.Padding!.Thickness = new (paddingThickness); - Application.Top.Padding.Id = "Padding"; + Application.Current.Margin!.Thickness = new (marginThickness); + Application.Current.Margin!.Id = "Margin"; + Application.Current.Border!.Thickness = new (borderThickness); + Application.Current.Border!.Id = "Border"; + Application.Current.Padding!.Thickness = new (paddingThickness); + Application.Current.Padding.Id = "Padding"; var location = new Point (testX, testY); // Act - List viewsUnderMouse = View.GetViewsUnderLocation (location, ViewportSettingsFlags.TransparentMouse); + List viewsUnderMouse = Application.Current.GetViewsUnderLocation (location, ViewportSettingsFlags.TransparentMouse); // Assert if (expectedViewsFound.Length == 0) @@ -98,7 +98,7 @@ public class GetViewsUnderLocationTests Assert.Equal (expectedViewsFound, foundIds); } - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -109,7 +109,7 @@ public class GetViewsUnderLocationTests public void Returns_Top_If_No_SubViews (int testX, int testY) { // Arrange - Application.Top = new () + Application.Current = new () { Frame = new (0, 0, 10, 10) }; @@ -117,11 +117,11 @@ public class GetViewsUnderLocationTests var location = new Point (testX, testY); // Act - List viewsUnderMouse = View.GetViewsUnderLocation (location, ViewportSettingsFlags.TransparentMouse); + List viewsUnderMouse = Application.Current.GetViewsUnderLocation (location, ViewportSettingsFlags.TransparentMouse); // Assert - Assert.Contains (viewsUnderMouse, v => v == Application.Top); - Application.Top.Dispose (); + Assert.Contains (viewsUnderMouse, v => v == Application.Current); + Application.Current.Dispose (); Application.ResetState (true); } @@ -134,13 +134,13 @@ public class GetViewsUnderLocationTests { Application.ResetState (true); - Application.Top = new () + Application.Current = new () { Width = 10, Height = 10 }; - Assert.Same (Application.Top, View.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault ()); - Application.Top.Dispose (); + Assert.Same (Application.Current, Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault ()); + Application.Current.Dispose (); Application.ResetState (true); } @@ -155,7 +155,7 @@ public class GetViewsUnderLocationTests [InlineData (5, 6, true)] public void Returns_Correct_If_SubViews (int testX, int testY, bool expectedSubViewFound) { - Application.Top = new () + Application.Current = new () { Width = 10, Height = 10 }; @@ -165,12 +165,12 @@ public class GetViewsUnderLocationTests X = 1, Y = 2, Width = 5, Height = 5 }; - Application.Top.Add (subview); + Application.Current.Add (subview); - View? found = View.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + View? found = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); Assert.Equal (expectedSubViewFound, found == subview); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -184,7 +184,7 @@ public class GetViewsUnderLocationTests [InlineData (5, 6, false)] public void Returns_Null_If_SubView_NotVisible (int testX, int testY, bool expectedSubViewFound) { - Application.Top = new () + Application.Current = new () { Width = 10, Height = 10 }; @@ -195,12 +195,12 @@ public class GetViewsUnderLocationTests Width = 5, Height = 5, Visible = false }; - Application.Top.Add (subview); + Application.Current.Add (subview); - View? found = View.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + View? found = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); Assert.Equal (expectedSubViewFound, found == subview); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -214,7 +214,7 @@ public class GetViewsUnderLocationTests [InlineData (5, 6, false)] public void Returns_Null_If_Not_Visible_And_SubView_Visible (int testX, int testY, bool expectedSubViewFound) { - Application.Top = new () + Application.Current = new () { Width = 10, Height = 10, Visible = false @@ -225,14 +225,14 @@ public class GetViewsUnderLocationTests X = 1, Y = 2, Width = 5, Height = 5 }; - Application.Top.Add (subview); + Application.Current.Add (subview); subview.Visible = true; Assert.True (subview.Visible); - Assert.False (Application.Top.Visible); - View? found = View.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + Assert.False (Application.Current.Visible); + View? found = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); Assert.Equal (expectedSubViewFound, found == subview); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -249,23 +249,23 @@ public class GetViewsUnderLocationTests [InlineData (6, 7, true)] public void Returns_Correct_If_Start_Has_Adornments (int testX, int testY, bool expectedSubViewFound) { - Application.Top = new () + Application.Current = new () { Width = 10, Height = 10 }; - Application.Top.Margin!.Thickness = new (1); + Application.Current.Margin!.Thickness = new (1); var subview = new View { X = 1, Y = 2, Width = 5, Height = 5 }; - Application.Top.Add (subview); + Application.Current.Add (subview); - View? found = View.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + View? found = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); Assert.Equal (expectedSubViewFound, found == subview); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -280,24 +280,24 @@ public class GetViewsUnderLocationTests [InlineData (-1, 0, 0, false)] public void Returns_Correct_If_Start_Has_Offset_Viewport (int offset, int testX, int testY, bool expectedSubViewFound) { - Application.Top = new () + Application.Current = new () { Width = 10, Height = 10, ViewportSettings = ViewportSettingsFlags.AllowNegativeLocation }; - Application.Top.Viewport = new (offset, offset, 10, 10); + Application.Current.Viewport = new (offset, offset, 10, 10); var subview = new View { X = 1, Y = 1, Width = 2, Height = 2 }; - Application.Top.Add (subview); + Application.Current.Add (subview); - View? found = View.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + View? found = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); Assert.Equal (expectedSubViewFound, found == subview); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -313,25 +313,25 @@ public class GetViewsUnderLocationTests [InlineData (6, 7, false)] public void Returns_Correct_If_Start_Has_Adornment_WithSubView (int testX, int testY, bool expectedSubViewFound) { - Application.Top = new () + Application.Current = new () { Width = 10, Height = 10 }; - Application.Top.Padding!.Thickness = new (1); + Application.Current.Padding!.Thickness = new (1); var subview = new View { X = Pos.AnchorEnd (1), Y = Pos.AnchorEnd (1), Width = 1, Height = 1 }; - Application.Top.Padding.Add (subview); - Application.Top.BeginInit (); - Application.Top.EndInit (); + Application.Current.Padding.Add (subview); + Application.Current.BeginInit (); + Application.Current.EndInit (); - View? found = View.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + View? found = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); Assert.Equal (expectedSubViewFound, found == subview); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -347,17 +347,17 @@ public class GetViewsUnderLocationTests { Application.ResetState (true); - Application.Top = new () + Application.Current = new () { Id = "Top", Width = 10, Height = 10 }; - Application.Top.Margin!.Thickness = new (1); - Application.Top.Margin!.Id = "Margin"; - Application.Top.Border!.Thickness = new (1); - Application.Top.Border!.Id = "Border"; - Application.Top.Padding!.Thickness = new (1); - Application.Top.Padding.Id = "Padding"; + Application.Current.Margin!.Thickness = new (1); + Application.Current.Margin!.Id = "Margin"; + Application.Current.Border!.Thickness = new (1); + Application.Current.Border!.Id = "Border"; + Application.Current.Padding!.Thickness = new (1); + Application.Current.Padding.Id = "Padding"; var subview = new View { @@ -365,13 +365,13 @@ public class GetViewsUnderLocationTests X = 1, Y = 1, Width = 1, Height = 1 }; - Application.Top.Add (subview); + Application.Current.Add (subview); - List viewsUnderMouse = View.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse); + List viewsUnderMouse = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse); string [] foundIds = viewsUnderMouse.Select (v => v!.Id).ToArray (); Assert.Equal (expectedViewsFound, foundIds); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -388,7 +388,7 @@ public class GetViewsUnderLocationTests [InlineData (2, 3, new [] { "Top", "subview" })] public void Returns_Correct_If_SubView_Has_Adornments (int testX, int testY, string [] expectedViewsFound) { - Application.Top = new () + Application.Current = new () { Id = "Top", Width = 10, Height = 10 @@ -402,13 +402,13 @@ public class GetViewsUnderLocationTests }; subview.Border!.Thickness = new (1); subview.Border!.Id = "border"; - Application.Top.Add (subview); + Application.Current.Add (subview); - List viewsUnderMouse = View.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse); + List viewsUnderMouse = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse); string [] foundIds = viewsUnderMouse.Select (v => v!.Id).ToArray (); Assert.Equal (expectedViewsFound, foundIds); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -425,7 +425,7 @@ public class GetViewsUnderLocationTests [InlineData (2, 3, new [] { "Top", "subview" })] public void Returns_Correct_If_SubView_Has_Adornments_With_TransparentMouse (int testX, int testY, string [] expectedViewsFound) { - Application.Top = new () + Application.Current = new () { Id = "Top", Width = 10, Height = 10 @@ -440,13 +440,13 @@ public class GetViewsUnderLocationTests subview.Border!.Thickness = new (1); subview.Border!.ViewportSettings = ViewportSettingsFlags.TransparentMouse; subview.Border!.Id = "border"; - Application.Top.Add (subview); + Application.Current.Add (subview); - List viewsUnderMouse = View.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse); + List viewsUnderMouse = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse); string [] foundIds = viewsUnderMouse.Select (v => v!.Id).ToArray (); Assert.Equal (expectedViewsFound, foundIds); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -463,7 +463,7 @@ public class GetViewsUnderLocationTests [InlineData (5, 5, true)] public void Returns_Correct_If_SubView_Has_Adornment_WithSubView (int testX, int testY, bool expectedSubViewFound) { - Application.Top = new () + Application.Current = new () { Width = 10, Height = 10 }; @@ -486,14 +486,14 @@ public class GetViewsUnderLocationTests Height = 1 }; subview.Padding.Add (paddingSubView); - Application.Top.Add (subview); - Application.Top.BeginInit (); - Application.Top.EndInit (); + Application.Current.Add (subview); + Application.Current.BeginInit (); + Application.Current.EndInit (); - View? found = View.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + View? found = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); Assert.Equal (expectedSubViewFound, found == paddingSubView); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -510,7 +510,7 @@ public class GetViewsUnderLocationTests [InlineData (5, 5, true)] public void Returns_Correct_If_SubView_Is_Scrolled_And_Has_Adornment_WithSubView (int testX, int testY, bool expectedSubViewFound) { - Application.Top = new () + Application.Current = new () { Width = 10, Height = 10 }; @@ -537,14 +537,14 @@ public class GetViewsUnderLocationTests Height = 1 }; subview.Padding.Add (paddingSubView); - Application.Top.Add (subview); - Application.Top.BeginInit (); - Application.Top.EndInit (); + Application.Current.Add (subview); + Application.Current.BeginInit (); + Application.Current.EndInit (); - View? found = View.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + View? found = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); Assert.Equal (expectedSubViewFound, found == paddingSubView); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -560,7 +560,7 @@ public class GetViewsUnderLocationTests [InlineData (5, 5, 2)] public void Returns_Correct_With_NestedSubViews (int testX, int testY, int expectedSubViewFound) { - Application.Top = new () + Application.Current = new () { Width = 10, Height = 10 }; @@ -583,11 +583,11 @@ public class GetViewsUnderLocationTests } } - Application.Top.Add (subviews [0]); + Application.Current.Add (subviews [0]); - View? found = View.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + View? found = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); Assert.Equal (expectedSubViewFound, subviews.IndexOf (found!)); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -604,7 +604,7 @@ public class GetViewsUnderLocationTests public void Tiled_SubViews (int mouseX, int mouseY, string [] viewIdStrings) { // Arrange - Application.Top = new () + Application.Current = new () { Frame = new (0, 0, 10, 10), Id = "top" @@ -630,15 +630,15 @@ public class GetViewsUnderLocationTests Arrangement = ViewArrangement.Overlapped }; // at 2,2 to 4,3 (screen) view.Add (subView); - Application.Top.Add (view); + Application.Current.Add (view); - List found = View.GetViewsUnderLocation (new (mouseX, mouseY), ViewportSettingsFlags.TransparentMouse); + List found = Application.Current.GetViewsUnderLocation (new (mouseX, mouseY), ViewportSettingsFlags.TransparentMouse); string [] foundIds = found.Select (v => v!.Id).ToArray (); Assert.Equal (viewIdStrings, foundIds); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -656,7 +656,7 @@ public class GetViewsUnderLocationTests public void Popover (int mouseX, int mouseY, string [] viewIdStrings) { // Arrange - Application.Top = new () + Application.Current = new () { Frame = new (0, 0, 10, 10), Id = "top" @@ -683,15 +683,15 @@ public class GetViewsUnderLocationTests }; // at 2,2 to 4,3 (screen) view.Add (popOver); - Application.Top.Add (view); + Application.Current.Add (view); - List found = View.GetViewsUnderLocation (new (mouseX, mouseY), ViewportSettingsFlags.TransparentMouse); + List found = Application.Current.GetViewsUnderLocation (new (mouseX, mouseY), ViewportSettingsFlags.TransparentMouse); string [] foundIds = found.Select (v => v!.Id).ToArray (); Assert.Equal (viewIdStrings, foundIds); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -714,18 +714,18 @@ public class GetViewsUnderLocationTests secondaryToplevel.Margin!.Thickness = new (1); secondaryToplevel.Layout (); - Application.TopLevels.Clear (); - Application.TopLevels.Push (topToplevel); - Application.TopLevels.Push (secondaryToplevel); - Application.Top = secondaryToplevel; + Application.SessionStack.Clear (); + Application.SessionStack.Push (topToplevel); + Application.SessionStack.Push (secondaryToplevel); + Application.Current = secondaryToplevel; - List found = View.GetViewsUnderLocation (new (2, 2), ViewportSettingsFlags.TransparentMouse); + List found = Application.Current.GetViewsUnderLocation (new (2, 2), ViewportSettingsFlags.TransparentMouse); Assert.Contains (found, v => v?.Id == topToplevel.Id); Assert.Contains (found, v => v == topToplevel); topToplevel.Dispose (); secondaryToplevel.Dispose (); - Application.TopLevels.Clear (); + Application.SessionStack.Clear (); Application.ResetState (true); } @@ -748,18 +748,18 @@ public class GetViewsUnderLocationTests secondaryToplevel.Margin!.Thickness = new (1); secondaryToplevel.Layout (); - Application.TopLevels.Clear (); - Application.TopLevels.Push (topToplevel); - Application.TopLevels.Push (secondaryToplevel); - Application.Top = secondaryToplevel; + Application.SessionStack.Clear (); + Application.SessionStack.Push (topToplevel); + Application.SessionStack.Push (secondaryToplevel); + Application.Current = secondaryToplevel; - List found = View.GetViewsUnderLocation (new (7, 7), ViewportSettingsFlags.TransparentMouse); + List found = Application.Current.GetViewsUnderLocation (new (7, 7), ViewportSettingsFlags.TransparentMouse); Assert.Contains (found, v => v?.Id == secondaryToplevel.Id); Assert.DoesNotContain (found, v => v?.Id == topToplevel.Id); topToplevel.Dispose (); secondaryToplevel.Dispose (); - Application.TopLevels.Clear (); + Application.SessionStack.Clear (); Application.ResetState (true); } @@ -781,27 +781,27 @@ public class GetViewsUnderLocationTests }; secondaryToplevel.Margin!.Thickness = new (1); - Application.TopLevels.Clear (); - Application.TopLevels.Push (topToplevel); - Application.TopLevels.Push (secondaryToplevel); - Application.Top = secondaryToplevel; + Application.SessionStack.Clear (); + Application.SessionStack.Push (topToplevel); + Application.SessionStack.Push (secondaryToplevel); + Application.Current = secondaryToplevel; secondaryToplevel.Margin!.ViewportSettings = ViewportSettingsFlags.None; - List found = View.GetViewsUnderLocation (new (5, 5), ViewportSettingsFlags.TransparentMouse); + List found = Application.Current.GetViewsUnderLocation (new (5, 5), ViewportSettingsFlags.TransparentMouse); Assert.Contains (found, v => v == secondaryToplevel); Assert.Contains (found, v => v == secondaryToplevel.Margin); Assert.DoesNotContain (found, v => v?.Id == topToplevel.Id); secondaryToplevel.Margin!.ViewportSettings = ViewportSettingsFlags.TransparentMouse; - found = View.GetViewsUnderLocation (new (5, 5), ViewportSettingsFlags.TransparentMouse); + found = Application.Current.GetViewsUnderLocation (new (5, 5), ViewportSettingsFlags.TransparentMouse); Assert.DoesNotContain (found, v => v == secondaryToplevel); Assert.DoesNotContain (found, v => v == secondaryToplevel.Margin); Assert.Contains (found, v => v?.Id == topToplevel.Id); topToplevel.Dispose (); secondaryToplevel.Dispose (); - Application.TopLevels.Clear (); + Application.SessionStack.Clear (); Application.ResetState (true); } @@ -824,17 +824,17 @@ public class GetViewsUnderLocationTests secondaryToplevel.Margin!.Thickness = new (1); secondaryToplevel.Layout (); - Application.TopLevels.Clear (); - Application.TopLevels.Push (topToplevel); - Application.TopLevels.Push (secondaryToplevel); - Application.Top = secondaryToplevel; + Application.SessionStack.Clear (); + Application.SessionStack.Push (topToplevel); + Application.SessionStack.Push (secondaryToplevel); + Application.Current = secondaryToplevel; - List found = View.GetViewsUnderLocation (new (20, 20), ViewportSettingsFlags.TransparentMouse); + List found = Application.Current.GetViewsUnderLocation (new (20, 20), ViewportSettingsFlags.TransparentMouse); Assert.Empty (found); topToplevel.Dispose (); secondaryToplevel.Dispose (); - Application.TopLevels.Clear (); + Application.SessionStack.Clear (); Application.ResetState (true); } } diff --git a/Tests/UnitTests/View/Layout/Pos.CombineTests.cs b/Tests/UnitTests/View/Layout/Pos.CombineTests.cs index f6ca4edcf..cbbf50da6 100644 --- a/Tests/UnitTests/View/Layout/Pos.CombineTests.cs +++ b/Tests/UnitTests/View/Layout/Pos.CombineTests.cs @@ -40,7 +40,7 @@ public class PosCombineTests (ITestOutputHelper output) [SetupFakeApplication] public void PosCombine_DimCombine_View_With_SubViews () { - Application.Top = new Toplevel () { Width = 80, Height = 25 }; + Application.Current = new Toplevel () { Width = 80, Height = 25 }; var win1 = new Window { Id = "win1", Width = 20, Height = 10 }; var view1 = new View { @@ -59,24 +59,24 @@ public class PosCombineTests (ITestOutputHelper output) view2.Add (view3); win2.Add (view2); win1.Add (view1, win2); - Application.Top.Add (win1); - Application.Top.Layout (); + Application.Current.Add (win1); + Application.Current.Layout (); - Assert.Equal (new Rectangle (0, 0, 80, 25), Application.Top.Frame); + Assert.Equal (new Rectangle (0, 0, 80, 25), Application.Current.Frame); Assert.Equal (new Rectangle (0, 0, 5, 1), view1.Frame); Assert.Equal (new Rectangle (0, 0, 20, 10), win1.Frame); Assert.Equal (new Rectangle (0, 2, 10, 3), win2.Frame); Assert.Equal (new Rectangle (0, 0, 8, 1), view2.Frame); Assert.Equal (new Rectangle (0, 0, 7, 1), view3.Frame); - var foundView = View.GetViewsUnderLocation (new Point(9, 4), ViewportSettingsFlags.None).LastOrDefault (); + var foundView = Application.Current.GetViewsUnderLocation (new Point(9, 4), ViewportSettingsFlags.None).LastOrDefault (); Assert.Equal (foundView, view2); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Fact] public void PosCombine_Refs_SuperView_Throws () { - Application.Init (null, "fake"); + Application.Init ("fake"); var top = new Toplevel (); var w = new Window { X = Pos.Left (top) + 2, Y = Pos.Top (top) + 2 }; @@ -89,13 +89,13 @@ public class PosCombineTests (ITestOutputHelper output) top.Add (w); Application.Begin (top); - f.X = Pos.X (Application.Top) + Pos.X (v2) - Pos.X (v1); - f.Y = Pos.Y (Application.Top) + Pos.Y (v2) - Pos.Y (v1); + f.X = Pos.X (Application.Current) + Pos.X (v2) - Pos.X (v1); + f.Y = Pos.Y (Application.Current) + Pos.Y (v2) - Pos.Y (v1); - Application.Top.SubViewsLaidOut += (s, e) => + Application.Current.SubViewsLaidOut += (s, e) => { - Assert.Equal (0, Application.Top.Frame.X); - Assert.Equal (0, Application.Top.Frame.Y); + Assert.Equal (0, Application.Current.Frame.X); + Assert.Equal (0, Application.Current.Frame.Y); Assert.Equal (2, w.Frame.X); Assert.Equal (2, w.Frame.Y); Assert.Equal (2, f.Frame.X); @@ -109,7 +109,7 @@ public class PosCombineTests (ITestOutputHelper output) Application.StopAfterFirstIteration = true; Assert.Throws (() => Application.Run ()); - Application.Top.Dispose (); + Application.Current.Dispose (); top.Dispose (); Application.Shutdown (); } diff --git a/Tests/UnitTests/View/Layout/Pos.Tests.cs b/Tests/UnitTests/View/Layout/Pos.Tests.cs index 4d419b29a..256c1c6e1 100644 --- a/Tests/UnitTests/View/Layout/Pos.Tests.cs +++ b/Tests/UnitTests/View/Layout/Pos.Tests.cs @@ -11,7 +11,7 @@ public class PosTests () public void Pos_Validation_Do_Not_Throws_If_NewValue_Is_PosAbsolute_And_OldValue_Is_Another_Type_After_Sets_To_LayoutStyle_Absolute () { - Application.Init (null, "fake"); + Application.Init ("fake"); Toplevel t = new (); @@ -43,7 +43,7 @@ public class PosTests () [TestRespondersDisposed] public void PosCombine_WHY_Throws () { - Application.Init (null, "fake"); + Application.Init ("fake"); Toplevel t = new Toplevel (); @@ -133,7 +133,7 @@ public class PosTests () [TestRespondersDisposed] public void Pos_Subtract_Operator () { - Application.Init (null, "fake"); + Application.Init ("fake"); Toplevel top = new (); @@ -207,7 +207,7 @@ public class PosTests () [Fact] public void Pos_Validation_Do_Not_Throws_If_NewValue_Is_PosAbsolute_And_OldValue_Is_Null () { - Application.Init (null, "fake"); + Application.Init ("fake"); Toplevel t = new (); @@ -233,7 +233,7 @@ public class PosTests () [Fact] public void Validation_Does_Not_Throw_If_NewValue_Is_PosAbsolute_And_OldValue_Is_Null () { - Application.Init (null, "fake"); + Application.Init ("fake"); Toplevel t = new Toplevel (); diff --git a/Tests/UnitTests/View/Layout/SetLayoutTests.cs b/Tests/UnitTests/View/Layout/SetLayoutTests.cs index ac3654e29..7ccf52dca 100644 --- a/Tests/UnitTests/View/Layout/SetLayoutTests.cs +++ b/Tests/UnitTests/View/Layout/SetLayoutTests.cs @@ -12,7 +12,7 @@ public class SetLayoutTests (ITestOutputHelper output) [AutoInitShutdown] public void Screen_Size_Change_Causes_Layout () { - Application.Top = new (); + Application.Current = new (); var view = new View { @@ -22,21 +22,21 @@ public class SetLayoutTests (ITestOutputHelper output) Height = 1, Text = "0123456789" }; - Application.Top.Add (view); + Application.Current.Add (view); - var rs = Application.Begin (Application.Top); + var rs = Application.Begin (Application.Current); Application.Driver!.SetScreenSize (80, 25); Assert.Equal (new (0, 0, 80, 25), new Rectangle (0, 0, Application.Screen.Width, Application.Screen.Height)); - Assert.Equal (new (0, 0, Application.Screen.Width, Application.Screen.Height), Application.Top.Frame); - Assert.Equal (new (0, 0, 80, 25), Application.Top.Frame); + Assert.Equal (new (0, 0, Application.Screen.Width, Application.Screen.Height), Application.Current.Frame); + Assert.Equal (new (0, 0, 80, 25), Application.Current.Frame); Application.Driver!.SetScreenSize (20, 10); - Assert.Equal (new (0, 0, Application.Screen.Width, Application.Screen.Height), Application.Top.Frame); + Assert.Equal (new (0, 0, Application.Screen.Width, Application.Screen.Height), Application.Current.Frame); - Assert.Equal (new (0, 0, 20, 10), Application.Top.Frame); + Assert.Equal (new (0, 0, 20, 10), Application.Current.Frame); Application.End (rs); - Application.Top.Dispose (); + Application.Current.Dispose (); } } diff --git a/Tests/UnitTests/View/Navigation/CanFocusTests.cs b/Tests/UnitTests/View/Navigation/CanFocusTests.cs index 679239fdc..8a82c605d 100644 --- a/Tests/UnitTests/View/Navigation/CanFocusTests.cs +++ b/Tests/UnitTests/View/Navigation/CanFocusTests.cs @@ -88,20 +88,21 @@ public class CanFocusTests [Fact] public void CanFocus_Set_True_Get_AdvanceFocus_Works () { + IApplication app = Application.Create (); + app.Current = new () { App = app }; + Label label = new () { Text = "label" }; View view = new () { Text = "view", CanFocus = true }; - Application.Navigation = new (); - Application.Top = new (); - Application.Top.Add (label, view); + app.Current.Add (label, view); - Application.Top.SetFocus (); - Assert.Equal (view, Application.Navigation.GetFocused ()); + app.Current.SetFocus (); + Assert.Equal (view, app.Navigation!.GetFocused ()); Assert.False (label.CanFocus); Assert.False (label.HasFocus); Assert.True (view.CanFocus); Assert.True (view.HasFocus); - Assert.False (Application.Navigation.AdvanceFocus (NavigationDirection.Forward, null)); + Assert.False (app.Navigation.AdvanceFocus (NavigationDirection.Forward, null)); Assert.False (label.HasFocus); Assert.True (view.HasFocus); @@ -111,7 +112,7 @@ public class CanFocusTests Assert.True (view.HasFocus); // label can now be focused, so AdvanceFocus should move to it. - Assert.True (Application.Navigation.AdvanceFocus (NavigationDirection.Forward, null)); + Assert.True (app.Navigation.AdvanceFocus (NavigationDirection.Forward, null)); Assert.True (label.HasFocus); Assert.False (view.HasFocus); @@ -120,11 +121,11 @@ public class CanFocusTests Assert.False (label.HasFocus); Assert.True (view.HasFocus); - Assert.True (Application.RaiseKeyDownEvent (Key.Tab)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Tab)); Assert.True (label.HasFocus); Assert.False (view.HasFocus); - Application.Top.Dispose (); - Application.ResetState (); + app.Current.Dispose (); + app.ResetState (); } } diff --git a/Tests/UnitTests/View/Navigation/NavigationTests.cs b/Tests/UnitTests/View/Navigation/NavigationTests.cs index 8198b14eb..466d6a1ca 100644 --- a/Tests/UnitTests/View/Navigation/NavigationTests.cs +++ b/Tests/UnitTests/View/Navigation/NavigationTests.cs @@ -27,8 +27,7 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews } Toplevel top = new (); - Application.Top = top; - Application.Navigation = new (); + Application.Current = top; View otherView = new () { @@ -118,8 +117,7 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews } Toplevel top = new (); - Application.Top = top; - Application.Navigation = new (); + Application.Current = top; View otherView = new () { @@ -150,8 +148,8 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews // Ensure the view is Visible view.Visible = true; - Application.Top.SetFocus (); - Assert.True (Application.Top!.HasFocus); + Application.Current.SetFocus (); + Assert.True (Application.Current!.HasFocus); Assert.True (top.HasFocus); // Start with the focus on our test view @@ -282,8 +280,7 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews Toplevel top = new (); - Application.Top = top; - Application.Navigation = new (); + Application.Current = top; View otherView = new () { diff --git a/Tests/UnitTests/View/TextTests.cs b/Tests/UnitTests/View/TextTests.cs index 3e552204c..625d0ce49 100644 --- a/Tests/UnitTests/View/TextTests.cs +++ b/Tests/UnitTests/View/TextTests.cs @@ -13,6 +13,7 @@ public class TextTests (ITestOutputHelper output) public void Setting_With_Height_Horizontal () { var top = new View { Width = 25, Height = 25 }; + top.App = ApplicationImpl.Instance; var label = new Label { Text = "Hello", /* Width = 10, Height = 2, */ ValidatePosDim = true }; var viewX = new View { Text = "X", X = Pos.Right (label), Width = 1, Height = 1 }; @@ -39,7 +40,7 @@ Y Assert.Equal (new (0, 0, 10, 2), label.Frame); top.LayoutSubViews (); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); expected = @" @@ -394,7 +395,7 @@ Y Assert.Equal (new (1, 5), view.TextFormatter.ConstrainToSize); Assert.Equal (new () { "Views" }, view.TextFormatter.GetLines ()); Assert.Equal (new (0, 0, 4, 10), win.Frame); - Assert.Equal (new (0, 0, 4, 10), Application.Top.Frame); + Assert.Equal (new (0, 0, 4, 10), Application.Current.Frame); var expected = @" ┌──┐ @@ -449,6 +450,7 @@ Y var view = new View { + App = ApplicationImpl.Instance, TextDirection = TextDirection.TopBottom_LeftRight, Text = text, Width = Dim.Auto (), @@ -1000,6 +1002,7 @@ w "; { Application.Driver!.SetScreenSize (32, 32); var top = new View { Width = 32, Height = 32 }; + top.App = ApplicationImpl.Instance; var text = $"First line{Environment.NewLine}Second line"; var horizontalView = new View { Width = 20, Height = 1, Text = text }; @@ -1075,7 +1078,7 @@ w "; verticalView.Width = 2; verticalView.TextFormatter.ConstrainToSize = new (2, 20); Assert.True (verticalView.TextFormatter.NeedsFormat); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); Assert.Equal (new (0, 3, 2, 20), verticalView.Frame); @@ -1124,7 +1127,7 @@ w "; View view; var text = "test"; - view = new Label { Text = text }; + view = new Label { App = ApplicationImpl.Instance, Text = text }; view.BeginInit (); view.EndInit (); view.Draw (); diff --git a/Tests/UnitTests/View/ViewCommandTests.cs b/Tests/UnitTests/View/ViewCommandTests.cs index 9564aefdd..cc48653eb 100644 --- a/Tests/UnitTests/View/ViewCommandTests.cs +++ b/Tests/UnitTests/View/ViewCommandTests.cs @@ -46,9 +46,9 @@ public class ViewCommandTests w.LayoutSubViews (); - Application.Top = w; - Application.TopLevels.Push (w); - Assert.Same (Application.Top, w); + Application.Current = w; + Application.SessionStack.Push (w); + Assert.Same (Application.Current, w); // Click button 2 Rectangle btn2Frame = btnB.FrameToScreen (); @@ -121,9 +121,9 @@ public class ViewCommandTests w.Add (btn); - Application.Top = w; - Application.TopLevels.Push (w); - Assert.Same (Application.Top, w); + Application.Current = w; + Application.SessionStack.Push (w); + Assert.Same (Application.Current, w); w.LayoutSubViews (); diff --git a/Tests/UnitTests/View/ViewTests.cs b/Tests/UnitTests/View/ViewTests.cs index 42d89f39a..f42715e6e 100644 --- a/Tests/UnitTests/View/ViewTests.cs +++ b/Tests/UnitTests/View/ViewTests.cs @@ -332,6 +332,7 @@ public class ViewTests public void Test_Nested_Views_With_Height_Equal_To_One () { var v = new View { Width = 11, Height = 3 }; + v.App = ApplicationImpl.Instance; var top = new View { Width = Dim.Fill (), Height = 1 }; var bottom = new View { Width = Dim.Fill (), Height = 1, Y = 2 }; diff --git a/Tests/UnitTests/View/Viewport/ViewportSettings.TransparentMouseTests.cs b/Tests/UnitTests/View/Viewport/ViewportSettings.TransparentMouseTests.cs index 089753bf7..2e66c4c9b 100644 --- a/Tests/UnitTests/View/Viewport/ViewportSettings.TransparentMouseTests.cs +++ b/Tests/UnitTests/View/Viewport/ViewportSettings.TransparentMouseTests.cs @@ -23,7 +23,7 @@ public class TransparentMouseTests { Id = "top", }; - Application.Top = top; + Application.Current = top; var underlying = new MouseTrackingView { Id = "underlying", X = 0, Y = 0, Width = 10, Height = 10 }; var overlay = new MouseTrackingView { Id = "overlay", X = 0, Y = 0, Width = 10, Height = 10, ViewportSettings = ViewportSettingsFlags.TransparentMouse }; @@ -56,7 +56,7 @@ public class TransparentMouseTests { // Arrange var top = new Toplevel (); - Application.Top = top; + Application.Current = top; var underlying = new MouseTrackingView { X = 0, Y = 0, Width = 10, Height = 10 }; var overlay = new MouseTrackingView { X = 0, Y = 0, Width = 10, Height = 10, ViewportSettings = ViewportSettingsFlags.None }; @@ -90,7 +90,7 @@ public class TransparentMouseTests { // Arrange var top = new Toplevel (); - Application.Top = top; + Application.Current = top; var underlying = new MouseTrackingView { X = 0, Y = 0, Width = 10, Height = 10, ViewportSettings = ViewportSettingsFlags.TransparentMouse }; var overlay = new MouseTrackingView { X = 0, Y = 0, Width = 10, Height = 10, ViewportSettings = ViewportSettingsFlags.TransparentMouse }; diff --git a/Tests/UnitTests/Views/AppendAutocompleteTests.cs b/Tests/UnitTests/Views/AppendAutocompleteTests.cs index a3c910710..3fa75b7b8 100644 --- a/Tests/UnitTests/Views/AppendAutocompleteTests.cs +++ b/Tests/UnitTests/Views/AppendAutocompleteTests.cs @@ -13,9 +13,9 @@ public class AppendAutocompleteTests (ITestOutputHelper output) // f is typed and suggestion is "fish" Application.RaiseKeyDownEvent ('f'); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.PositionCursor (); DriverAssert.AssertDriverContentsAre ("fish", output); Assert.Equal ("f", tf.Text); @@ -25,17 +25,17 @@ public class AppendAutocompleteTests (ITestOutputHelper output) // Suggestion should disappear tf.Draw (); - View.SetClipToScreen (); + tf.SetClipToScreen (); DriverAssert.AssertDriverContentsAre ("f", output); Assert.Equal ("f", tf.Text); // Still has focus though - Assert.Same (tf, Application.Top.Focused); + Assert.Same (tf, Application.Current.Focused); // But can tab away Application.RaiseKeyDownEvent ('\t'); - Assert.NotSame (tf, Application.Top.Focused); - Application.Top.Dispose (); + Assert.NotSame (tf, Application.Current.Focused); + Application.Current.Dispose (); } [Fact] @@ -46,9 +46,9 @@ public class AppendAutocompleteTests (ITestOutputHelper output) // f is typed and suggestion is "fish" Application.RaiseKeyDownEvent ('f'); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.PositionCursor (); DriverAssert.AssertDriverContentsAre ("fish", output); Assert.Equal ("f", tf.Text); @@ -63,13 +63,13 @@ public class AppendAutocompleteTests (ITestOutputHelper output) // Should reappear when you press next letter Application.RaiseKeyDownEvent (Key.I); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.PositionCursor (); DriverAssert.AssertDriverContentsAre ("fish", output); Assert.Equal ("fi", tf.Text); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Theory] @@ -82,9 +82,9 @@ public class AppendAutocompleteTests (ITestOutputHelper output) // f is typed and suggestion is "fish" Application.RaiseKeyDownEvent ('f'); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.PositionCursor (); DriverAssert.AssertDriverContentsAre ("fish", output); Assert.Equal ("f", tf.Text); @@ -92,22 +92,22 @@ public class AppendAutocompleteTests (ITestOutputHelper output) // When cycling autocomplete Application.RaiseKeyDownEvent (cycleKey); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.PositionCursor (); DriverAssert.AssertDriverContentsAre ("friend", output); Assert.Equal ("f", tf.Text); // Should be able to cycle in circles endlessly Application.RaiseKeyDownEvent (cycleKey); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.PositionCursor (); DriverAssert.AssertDriverContentsAre ("fish", output); Assert.Equal ("f", tf.Text); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Fact] @@ -118,9 +118,9 @@ public class AppendAutocompleteTests (ITestOutputHelper output) // f is typed and suggestion is "fish" Application.RaiseKeyDownEvent ('f'); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.PositionCursor (); DriverAssert.AssertDriverContentsAre ("fish", output); Assert.Equal ("f", tf.Text); @@ -129,11 +129,11 @@ public class AppendAutocompleteTests (ITestOutputHelper output) Application.RaiseKeyDownEvent (' '); Application.RaiseKeyDownEvent (Key.CursorLeft); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); DriverAssert.AssertDriverContentsAre ("f", output); Assert.Equal ("f ", tf.Text); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Fact] @@ -144,20 +144,20 @@ public class AppendAutocompleteTests (ITestOutputHelper output) // f is typed and suggestion is "fish" Application.RaiseKeyDownEvent ('f'); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.PositionCursor (); DriverAssert.AssertDriverContentsAre ("fish", output); Assert.Equal ("f", tf.Text); // x is typed and suggestion should disappear Application.RaiseKeyDownEvent (Key.X); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); DriverAssert.AssertDriverContentsAre ("fx", output); Assert.Equal ("fx", tf.Text); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Fact] @@ -170,9 +170,9 @@ public class AppendAutocompleteTests (ITestOutputHelper output) var generator = (SingleWordSuggestionGenerator)tf.Autocomplete.SuggestionGenerator; generator.AllSuggestions = new() { "FISH" }; - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.PositionCursor (); DriverAssert.AssertDriverContentsAre ("", output); tf.NewKeyDownEvent (Key.M); @@ -182,20 +182,20 @@ public class AppendAutocompleteTests (ITestOutputHelper output) Assert.Equal ("my f", tf.Text); // Even though there is no match on case we should still get the suggestion - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.PositionCursor (); DriverAssert.AssertDriverContentsAre ("my fISH", output); Assert.Equal ("my f", tf.Text); // When tab completing the case of the whole suggestion should be applied Application.RaiseKeyDownEvent ('\t'); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); DriverAssert.AssertDriverContentsAre ("my FISH", output); Assert.Equal ("my FISH", tf.Text); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Fact] @@ -208,35 +208,35 @@ public class AppendAutocompleteTests (ITestOutputHelper output) var generator = (SingleWordSuggestionGenerator)tf.Autocomplete.SuggestionGenerator; generator.AllSuggestions = new() { "fish" }; - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.PositionCursor (); DriverAssert.AssertDriverContentsAre ("", output); tf.NewKeyDownEvent (new ('f')); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.PositionCursor (); DriverAssert.AssertDriverContentsAre ("fish", output); Assert.Equal ("f", tf.Text); Application.RaiseKeyDownEvent ('\t'); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); DriverAssert.AssertDriverContentsAre ("fish", output); Assert.Equal ("fish", tf.Text); // Tab should autcomplete but not move focus - Assert.Same (tf, Application.Top.Focused); + Assert.Same (tf, Application.Current.Focused); // Second tab should move focus (nothing to autocomplete) Application.RaiseKeyDownEvent ('\t'); - Assert.NotSame (tf, Application.Top.Focused); - Application.Top.Dispose (); + Assert.NotSame (tf, Application.Current.Focused); + Application.Current.Dispose (); } [Theory] @@ -250,13 +250,13 @@ public class AppendAutocompleteTests (ITestOutputHelper output) // f is typed we should only see 'f' up to size of View (10) Application.RaiseKeyDownEvent ('f'); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.PositionCursor (); DriverAssert.AssertDriverContentsAre (expectRender, output); Assert.Equal ("f", tf.Text); - Application.Top.Dispose (); + Application.Current.Dispose (); } private TextField GetTextFieldsInView () diff --git a/Tests/UnitTests/Views/ButtonTests.cs b/Tests/UnitTests/Views/ButtonTests.cs index d05a3fc53..83d8cc032 100644 --- a/Tests/UnitTests/Views/ButtonTests.cs +++ b/Tests/UnitTests/Views/ButtonTests.cs @@ -13,7 +13,10 @@ public class ButtonTests (ITestOutputHelper output) // Override CM Button.DefaultShadow = ShadowStyle.None; - var btn = new Button (); + var btn = new Button () + { + App = ApplicationImpl.Instance + }; Assert.Equal (string.Empty, btn.Text); btn.BeginInit (); btn.EndInit (); @@ -45,7 +48,8 @@ public class ButtonTests (ITestOutputHelper output) DriverAssert.AssertDriverContentsWithFrameAre (expected, output); btn.Dispose (); - btn = new () { Text = "_Test", IsDefault = true }; + btn = new () { App = ApplicationImpl.Instance, + Text = "_Test", IsDefault = true }; btn.Layout (); Assert.Equal (new (10, 1), btn.TextFormatter.ConstrainToSize); @@ -77,7 +81,8 @@ public class ButtonTests (ITestOutputHelper output) btn.Dispose (); - btn = new () { X = 1, Y = 2, Text = "_abc", IsDefault = true }; + btn = new () { App = ApplicationImpl.Instance, + X = 1, Y = 2, Text = "_abc", IsDefault = true }; btn.BeginInit (); btn.EndInit (); Assert.Equal ("_abc", btn.Text); @@ -92,13 +97,13 @@ public class ButtonTests (ITestOutputHelper output) Assert.Equal ('_', btn.HotKeySpecifier.Value); Assert.True (btn.CanFocus); - Application.Driver?.ClearContents (); + ApplicationImpl.Instance.Driver?.ClearContents (); btn.Draw (); expected = @$" {Glyphs.LeftBracket}{Glyphs.LeftDefaultIndicator} abc {Glyphs.RightDefaultIndicator}{Glyphs.RightBracket} "; - DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + DriverAssert.AssertDriverContentsWithFrameAre (expected, output, ApplicationImpl.Instance.Driver); Assert.Equal (new (0, 0, 9, 1), btn.Viewport); Assert.Equal (new (1, 2, 9, 1), btn.Frame); @@ -204,7 +209,7 @@ public class ButtonTests (ITestOutputHelper output) clicked = false; // Toplevel does not handle Enter, so it should get passed on to button - Assert.False (Application.Top.NewKeyDownEvent (Key.Enter)); + Assert.False (Application.Current.NewKeyDownEvent (Key.Enter)); Assert.True (clicked); clicked = false; diff --git a/Tests/UnitTests/Views/CheckBoxTests.cs b/Tests/UnitTests/Views/CheckBoxTests.cs index adfde4db8..39fe5db91 100644 --- a/Tests/UnitTests/Views/CheckBoxTests.cs +++ b/Tests/UnitTests/Views/CheckBoxTests.cs @@ -15,12 +15,11 @@ public class CheckBoxTests (ITestOutputHelper output) [Fact] public void Commands_Select () { - Application.Navigation = new (); - Application.Top = new (); + Application.Current = new (); View otherView = new () { CanFocus = true }; var ckb = new CheckBox (); - Application.Top.Add (ckb, otherView); - Application.Top.SetFocus (); + Application.Current.Add (ckb, otherView); + Application.Current.SetFocus (); Assert.True (ckb.HasFocus); var checkedStateChangingCount = 0; @@ -64,7 +63,7 @@ public class CheckBoxTests (ITestOutputHelper output) Assert.Equal (3, selectCount); Assert.Equal (1, acceptCount); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (); } diff --git a/Tests/UnitTests/Views/ColorPickerTests.cs b/Tests/UnitTests/Views/ColorPickerTests.cs index e6beba010..8e454a289 100644 --- a/Tests/UnitTests/Views/ColorPickerTests.cs +++ b/Tests/UnitTests/Views/ColorPickerTests.cs @@ -12,7 +12,7 @@ public class ColorPickerTests var otherView = new View { CanFocus = true }; - Application.Top?.Add (otherView); // thi sets focus to otherView + Application.Current?.Add (otherView); // thi sets focus to otherView Assert.True (otherView.HasFocus); cp.SetFocus (); @@ -772,13 +772,11 @@ public class ColorPickerTests cp.Style.ShowColorName = showName; cp.ApplyStyleChanges (); - Application.Navigation = new (); + Application.Current = new () { Width = 20, Height = 5 }; + Application.Current.Add (cp); - Application.Top = new () { Width = 20, Height = 5 }; - Application.Top.Add (cp); - - Application.Top.LayoutSubViews (); - Application.Top.SetFocus (); + Application.Current.LayoutSubViews (); + Application.Current.SetFocus (); return cp; } diff --git a/Tests/UnitTests/Views/ComboBoxTests.cs b/Tests/UnitTests/Views/ComboBoxTests.cs index e1280424c..260251659 100644 --- a/Tests/UnitTests/Views/ComboBoxTests.cs +++ b/Tests/UnitTests/Views/ComboBoxTests.cs @@ -565,7 +565,7 @@ Three ", Assert.True (cb.IsShow); Assert.Equal (-1, cb.SelectedItem); Assert.Equal ("", cb.Text); - View.SetClipToScreen (); + cb.SetClipToScreen (); cb.Draw (); DriverAssert.AssertDriverAttributesAre ( @@ -584,7 +584,7 @@ Three ", Assert.True (cb.IsShow); Assert.Equal (-1, cb.SelectedItem); Assert.Equal ("", cb.Text); - View.SetClipToScreen (); + cb.SetClipToScreen (); cb.Draw (); DriverAssert.AssertDriverAttributesAre ( @@ -609,7 +609,7 @@ Three ", Assert.True (cb.IsShow); Assert.Equal (2, cb.SelectedItem); Assert.Equal ("Three", cb.Text); - View.SetClipToScreen (); + cb.SetClipToScreen (); cb.Draw (); DriverAssert.AssertDriverAttributesAre ( @@ -628,7 +628,7 @@ Three ", Assert.True (cb.IsShow); Assert.Equal (2, cb.SelectedItem); Assert.Equal ("Three", cb.Text); - View.SetClipToScreen (); + cb.SetClipToScreen (); cb.Draw (); DriverAssert.AssertDriverAttributesAre ( @@ -647,7 +647,7 @@ Three ", Assert.True (cb.IsShow); Assert.Equal (2, cb.SelectedItem); Assert.Equal ("Three", cb.Text); - View.SetClipToScreen (); + cb.SetClipToScreen (); cb.Draw (); DriverAssert.AssertDriverAttributesAre ( @@ -928,7 +928,7 @@ One Assert.Equal (1, cb.SelectedItem); Assert.Equal ("Two", cb.Text); - View.SetClipToScreen (); + cb.SetClipToScreen (); cb.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -944,7 +944,7 @@ Two Assert.Equal (2, cb.SelectedItem); Assert.Equal ("Three", cb.Text); - View.SetClipToScreen (); + cb.SetClipToScreen (); cb.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -1013,10 +1013,9 @@ Three [Fact] public void Source_Equal_Null_Or_Count_Equal_Zero_Sets_SelectedItem_Equal_To_Minus_One () { - Application.Navigation = new (); var cb = new ComboBox (); var top = new Toplevel (); - Application.Top = top; + Application.Current = top; top.Add (cb); top.FocusDeepest (NavigationDirection.Forward, null); @@ -1055,7 +1054,7 @@ Three Assert.Equal (-1, cb.SelectedItem); Assert.Equal ("", cb.Text); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } } diff --git a/Tests/UnitTests/Views/GraphViewTests.cs b/Tests/UnitTests/Views/GraphViewTests.cs index d2184945b..7bcb42a4e 100644 --- a/Tests/UnitTests/Views/GraphViewTests.cs +++ b/Tests/UnitTests/Views/GraphViewTests.cs @@ -44,7 +44,7 @@ internal class FakeVAxis : VerticalAxis #endregion -public class GraphViewTests +public class GraphViewTests : FakeDriverBase { /// /// A cell size of 0 would result in mapping all graph space into the same cell of the console. Since @@ -74,7 +74,10 @@ public class GraphViewTests /// public static GraphView GetGraph () { - var gv = new GraphView (); + var gv = new GraphView () + { + Driver = Application.Driver ?? CreateFakeDriver () + }; gv.BeginInit (); gv.EndInit (); @@ -650,7 +653,7 @@ public class MultiBarSeriesTests fakeXAxis.LabelPoints.Clear (); gv.LayoutSubViews (); gv.SetNeedsDraw (); - View.SetClipToScreen (); + gv.SetClipToScreen (); gv.Draw (); Assert.Equal (3, fakeXAxis.LabelPoints.Count); @@ -677,12 +680,13 @@ public class MultiBarSeriesTests } } -public class BarSeriesTests +public class BarSeriesTests : FakeDriverBase { [Fact] public void TestOneLongOneShortHorizontalBars_WithOffset () { GraphView graph = GetGraph (out FakeBarSeries barSeries, out FakeHAxis axisX, out FakeVAxis axisY); + graph.Driver = CreateFakeDriver (); graph.Draw (); // no bars @@ -1125,7 +1129,7 @@ public class TextAnnotationTests // user scrolls up one unit of graph space gv.ScrollOffset = new PointF (0, 1f); gv.SetNeedsDraw (); - View.SetClipToScreen (); + gv.SetClipToScreen (); gv.Draw (); // we expect the text annotation to go down one line since @@ -1222,7 +1226,7 @@ public class TextAnnotationTests new TextAnnotation { Text = "hey!", ScreenPosition = new Point (3, 1) } ); gv.LayoutSubViews (); - View.SetClipToScreen (); + gv.SetClipToScreen (); gv.Draw (); var expected = @@ -1238,7 +1242,7 @@ public class TextAnnotationTests // user scrolls up one unit of graph space gv.ScrollOffset = new PointF (0, 1f); gv.SetNeedsDraw (); - View.SetClipToScreen (); + gv.SetClipToScreen (); gv.Draw (); // we expect no change in the location of the annotation (only the axis label changes) @@ -1257,7 +1261,7 @@ public class TextAnnotationTests // user scrolls up one unit of graph space gv.ScrollOffset = new PointF (0, 1f); gv.SetNeedsDraw (); - View.SetClipToScreen (); + gv.SetClipToScreen (); gv.Draw (); // we expect no change in the location of the annotation (only the axis label changes) @@ -1385,7 +1389,7 @@ public class PathAnnotationTests "; - DriverAssert.AssertDriverContentsAre (expected, _output); + DriverAssert.AssertDriverContentsAre (expected, _output, gv.Driver); // Shutdown must be called to safely clean up Application if Init has been called Application.Shutdown (); @@ -1410,7 +1414,7 @@ public class PathAnnotationTests "; - DriverAssert.AssertDriverContentsAre (expected, _output); + DriverAssert.AssertDriverContentsAre (expected, _output, gv.Driver); // Shutdown must be called to safely clean up Application if Init has been called Application.Shutdown (); @@ -1528,7 +1532,7 @@ public class PathAnnotationTests // change the text and redraw view.Text = "ff1234"; mount.SetNeedsDraw (); - View.SetClipToScreen (); + top.SetClipToScreen (); mount.Draw (); // should have the new text rendered diff --git a/Tests/UnitTests/Views/HexViewTests.cs b/Tests/UnitTests/Views/HexViewTests.cs index 83d661f6f..63ea65cea 100644 --- a/Tests/UnitTests/Views/HexViewTests.cs +++ b/Tests/UnitTests/Views/HexViewTests.cs @@ -1,6 +1,5 @@ #nullable enable using System.Text; -using JetBrains.Annotations; namespace UnitTests.ViewsTests; @@ -32,10 +31,10 @@ public class HexViewTests public void ReadOnly_Prevents_Edits () { var hv = new HexView (LoadStream (null, out _, true)) { Width = 20, Height = 20 }; - Application.Navigation = new ApplicationNavigation (); - Application.Top = new Toplevel (); - Application.Top.Add (hv); - Application.Top.SetFocus (); + + Application.Current = new (); + Application.Current.Add (hv); + Application.Current.SetFocus (); // Needed because HexView relies on LayoutComplete to calc sizes hv.LayoutSubViews (); @@ -70,15 +69,14 @@ public class HexViewTests Assert.Empty (hv.Edits); Assert.Equal (127, hv.Source.Length); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } [Fact] public void ApplyEdits_With_Argument () { - Application.Navigation = new ApplicationNavigation (); - Application.Top = new Toplevel (); + Application.Current = new (); byte [] buffer = Encoding.Default.GetBytes ("Fest"); var original = new MemoryStream (); @@ -89,8 +87,8 @@ public class HexViewTests original.CopyTo (copy); copy.Flush (); var hv = new HexView (copy) { Width = Dim.Fill (), Height = Dim.Fill () }; - Application.Top.Add (hv); - Application.Top.SetFocus (); + Application.Current.Add (hv); + Application.Current.SetFocus (); // Needed because HexView relies on LayoutComplete to calc sizes hv.LayoutSubViews (); @@ -107,7 +105,7 @@ public class HexViewTests Assert.Equal ("Test", Encoding.Default.GetString (readBuffer)); Assert.True (Application.RaiseKeyDownEvent (Key.Tab)); // Move to right side - Assert.True (Application.RaiseKeyDownEvent (Key.CursorLeft)); + Assert.True (Application.RaiseKeyDownEvent (Key.CursorLeft)); Assert.True (Application.RaiseKeyDownEvent (Key.Z.WithShift)); readBuffer [hv.Edits.ToList () [0].Key] = hv.Edits.ToList () [0].Value; Assert.Equal ("Zest", Encoding.Default.GetString (readBuffer)); @@ -121,7 +119,7 @@ public class HexViewTests Assert.Equal ("Zest", Encoding.Default.GetString (readBuffer)); Assert.Equal (Encoding.Default.GetString (buffer), Encoding.Default.GetString (readBuffer)); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -144,13 +142,11 @@ public class HexViewTests [Fact] public void Position_Encoding_Default () { - Application.Navigation = new ApplicationNavigation (); - var hv = new HexView (LoadStream (null, out _)) { Width = 100, Height = 100 }; - Application.Top = new Toplevel (); - Application.Top.Add (hv); + Application.Current = new (); + Application.Current.Add (hv); - Application.Top.LayoutSubViews (); + Application.Current.LayoutSubViews (); Assert.Equal (63, hv.Source!.Length); Assert.Equal (20, hv.BytesPerLine); @@ -175,18 +171,16 @@ public class HexViewTests Assert.Equal (new (3, 3), hv.GetPosition (hv.Address)); Assert.Equal (hv.Source!.Length, hv.Address); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } [Fact] public void Position_Encoding_Unicode () { - Application.Navigation = new ApplicationNavigation (); - - var hv = new HexView (LoadStream (null, out _, unicode: true)) { Width = 100, Height = 100 }; - Application.Top = new Toplevel (); - Application.Top.Add (hv); + var hv = new HexView (LoadStream (null, out _, true)) { Width = 100, Height = 100 }; + Application.Current = new (); + Application.Current.Add (hv); hv.LayoutSubViews (); @@ -212,7 +206,7 @@ public class HexViewTests Assert.Equal (new (6, 6), hv.GetPosition (hv.Address)); Assert.Equal (hv.Source!.Length, hv.Address); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -264,10 +258,9 @@ public class HexViewTests [Fact] public void KeyBindings_Test_Movement_LeftSide () { - Application.Navigation = new ApplicationNavigation (); - Application.Top = new Toplevel (); + Application.Current = new (); var hv = new HexView (LoadStream (null, out _)) { Width = 20, Height = 10 }; - Application.Top.Add (hv); + Application.Current.Add (hv); hv.LayoutSubViews (); @@ -313,19 +306,18 @@ public class HexViewTests Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp.WithCtrl)); Assert.Equal (0, hv.Address); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } [Fact] public void PositionChanged_Event () { - Application.Navigation = new ApplicationNavigation (); var hv = new HexView (LoadStream (null, out _)) { Width = 20, Height = 10 }; - Application.Top = new Toplevel (); - Application.Top.Add (hv); + Application.Current = new (); + Application.Current.Add (hv); - Application.Top.LayoutSubViews (); + Application.Current.LayoutSubViews (); HexViewEventArgs hexViewEventArgs = null!; hv.PositionChanged += (s, e) => hexViewEventArgs = e; @@ -339,41 +331,40 @@ public class HexViewTests Assert.Equal (4, hexViewEventArgs.BytesPerLine); Assert.Equal (new (1, 1), hexViewEventArgs.Position); Assert.Equal (5, hexViewEventArgs.Address); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } [Fact] public void Source_Sets_Address_To_Zero_If_Greater_Than_Source_Length () { - Application.Navigation = new ApplicationNavigation (); var hv = new HexView (LoadStream (null, out _)) { Width = 10, Height = 5 }; - Application.Top = new Toplevel (); - Application.Top.Add (hv); + Application.Current = new (); + Application.Current.Add (hv); - Application.Top.Layout (); + Application.Current.Layout (); Assert.True (hv.NewKeyDownEvent (Key.End)); Assert.Equal (MEM_STRING_LENGTH, hv.Address); hv.Source = new MemoryStream (); - Application.Top.Layout (); + Application.Current.Layout (); Assert.Equal (0, hv.Address); hv.Source = LoadStream (null, out _); hv.Width = Dim.Fill (); hv.Height = Dim.Fill (); - Application.Top.Layout (); + Application.Current.Layout (); Assert.Equal (0, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.End)); Assert.Equal (MEM_STRING_LENGTH, hv.Address); hv.Source = new MemoryStream (); - Application.Top.Layout (); + Application.Current.Layout (); Assert.Equal (0, hv.Address); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -400,6 +391,7 @@ public class HexViewTests { bArray = Encoding.Default.GetBytes (memString); } + numBytesInMemString = bArray.Length; stream.Write (bArray); @@ -421,8 +413,8 @@ public class HexViewTests } public override void Flush () { baseStream.Flush (); } - public override int Read (byte [] buffer, int offset, int count) { return baseStream.Read (buffer, offset, count); } - public override long Seek (long offset, SeekOrigin origin) { throw new NotImplementedException (); } + public override int Read (byte [] buffer, int offset, int count) => baseStream.Read (buffer, offset, count); + public override long Seek (long offset, SeekOrigin origin) => throw new NotImplementedException (); public override void SetLength (long value) { throw new NotSupportedException (); } public override void Write (byte [] buffer, int offset, int count) { baseStream.Write (buffer, offset, count); } } diff --git a/Tests/UnitTests/Views/LabelTests.cs b/Tests/UnitTests/Views/LabelTests.cs index 550abf360..7b949c459 100644 --- a/Tests/UnitTests/Views/LabelTests.cs +++ b/Tests/UnitTests/Views/LabelTests.cs @@ -1,5 +1,4 @@ -using UnitTests; -using Xunit.Abstractions; +using Xunit.Abstractions; namespace UnitTests.ViewsTests; @@ -112,9 +111,17 @@ public class LabelTests (ITestOutputHelper output) AutoInitShutdownAttribute.RunIteration (); - tf1.Draw (new (new (0, 1), tfSize), label.GetAttributeForRole (VisualRole.Normal), label.GetAttributeForRole (VisualRole.HotNormal)); + tf1.Draw ( + Application.Driver, + new (new (0, 1), tfSize), + label.GetAttributeForRole (VisualRole.Normal), + label.GetAttributeForRole (VisualRole.HotNormal)); - tf2.Draw (new (new (0, 2), tfSize), label.GetAttributeForRole (VisualRole.Normal), label.GetAttributeForRole (VisualRole.HotNormal)); + tf2.Draw ( + Application.Driver, + new (new (0, 2), tfSize), + label.GetAttributeForRole (VisualRole.Normal), + label.GetAttributeForRole (VisualRole.HotNormal)); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -135,10 +142,20 @@ This TextFormatter (tf2) with fill will be cleared on rewritten. ", label.Draw (); tf1.Text = "This TextFormatter (tf1) is rewritten."; - tf1.Draw (new (new (0, 1), tfSize), label.GetAttributeForRole (VisualRole.Normal), label.GetAttributeForRole (VisualRole.HotNormal)); + + tf1.Draw ( + Application.Driver, + new (new (0, 1), tfSize), + label.GetAttributeForRole (VisualRole.Normal), + label.GetAttributeForRole (VisualRole.HotNormal)); tf2.Text = "This TextFormatter (tf2) is rewritten."; - tf2.Draw (new (new (0, 2), tfSize), label.GetAttributeForRole (VisualRole.Normal), label.GetAttributeForRole (VisualRole.HotNormal)); + + tf2.Draw ( + Application.Driver, + new (new (0, 2), tfSize), + label.GetAttributeForRole (VisualRole.Normal), + label.GetAttributeForRole (VisualRole.HotNormal)); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -301,7 +318,11 @@ e [SetupFakeApplication] public void Full_Border () { - var label = new Label { BorderStyle = LineStyle.Single, Text = "Test" }; + var label = new Label + { + Driver = Application.Driver, + BorderStyle = LineStyle.Single, Text = "Test" + }; label.BeginInit (); label.EndInit (); label.SetRelativeLayout (Application.Screen.Size); @@ -699,6 +720,7 @@ e var label = new Label { Text = "This should be the last line.", + //Width = Dim.Fill (), X = 0, // keep unit test focused; don't use Center here Y = Pos.AnchorEnd (1) @@ -745,6 +767,7 @@ e var label = new Label { Text = "This should be the last line.", + //Width = Dim.Fill (), X = 0, Y = Pos.Bottom (win) @@ -907,7 +930,11 @@ e label.Width = Dim.Fill () - text.Length; label.Height = 0; - var win = new View { CanFocus = true, BorderStyle = LineStyle.Single, Width = Dim.Fill (), Height = Dim.Fill () }; + var win = new View + { + App = ApplicationImpl.Instance, + CanFocus = true, BorderStyle = LineStyle.Single, Width = Dim.Fill (), Height = Dim.Fill () + }; win.Add (label); win.BeginInit (); win.EndInit (); @@ -1071,7 +1098,7 @@ e Assert.Equal (new (5, 1), label.TextFormatter.ConstrainToSize); Assert.Equal (["Label"], label.TextFormatter.GetLines ()); Assert.Equal (new (0, 0, 10, 4), win.Frame); - Assert.Equal (new (0, 0, 10, 4), Application.Top.Frame); + Assert.Equal (new (0, 0, 10, 4), Application.Current.Frame); var expected = @" ┌────────┐ @@ -1130,7 +1157,7 @@ e Assert.Equal (new (5, 1), label.TextFormatter.ConstrainToSize); Assert.Equal (["Label"], label.TextFormatter.GetLines ()); Assert.Equal (new (0, 0, 10, 4), win.Frame); - Assert.Equal (new (0, 0, 10, 4), Application.Top.Frame); + Assert.Equal (new (0, 0, 10, 4), Application.Current.Frame); var expected = @" ┌────────┐ @@ -1202,11 +1229,11 @@ e Text = "nextView", CanFocus = true }; - Application.Navigation = new (); - Application.Top = new (); - Application.Top.Add (otherView, label, nextView); - Application.Top.SetFocus (); + Application.Current = new (); + Application.Current.Add (otherView, label, nextView); + + Application.Current.SetFocus (); Assert.True (otherView.HasFocus); Assert.True (Application.RaiseKeyDownEvent (label.HotKey)); @@ -1214,7 +1241,7 @@ e Assert.False (label.HasFocus); Assert.True (nextView.HasFocus); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (); } @@ -1224,19 +1251,18 @@ e View otherView = new () { X = 0, Y = 0, Width = 1, Height = 1, Id = "otherView", CanFocus = true }; Label label = new () { X = 0, Y = 1, Text = "_label" }; View nextView = new () { X = Pos.Right (label), Y = Pos.Top (label), Width = 1, Height = 1, Id = "nextView", CanFocus = true }; - Application.Navigation = new (); - Application.Top = new (); - Application.Top.Add (otherView, label, nextView); - Application.Top.Layout (); + Application.Current = new (); + Application.Current.Add (otherView, label, nextView); + Application.Current.Layout (); - Application.Top.SetFocus (); + Application.Current.SetFocus (); // click on label Application.RaiseMouseEvent (new () { ScreenPosition = label.Frame.Location, Flags = MouseFlags.Button1Clicked }); Assert.False (label.HasFocus); Assert.True (nextView.HasFocus); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (); } @@ -1254,9 +1280,9 @@ e Text = "view", CanFocus = true }; - Application.Navigation = new (); - Application.Top = new (); - Application.Top.Add (label, view); + + Application.Current = new (); + Application.Current.Add (label, view); view.SetFocus (); Assert.True (label.CanFocus); @@ -1269,15 +1295,13 @@ e Assert.True (label.HasFocus); Assert.False (view.HasFocus); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (); } [Fact] public void CanFocus_True_MouseClick_Focuses () { - Application.Navigation = new (); - Label label = new () { Text = "label", @@ -1296,14 +1320,14 @@ e CanFocus = true }; - Application.Top = new () + Application.Current = new () { Width = 10, Height = 10 }; - Application.Top.Add (label, otherView); - Application.Top.SetFocus (); - Application.Top.Layout (); + Application.Current.Add (label, otherView); + Application.Current.SetFocus (); + Application.Current.Layout (); Assert.True (label.CanFocus); Assert.True (label.HasFocus); @@ -1323,7 +1347,7 @@ e Assert.False (label.HasFocus); Assert.True (otherView.HasFocus); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (); } diff --git a/Tests/UnitTests/Views/MenuBarTests.cs b/Tests/UnitTests/Views/MenuBarTests.cs index 8ad5f05eb..b77ebc1ad 100644 --- a/Tests/UnitTests/Views/MenuBarTests.cs +++ b/Tests/UnitTests/Views/MenuBarTests.cs @@ -10,18 +10,33 @@ public class MenuBarTests () public void DefaultKey_Activates_And_Opens () { // Arrange + var top = new Toplevel () + { + App = ApplicationImpl.Instance + }; + + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + top.Add (menuBar); + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; var menu = new Menuv2 ([menuItem]) { Id = "menu" }; var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; var menuBarItemPopover = new PopoverMenu (); + + menuBar.Add (menuBarItem); menuBarItem.PopoverMenu = menuBarItemPopover; menuBarItemPopover.Root = menu; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; - menuBar.Add (menuBarItem); + + + Assert.NotNull (menuBar.App); + Assert.NotNull (menu.App); + Assert.NotNull (menuItem.App); + Assert.NotNull (menuBarItem); + Assert.NotNull (menuBarItemPopover); + Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); - var top = new Toplevel (); - top.Add (menuBar); + SessionToken rs = Application.Begin (top); Assert.False (menuBar.Active); @@ -43,17 +58,10 @@ public class MenuBarTests () public void DefaultKey_Deactivates () { // Arrange - var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; - var menuBarItemPopover = new PopoverMenu (); - menuBarItem.PopoverMenu = menuBarItemPopover; - menuBarItemPopover.Root = menu; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; - menuBar.Add (menuBarItem); - Assert.Single (menuBar.SubViews); - Assert.Single (menuBarItem.SubViews); - var top = new Toplevel (); + var top = new Toplevel () { App = ApplicationImpl.Instance }; + MenuBarv2 menuBar = new MenuBarv2 () { App = ApplicationImpl.Instance }; + menuBar.EnableForDesign (ref top); + top.Add (menuBar); SessionToken rs = Application.Begin (top); Assert.False (menuBar.Active); @@ -61,15 +69,12 @@ public class MenuBarTests () // Act Application.RaiseKeyDownEvent (MenuBarv2.DefaultKey); Assert.True (menuBar.IsOpen ()); - Assert.True (menuBarItem.PopoverMenu.Visible); Application.RaiseKeyDownEvent (MenuBarv2.DefaultKey); Assert.False (menuBar.Active); Assert.False (menuBar.IsOpen ()); Assert.False (menuBar.HasFocus); Assert.False (menuBar.CanFocus); - Assert.False (menuBarItem.PopoverMenu.Visible); - Assert.False (menuBarItem.PopoverMenu.HasFocus); Application.End (rs); top.Dispose (); @@ -377,17 +382,10 @@ public class MenuBarTests () public void Mouse_Click_Activates_And_Opens () { // Arrange - var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; - var menuBarItemPopover = new PopoverMenu (); - menuBarItem.PopoverMenu = menuBarItemPopover; - menuBarItemPopover.Root = menu; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; - menuBar.Add (menuBarItem); - Assert.Single (menuBar.SubViews); - Assert.Single (menuBarItem.SubViews); - var top = new Toplevel (); + var top = new Toplevel () { App = ApplicationImpl.Instance }; + MenuBarv2 menuBar = new MenuBarv2 () { App = ApplicationImpl.Instance }; + menuBar.EnableForDesign (ref top); + top.Add (menuBar); SessionToken rs = Application.Begin (top); Assert.False (menuBar.Active); @@ -401,8 +399,6 @@ public class MenuBarTests () Assert.True (menuBar.IsOpen ()); Assert.True (menuBar.HasFocus); Assert.True (menuBar.CanFocus); - Assert.True (menuBarItem.PopoverMenu.Visible); - Assert.True (menuBarItem.PopoverMenu.HasFocus); Application.End (rs); top.Dispose (); @@ -483,7 +479,7 @@ public class MenuBarTests () Application.RaiseKeyDownEvent (Key.N.WithAlt); Assert.Equal (0, action); - Assert.Equal(Key.I, menuItem.HotKey); + Assert.Equal (Key.I, menuItem.HotKey); Application.RaiseKeyDownEvent (Key.I); Assert.Equal (1, action); Assert.False (menuBar.Active); diff --git a/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs b/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs index 31ec42bde..1241716ca 100644 --- a/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs +++ b/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs @@ -698,7 +698,7 @@ public class MenuBarv1Tests (ITestOutputHelper output) Dialog.DefaultShadow = ShadowStyle.None; Button.DefaultShadow = ShadowStyle.None; - Assert.Equal (new (0, 0, 40, 15), View.GetClip ()!.GetBounds ()); + Assert.Equal (new (0, 0, 40, 15), Application.Current!.GetClip ()!.GetBounds ()); DriverAssert.AssertDriverContentsWithFrameAre (@"", output); List items = new () @@ -1100,11 +1100,11 @@ wo Assert.False (copyAction); #if SUPPORT_ALT_TO_ACTIVATE_MENU - Assert.False (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); - Assert.False (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); - Assert.True (Application.Top.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); + Assert.False (Application.Current.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); + Assert.False (Application.Current.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); + Assert.True (Application.Current.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); Assert.True (menu.IsMenuOpen); - Application.Top.Draw (); + Application.Current.Draw (); string expected = @" File Edit @@ -1113,26 +1113,26 @@ wo var pos = DriverAsserts.AssertDriverContentsWithFrameAre (expected, output); Assert.Equal (new (1, 0, 11, 1), pos); - Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.N))); + Assert.True (Application.Current.ProcessKeyDown (new KeyEventArgs (Key.N))); AutoInitShutdownAttribute.RunIteration (); Assert.False (newAction); // not yet, hot keys don't work if the item is not visible - Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.F))); + Assert.True (Application.Current.ProcessKeyDown (new KeyEventArgs (Key.F))); AutoInitShutdownAttribute.RunIteration (); - Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.N))); + Assert.True (Application.Current.ProcessKeyDown (new KeyEventArgs (Key.N))); AutoInitShutdownAttribute.RunIteration (); Assert.True (newAction); - Application.Top.Draw (); + Application.Current.Draw (); expected = @" File Edit "; - Assert.False (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); - Assert.True (Application.Top.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); - Assert.True (Application.Top.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); + Assert.False (Application.Current.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); + Assert.True (Application.Current.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); + Assert.True (Application.Current.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); Assert.True (menu.IsMenuOpen); - Application.Top.Draw (); + Application.Current.Draw (); expected = @" File Edit @@ -1141,8 +1141,8 @@ wo pos = DriverAsserts.AssertDriverContentsWithFrameAre (expected, output); Assert.Equal (new (1, 0, 11, 1), pos); - Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.CursorRight))); - Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.C))); + Assert.True (Application.Current.ProcessKeyDown (new KeyEventArgs (Key.CursorRight))); + Assert.True (Application.Current.ProcessKeyDown (new KeyEventArgs (Key.C))); AutoInitShutdownAttribute.RunIteration (); Assert.True (copyAction); #endif @@ -1211,19 +1211,19 @@ wo Assert.True (menu.NewKeyDownEvent (Key.F.WithAlt)); Assert.True (menu.IsMenuOpen); - Application.Top.Draw (); + Application.Current.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.N)); + Assert.True (Application.Current.SubViews.ElementAt (1).NewKeyDownEvent (Key.N)); AutoInitShutdownAttribute.RunIteration (); Assert.True (newAction); Assert.True (menu.NewKeyDownEvent (Key.E.WithAlt)); Assert.True (menu.IsMenuOpen); - Application.Top.Draw (); + Application.Current.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.C)); + Assert.True (Application.Current.SubViews.ElementAt (1).NewKeyDownEvent (Key.C)); AutoInitShutdownAttribute.RunIteration (); Assert.True (copyAction); top.Dispose (); @@ -1594,7 +1594,7 @@ wo Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); Assert.False (menu.IsMenuOpen); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); top.Dispose (); @@ -1692,7 +1692,7 @@ wo ); Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -1804,7 +1804,7 @@ wo ); Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -1945,7 +1945,7 @@ wo Application.AddTimeout (TimeSpan.Zero, () => { - Toplevel top = Application.Top; + Toplevel top = Application.Current; AutoInitShutdownAttribute.RunIteration (); @@ -2015,7 +2015,7 @@ wo Assert.True ( ((MenuBar)top.SubViews.ElementAt (0))._openMenu.NewKeyDownEvent (Key.CursorRight) ); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -2090,16 +2090,16 @@ wo DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); // Open second - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorRight)); + Assert.True (Application.Current.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorRight)); Assert.True (menu.IsMenuOpen); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); // Close menu Assert.True (menu.NewKeyDownEvent (menu.Key)); Assert.False (menu.IsMenuOpen); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); @@ -2133,21 +2133,21 @@ wo // Open first Assert.True (menu.NewKeyDownEvent (menu.Key)); Assert.True (menu.IsMenuOpen); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); // Open second Assert.True (top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorRight)); Assert.True (menu.IsMenuOpen); - View.SetClipToScreen (); - Application.Top.Draw (); + top.SetClipToScreen (); + Application.Current.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); // Close menu Assert.True (menu.NewKeyDownEvent (menu.Key)); Assert.False (menu.IsMenuOpen); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); top.Dispose (); @@ -2216,7 +2216,7 @@ wo top.Add (menu); Application.Begin (top); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); @@ -2224,7 +2224,7 @@ wo { menu.OpenMenu (i); Assert.True (menu.IsMenuOpen); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (i), output); } @@ -2483,7 +2483,7 @@ Edit Assert.True (menu.NewKeyDownEvent (menu.Key)); Assert.True (menu.IsMenuOpen); Assert.False (isMenuClosed); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); expected = @" @@ -2498,7 +2498,7 @@ Edit Assert.True (menu.NewKeyDownEvent (menu.Key)); Assert.False (menu.IsMenuOpen); Assert.True (isMenuClosed); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); expected = @" @@ -2655,7 +2655,7 @@ Edit Assert.Equal (1, menu._selected); Assert.Equal (-1, menu._selectedSub); Assert.Null (menu._openSubMenu); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); @@ -2663,7 +2663,7 @@ Edit Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); Assert.True (menu.IsMenuOpen); Assert.False (tf.HasFocus); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (2), output); @@ -2671,21 +2671,21 @@ Edit Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorLeft)); Assert.True (menu.IsMenuOpen); Assert.False (tf.HasFocus); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorLeft)); Assert.True (menu.IsMenuOpen); Assert.False (tf.HasFocus); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); Assert.True (Application.RaiseKeyDownEvent (menu.Key)); Assert.False (menu.IsMenuOpen); Assert.True (tf.HasFocus); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); top.Dispose (); @@ -2756,7 +2756,7 @@ Edit ); Assert.True (menu.IsMenuOpen); Assert.False (tf.HasFocus); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); @@ -2767,7 +2767,7 @@ Edit ); Assert.True (menu.IsMenuOpen); Assert.False (tf.HasFocus); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (2), output); @@ -2778,7 +2778,7 @@ Edit ); Assert.True (menu.IsMenuOpen); Assert.False (tf.HasFocus); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); @@ -2789,14 +2789,14 @@ Edit ); Assert.True (menu.IsMenuOpen); Assert.False (tf.HasFocus); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); Assert.True (menu.NewMouseEvent (new () { Position = new (8, 0), Flags = MouseFlags.Button1Pressed, View = menu })); Assert.False (menu.IsMenuOpen); Assert.True (tf.HasFocus); - View.SetClipToScreen (); + top.SetClipToScreen (); top.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); top.Dispose (); @@ -3075,7 +3075,7 @@ Edit pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorDown)); + Assert.True (Application.Current.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorDown)); top.Draw (); expected = @" @@ -3090,7 +3090,7 @@ Edit pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.True (Application.Top.SubViews.ElementAt (2).NewKeyDownEvent (Key.CursorLeft)); + Assert.True (Application.Current.SubViews.ElementAt (2).NewKeyDownEvent (Key.CursorLeft)); top.Draw (); expected = @" @@ -3104,7 +3104,7 @@ Edit pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.Esc)); + Assert.True (Application.Current.SubViews.ElementAt (1).NewKeyDownEvent (Key.Esc)); top.Draw (); expected = @" @@ -3186,7 +3186,7 @@ Edit menu.NewMouseEvent ( new () { - Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = Application.Top.SubViews.ElementAt (1) + Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = Application.Current.SubViews.ElementAt (1) } ); top.Draw (); @@ -3208,7 +3208,7 @@ Edit menu.NewMouseEvent ( new () { - Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = Application.Top.SubViews.ElementAt (1) + Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = Application.Current.SubViews.ElementAt (1) } ) ); @@ -3227,7 +3227,7 @@ Edit Assert.Equal (new (1, 0, 10, 6), pos); menu.NewMouseEvent ( - new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top } + new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Current } ); top.Draw (); diff --git a/Tests/UnitTests/Views/ProgressBarTests.cs b/Tests/UnitTests/Views/ProgressBarTests.cs index 26b70fe0e..b7bb6aaa2 100644 --- a/Tests/UnitTests/Views/ProgressBarTests.cs +++ b/Tests/UnitTests/Views/ProgressBarTests.cs @@ -26,9 +26,13 @@ public class ProgressBarTests [AutoInitShutdown] public void Fraction_Redraw () { - var driver = Application.Driver; + var driver = ApplicationImpl.Instance.Driver; - var pb = new ProgressBar { Width = 5 }; + var pb = new ProgressBar + { + Driver = driver, + Width = 5 + }; pb.BeginInit (); pb.EndInit (); @@ -37,7 +41,7 @@ public class ProgressBarTests for (var i = 0; i <= pb.Frame.Width; i++) { pb.Fraction += 0.2F; - View.SetClipToScreen (); + pb.SetClipToScreen (); pb.Draw (); if (i == 0) @@ -161,10 +165,11 @@ public class ProgressBarTests [AutoInitShutdown] public void Pulse_Redraw_BidirectionalMarquee_False () { - var driver = Application.Driver; + var driver = ApplicationImpl.Instance.Driver; var pb = new ProgressBar { + Driver = driver, Width = 15, ProgressBarStyle = ProgressBarStyle.MarqueeBlocks, BidirectionalMarquee = false }; @@ -175,7 +180,7 @@ public class ProgressBarTests for (var i = 0; i < 38; i++) { pb.Pulse (); - View.SetClipToScreen (); + pb.SetClipToScreen (); pb.Draw (); if (i == 0) @@ -869,9 +874,13 @@ public class ProgressBarTests [AutoInitShutdown] public void Pulse_Redraw_BidirectionalMarquee_True_Default () { - var driver = Application.Driver; + var driver = ApplicationImpl.Instance.Driver; - var pb = new ProgressBar { Width = 15, ProgressBarStyle = ProgressBarStyle.MarqueeBlocks }; + var pb = new ProgressBar + { + Driver = driver, + Width = 15, ProgressBarStyle = ProgressBarStyle.MarqueeBlocks + }; pb.BeginInit (); pb.EndInit (); @@ -880,7 +889,7 @@ public class ProgressBarTests for (var i = 0; i < 38; i++) { pb.Pulse (); - View.SetClipToScreen (); + pb.SetClipToScreen (); pb.Draw (); if (i == 0) diff --git a/Tests/UnitTests/Views/ScrollBarTests.cs b/Tests/UnitTests/Views/ScrollBarTests.cs index 9d0433832..4a5cdf7ad 100644 --- a/Tests/UnitTests/Views/ScrollBarTests.cs +++ b/Tests/UnitTests/Views/ScrollBarTests.cs @@ -495,6 +495,8 @@ public class ScrollBarTests (ITestOutputHelper output) { var super = new Window { + Driver = ApplicationImpl.Instance.Driver, + Id = "super", Width = width + 2, Height = height + 2, diff --git a/Tests/UnitTests/Views/ScrollSliderTests.cs b/Tests/UnitTests/Views/ScrollSliderTests.cs deleted file mode 100644 index 5397b16b1..000000000 --- a/Tests/UnitTests/Views/ScrollSliderTests.cs +++ /dev/null @@ -1,340 +0,0 @@ -using UnitTests; -using Xunit.Abstractions; - -namespace UnitTests.ViewsTests; - -public class ScrollSliderTests (ITestOutputHelper output) -{ - [Theory] - [SetupFakeApplication] - [InlineData ( - 3, - 10, - 1, - 0, - Orientation.Vertical, - @" -┌───┐ -│███│ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -└───┘")] - [InlineData ( - 10, - 1, - 3, - 0, - Orientation.Horizontal, - @" -┌──────────┐ -│███ │ -└──────────┘")] - [InlineData ( - 3, - 10, - 3, - 0, - Orientation.Vertical, - @" -┌───┐ -│███│ -│███│ -│███│ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -└───┘")] - - - - [InlineData ( - 3, - 10, - 5, - 0, - Orientation.Vertical, - @" -┌───┐ -│███│ -│███│ -│███│ -│███│ -│███│ -│ │ -│ │ -│ │ -│ │ -│ │ -└───┘")] - - [InlineData ( - 3, - 10, - 5, - 1, - Orientation.Vertical, - @" -┌───┐ -│ │ -│███│ -│███│ -│███│ -│███│ -│███│ -│ │ -│ │ -│ │ -│ │ -└───┘")] - [InlineData ( - 3, - 10, - 5, - 4, - Orientation.Vertical, - @" -┌───┐ -│ │ -│ │ -│ │ -│ │ -│███│ -│███│ -│███│ -│███│ -│███│ -│ │ -└───┘")] - [InlineData ( - 3, - 10, - 5, - 5, - Orientation.Vertical, - @" -┌───┐ -│ │ -│ │ -│ │ -│ │ -│ │ -│███│ -│███│ -│███│ -│███│ -│███│ -└───┘")] - [InlineData ( - 3, - 10, - 5, - 6, - Orientation.Vertical, - @" -┌───┐ -│ │ -│ │ -│ │ -│ │ -│ │ -│███│ -│███│ -│███│ -│███│ -│███│ -└───┘")] - - [InlineData ( - 3, - 10, - 10, - 0, - Orientation.Vertical, - @" -┌───┐ -│███│ -│███│ -│███│ -│███│ -│███│ -│███│ -│███│ -│███│ -│███│ -│███│ -└───┘")] - - [InlineData ( - 3, - 10, - 10, - 5, - Orientation.Vertical, - @" -┌───┐ -│███│ -│███│ -│███│ -│███│ -│███│ -│███│ -│███│ -│███│ -│███│ -│███│ -└───┘")] - [InlineData ( - 3, - 10, - 11, - 0, - Orientation.Vertical, - @" -┌───┐ -│███│ -│███│ -│███│ -│███│ -│███│ -│███│ -│███│ -│███│ -│███│ -│███│ -└───┘")] - - [InlineData ( - 10, - 3, - 5, - 0, - Orientation.Horizontal, - @" -┌──────────┐ -│█████ │ -│█████ │ -│█████ │ -└──────────┘")] - - [InlineData ( - 10, - 3, - 5, - 1, - Orientation.Horizontal, - @" -┌──────────┐ -│ █████ │ -│ █████ │ -│ █████ │ -└──────────┘")] - [InlineData ( - 10, - 3, - 5, - 4, - Orientation.Horizontal, - @" -┌──────────┐ -│ █████ │ -│ █████ │ -│ █████ │ -└──────────┘")] - [InlineData ( - 10, - 3, - 5, - 5, - Orientation.Horizontal, - @" -┌──────────┐ -│ █████│ -│ █████│ -│ █████│ -└──────────┘")] - [InlineData ( - 10, - 3, - 5, - 6, - Orientation.Horizontal, - @" -┌──────────┐ -│ █████│ -│ █████│ -│ █████│ -└──────────┘")] - - [InlineData ( - 10, - 3, - 10, - 0, - Orientation.Horizontal, - @" -┌──────────┐ -│██████████│ -│██████████│ -│██████████│ -└──────────┘")] - - [InlineData ( - 10, - 3, - 10, - 5, - Orientation.Horizontal, - @" -┌──────────┐ -│██████████│ -│██████████│ -│██████████│ -└──────────┘")] - [InlineData ( - 10, - 3, - 11, - 0, - Orientation.Horizontal, - @" -┌──────────┐ -│██████████│ -│██████████│ -│██████████│ -└──────────┘")] - public void Draws_Correctly (int superViewportWidth, int superViewportHeight, int sliderSize, int position, Orientation orientation, string expected) - { - var super = new Window - { - Id = "super", - Width = superViewportWidth + 2, - Height = superViewportHeight + 2 - }; - - var scrollSlider = new ScrollSlider - { - Orientation = orientation, - Size = sliderSize, - //Position = position, - }; - Assert.Equal (sliderSize, scrollSlider.Size); - super.Add (scrollSlider); - scrollSlider.Position = position; - - super.Layout (); - super.Draw (); - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - } -} diff --git a/Tests/UnitTests/Views/ShortcutTests.cs b/Tests/UnitTests/Views/ShortcutTests.cs index 09b4e66d1..79da2dbc6 100644 --- a/Tests/UnitTests/Views/ShortcutTests.cs +++ b/Tests/UnitTests/Views/ShortcutTests.cs @@ -23,7 +23,7 @@ public class ShortcutTests [InlineData (9, 0)] public void MouseClick_Raises_Accepted (int x, int expectedAccepted) { - Application.Top = new (); + Application.Current = new (); var shortcut = new Shortcut { @@ -31,8 +31,8 @@ public class ShortcutTests Text = "0", Title = "C" }; - Application.Top.Add (shortcut); - Application.Top.Layout (); + Application.Current.Add (shortcut); + Application.Current.Layout (); var accepted = 0; shortcut.Accepting += (s, e) => accepted++; @@ -46,7 +46,7 @@ public class ShortcutTests Assert.Equal (expectedAccepted, accepted); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -74,7 +74,7 @@ public class ShortcutTests int expectedShortcutSelected ) { - Application.Top = new (); + Application.Current = new (); var shortcut = new Shortcut { @@ -93,9 +93,9 @@ public class ShortcutTests var shortcutSelectCount = 0; shortcut.Selecting += (s, e) => { shortcutSelectCount++; }; - Application.Top.Add (shortcut); - Application.Top.SetRelativeLayout (new (100, 100)); - Application.Top.LayoutSubViews (); + Application.Current.Add (shortcut); + Application.Current.SetRelativeLayout (new (100, 100)); + Application.Current.LayoutSubViews (); Application.RaiseMouseEvent ( new () @@ -109,7 +109,7 @@ public class ShortcutTests Assert.Equal (expectedCommandViewAccepted, commandViewAcceptCount); Assert.Equal (expectedCommandViewSelected, commandViewSelectCount); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -130,7 +130,7 @@ public class ShortcutTests [InlineData (9, 0, 0)] public void MouseClick_Button_CommandView_Raises_Shortcut_Accepted (int mouseX, int expectedAccept, int expectedButtonAccept) { - Application.Top = new (); + Application.Current = new (); var shortcut = new Shortcut { @@ -147,9 +147,9 @@ public class ShortcutTests }; var buttonAccepted = 0; shortcut.CommandView.Accepting += (s, e) => { buttonAccepted++; }; - Application.Top.Add (shortcut); - Application.Top.SetRelativeLayout (new (100, 100)); - Application.Top.LayoutSubViews (); + Application.Current.Add (shortcut); + Application.Current.SetRelativeLayout (new (100, 100)); + Application.Current.LayoutSubViews (); var accepted = 0; shortcut.Accepting += (s, e) => { accepted++; }; @@ -164,7 +164,7 @@ public class ShortcutTests Assert.Equal (expectedAccept, accepted); Assert.Equal (expectedButtonAccept, buttonAccepted); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -186,7 +186,7 @@ public class ShortcutTests [InlineData (10, 1, 0)] public void MouseClick_CheckBox_CommandView_Raises_Shortcut_Accepted_Selected_Correctly (int mouseX, int expectedAccepted, int expectedCheckboxAccepted) { - Application.Top = new (); + Application.Current = new (); var shortcut = new Shortcut { @@ -212,9 +212,9 @@ public class ShortcutTests checkboxSelected++; }; - Application.Top.Add (shortcut); - Application.Top.SetRelativeLayout (new (100, 100)); - Application.Top.LayoutSubViews (); + Application.Current.Add (shortcut); + Application.Current.SetRelativeLayout (new (100, 100)); + Application.Current.LayoutSubViews (); var selected = 0; shortcut.Selecting += (s, e) => @@ -241,7 +241,7 @@ public class ShortcutTests Assert.Equal (expectedCheckboxAccepted, checkboxAccepted); Assert.Equal (expectedCheckboxAccepted, checkboxSelected); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -260,7 +260,7 @@ public class ShortcutTests [InlineData (false, KeyCode.F1, 0, 0)] public void KeyDown_Raises_Accepted_Selected (bool canFocus, KeyCode key, int expectedAccept, int expectedSelect) { - Application.Top = new (); + Application.Current = new (); var shortcut = new Shortcut { @@ -269,7 +269,7 @@ public class ShortcutTests Title = "_C", CanFocus = canFocus }; - Application.Top.Add (shortcut); + Application.Current.Add (shortcut); shortcut.SetFocus (); Assert.Equal (canFocus, shortcut.HasFocus); @@ -285,7 +285,7 @@ public class ShortcutTests Assert.Equal (expectedAccept, accepted); Assert.Equal (expectedSelect, selected); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -305,7 +305,7 @@ public class ShortcutTests [InlineData (false, KeyCode.F1, 0, 0)] public void KeyDown_CheckBox_Raises_Accepted_Selected (bool canFocus, KeyCode key, int expectedAccept, int expectedSelect) { - Application.Top = new (); + Application.Current = new (); var shortcut = new Shortcut { @@ -317,7 +317,7 @@ public class ShortcutTests }, CanFocus = canFocus }; - Application.Top.Add (shortcut); + Application.Current.Add (shortcut); shortcut.SetFocus (); Assert.Equal (canFocus, shortcut.HasFocus); @@ -337,7 +337,7 @@ public class ShortcutTests Assert.Equal (expectedAccept, accepted); Assert.Equal (expectedSelect, selected); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } [Theory] @@ -349,17 +349,17 @@ public class ShortcutTests [InlineData (KeyCode.F1, 0)] public void KeyDown_App_Scope_Invokes_Accept (KeyCode key, int expectedAccept) { - Application.Top = new (); + Application.Current = new () { App = Application.Create () }; var shortcut = new Shortcut { Key = Key.A, - BindKeyToApplication = true, Text = "0", Title = "_C" }; - Application.Top.Add (shortcut); - Application.Top.SetFocus (); + Application.Current.Add (shortcut); + shortcut.BindKeyToApplication = true; + Application.Current.SetFocus (); var accepted = 0; shortcut.Accepting += (s, e) => accepted++; @@ -368,7 +368,7 @@ public class ShortcutTests Assert.Equal (expectedAccept, accepted); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -427,19 +427,24 @@ public class ShortcutTests [InlineData (false, KeyCode.F1, 0)] public void KeyDown_App_Scope_Invokes_Action (bool canFocus, KeyCode key, int expectedAction) { - Application.Top = new (); + Application.Current = new (); var shortcut = new Shortcut { - Key = Key.A, BindKeyToApplication = true, + Key = Key.A, Text = "0", Title = "_C", CanFocus = canFocus }; - Application.Top.Add (shortcut); - Application.Top.SetFocus (); + Application.Current.Add (shortcut); + + // Shortcut requires Init for App scoped hotkeys to work + Application.Current.BeginInit (); + Application.Current.EndInit (); + + Application.Current.SetFocus (); var action = 0; shortcut.Action += () => action++; @@ -448,7 +453,7 @@ public class ShortcutTests Assert.Equal (expectedAction, action); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (true); } @@ -456,11 +461,11 @@ public class ShortcutTests [Fact] public void Scheme_SetScheme_Does_Not_Fault_3664 () { - Application.Top = new (); - Application.Navigation = new (); + Application.Current = new (); + var shortcut = new Shortcut (); - Application.Top.SetScheme (null); + Application.Current.SetScheme (null); Assert.False (shortcut.HasScheme); Assert.NotNull (shortcut.GetScheme ()); @@ -470,7 +475,7 @@ public class ShortcutTests Assert.False (shortcut.HasScheme); Assert.NotNull (shortcut.GetScheme ()); - Application.Top.Dispose (); + Application.Current.Dispose (); Application.ResetState (); } } diff --git a/Tests/UnitTests/Views/SpinnerViewTests.cs b/Tests/UnitTests/Views/SpinnerViewTests.cs index f68b96b0a..35b25e039 100644 --- a/Tests/UnitTests/Views/SpinnerViewTests.cs +++ b/Tests/UnitTests/Views/SpinnerViewTests.cs @@ -40,7 +40,7 @@ public class SpinnerViewTests (ITestOutputHelper output) // Dispose clears timeout view.Dispose (); Assert.Empty (Application.TimedEvents.Timeouts); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Fact] @@ -57,12 +57,12 @@ public class SpinnerViewTests (ITestOutputHelper output) DriverAssert.AssertDriverContentsWithFrameAre (expected, output); view.AdvanceAnimation (); - View.SetClipToScreen (); + view.SetClipToScreen (); view.Draw (); expected = "/"; DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Fact] @@ -95,7 +95,7 @@ public class SpinnerViewTests (ITestOutputHelper output) //expected = "|"; //DriverAsserts.AssertDriverContentsWithFrameAre (expected, output); - Application.Top.Dispose (); + Application.Current.Dispose (); } private SpinnerView GetSpinnerView () diff --git a/Tests/UnitTests/Views/TabViewTests.cs b/Tests/UnitTests/Views/TabViewTests.cs index 746f0307d..db11ed0eb 100644 --- a/Tests/UnitTests/Views/TabViewTests.cs +++ b/Tests/UnitTests/Views/TabViewTests.cs @@ -664,7 +664,7 @@ public class TabViewTests (ITestOutputHelper output) Assert.Equal (tab2, tv.SubViews.First (v => v.Id.Contains ("tabRow")).MostFocused); tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -683,7 +683,7 @@ public class TabViewTests (ITestOutputHelper output) tab1.DisplayText = "12345678910"; tab2.DisplayText = "13"; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -700,7 +700,7 @@ public class TabViewTests (ITestOutputHelper output) tv.SelectedTab = tab2; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -717,7 +717,7 @@ public class TabViewTests (ITestOutputHelper output) tab2.DisplayText = "abcdefghijklmnopq"; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -810,7 +810,7 @@ public class TabViewTests (ITestOutputHelper output) Assert.Equal (tab2, tv.SubViews.First (v => v.Id.Contains ("tabRow")).MostFocused); tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -830,7 +830,7 @@ public class TabViewTests (ITestOutputHelper output) tab2.DisplayText = "13"; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -847,7 +847,7 @@ public class TabViewTests (ITestOutputHelper output) tv.SelectedTab = tab2; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -865,7 +865,7 @@ public class TabViewTests (ITestOutputHelper output) tab2.DisplayText = "abcdefghijklmnopq"; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -910,7 +910,7 @@ public class TabViewTests (ITestOutputHelper output) tv.Height = 5; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -952,7 +952,7 @@ public class TabViewTests (ITestOutputHelper output) tv.SelectedTab = tab2; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -972,7 +972,7 @@ public class TabViewTests (ITestOutputHelper output) tab2.DisplayText = "13"; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -989,7 +989,7 @@ public class TabViewTests (ITestOutputHelper output) tv.SelectedTab = tab2; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -1007,7 +1007,7 @@ public class TabViewTests (ITestOutputHelper output) tab2.DisplayText = "abcdefghijklmnopq"; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -1049,7 +1049,7 @@ public class TabViewTests (ITestOutputHelper output) tv.SelectedTab = tab2; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -1144,7 +1144,7 @@ public class TabViewTests (ITestOutputHelper output) tab2.DisplayText = "13"; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -1161,7 +1161,7 @@ public class TabViewTests (ITestOutputHelper output) tv.SelectedTab = tab2; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -1179,7 +1179,7 @@ public class TabViewTests (ITestOutputHelper output) tab2.DisplayText = "abcdefghijklmnopq"; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -1223,7 +1223,7 @@ public class TabViewTests (ITestOutputHelper output) tv.SelectedTab = tab2; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -1337,7 +1337,7 @@ public class TabViewTests (ITestOutputHelper output) tv.SelectedTab = tab2; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -1354,7 +1354,7 @@ public class TabViewTests (ITestOutputHelper output) tv.SelectedTab = tab3; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -1404,7 +1404,7 @@ public class TabViewTests (ITestOutputHelper output) tv.SelectedTab = tab2; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -1421,7 +1421,7 @@ public class TabViewTests (ITestOutputHelper output) tv.SelectedTab = tab3; tv.Layout (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -1489,7 +1489,11 @@ public class TabViewTests (ITestOutputHelper output) private TabView GetTabView (out Tab tab1, out Tab tab2) { - var tv = new TabView () { Id = "tv " }; + var tv = new TabView () + { + Driver = ApplicationImpl.Instance.Driver, + Id = "tv " + }; tv.BeginInit (); tv.EndInit (); //tv.Scheme = new (); diff --git a/Tests/UnitTests/Views/TableViewTests.cs b/Tests/UnitTests/Views/TableViewTests.cs index 885bb435a..4c162a037 100644 --- a/Tests/UnitTests/Views/TableViewTests.cs +++ b/Tests/UnitTests/Views/TableViewTests.cs @@ -57,7 +57,11 @@ public class TableViewTests (ITestOutputHelper output) [AutoInitShutdown] public void CellEventsBackgroundFill () { - var tv = new TableView { Width = 20, Height = 4 }; + var tv = new TableView + { + Driver = ApplicationImpl.Instance.Driver, + Width = 20, Height = 4 + }; var dt = new DataTable (); dt.Columns.Add ("C1"); @@ -677,7 +681,10 @@ public class TableViewTests (ITestOutputHelper output) [SetupFakeApplication] public void ScrollIndicators () { - var tableView = new TableView (); + var tableView = new TableView () + { + Driver = ApplicationImpl.Instance.Driver + }; tableView.BeginInit (); tableView.EndInit (); @@ -722,7 +729,7 @@ public class TableViewTests (ITestOutputHelper output) // since A is now pushed off screen we get indicator showing // that user can scroll left to see first column - View.SetClipToScreen (); + tableView.SetClipToScreen (); tableView.Draw (); expected = @@ -737,7 +744,7 @@ public class TableViewTests (ITestOutputHelper output) tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorRight }); tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorRight }); - View.SetClipToScreen (); + tableView.SetClipToScreen (); tableView.Draw (); expected = @@ -756,7 +763,10 @@ public class TableViewTests (ITestOutputHelper output) [SetupFakeApplication] public void ScrollRight_SmoothScrolling () { - var tableView = new TableView (); + var tableView = new TableView () + { + Driver = ApplicationImpl.Instance.Driver + }; tableView.BeginInit (); tableView.EndInit (); @@ -796,7 +806,7 @@ public class TableViewTests (ITestOutputHelper output) // Scroll right tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorRight }); - View.SetClipToScreen (); + tableView.SetClipToScreen (); tableView.Draw (); // Note that with SmoothHorizontalScrolling only a single new column @@ -818,7 +828,11 @@ public class TableViewTests (ITestOutputHelper output) [SetupFakeApplication] public void ScrollRight_WithoutSmoothScrolling () { - var tableView = new TableView (); + var tableView = new TableView () + { + Driver = ApplicationImpl.Instance.Driver + }; + tableView.BeginInit (); tableView.EndInit (); tableView.SchemeName = "TopLevel"; @@ -844,7 +858,7 @@ public class TableViewTests (ITestOutputHelper output) // select last visible column tableView.SelectedColumn = 2; // column C - View.SetClipToScreen (); + tableView.SetClipToScreen (); tableView.Draw (); var expected = @@ -856,7 +870,7 @@ public class TableViewTests (ITestOutputHelper output) // Scroll right tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorRight }); - View.SetClipToScreen (); + tableView.SetClipToScreen (); tableView.Draw (); // notice that without smooth scrolling we just update the first column @@ -1563,7 +1577,10 @@ public class TableViewTests (ITestOutputHelper output) [AutoInitShutdown] public void Test_CollectionNavigator () { - var tv = new TableView (); + var tv = new TableView () + { + Driver = ApplicationImpl.Instance.Driver + }; tv.SchemeName = "TopLevel"; tv.Viewport = new (0, 0, 50, 7); @@ -1974,7 +1991,7 @@ public class TableViewTests (ITestOutputHelper output) ◄─┼─┼─┤ │2│3│4│"; tableView.SetNeedsDraw (); - View.SetClipToScreen (); + tableView.SetClipToScreen (); tableView.Draw (); DriverAssert.AssertDriverContentsAre (expected, output); @@ -1988,7 +2005,7 @@ public class TableViewTests (ITestOutputHelper output) ├─┼─┼─┤ │2│3│4│"; tableView.SetNeedsDraw (); - View.SetClipToScreen (); + tableView.SetClipToScreen (); tableView.Draw (); DriverAssert.AssertDriverContentsAre (expected, output); @@ -2004,7 +2021,7 @@ public class TableViewTests (ITestOutputHelper output) tableView.Style.ShowHorizontalHeaderUnderline = true; tableView.LayoutSubViews (); tableView.SetNeedsDraw (); - View.SetClipToScreen (); + tableView.SetClipToScreen (); tableView.Draw (); // normally we should have scroll indicators because DEF are of screen @@ -2027,7 +2044,7 @@ public class TableViewTests (ITestOutputHelper output) ├─┼─┼─┤ │1│2│3│"; tableView.SetNeedsDraw (); - View.SetClipToScreen (); + tableView.SetClipToScreen (); tableView.Draw (); DriverAssert.AssertDriverContentsAre (expected, output); } @@ -2206,8 +2223,11 @@ public class TableViewTests (ITestOutputHelper output) [SetupFakeApplication] public void TestEnumerableDataSource_BasicTypes () { - Application.Driver!.SetScreenSize (100, 100); - var tv = new TableView (); + ApplicationImpl.Instance.Driver!.SetScreenSize (100, 100); + var tv = new TableView () + { + Driver = ApplicationImpl.Instance.Driver + }; tv.SchemeName = "TopLevel"; tv.Viewport = new (0, 0, 50, 6); @@ -2411,7 +2431,10 @@ A B C { IList list = BuildList (16); - var tv = new TableView (); + var tv = new TableView () + { + Driver = ApplicationImpl.Instance.Driver + }; //tv.BeginInit (); tv.EndInit (); tv.SchemeName = "TopLevel"; @@ -2600,7 +2623,7 @@ A B C Assert.True (pets.First ().IsPicked); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -2620,7 +2643,7 @@ A B C Assert.True (pets.ElementAt (0).IsPicked); Assert.True (pets.ElementAt (1).IsPicked); Assert.False (pets.ElementAt (2).IsPicked); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -2640,7 +2663,7 @@ A B C Assert.False (pets.ElementAt (0).IsPicked); Assert.True (pets.ElementAt (1).IsPicked); Assert.False (pets.ElementAt (2).IsPicked); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -2668,7 +2691,7 @@ A B C wrapper.CheckedRows.Add (0); wrapper.CheckedRows.Add (2); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); var expected = @@ -2692,7 +2715,7 @@ A B C Assert.Contains (2, wrapper.CheckedRows); Assert.Equal (3, wrapper.CheckedRows.Count); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -2708,7 +2731,7 @@ A B C // Untoggle the top 2 tv.NewKeyDownEvent (Key.Space); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -2737,7 +2760,7 @@ A B C tv.NewKeyDownEvent (Key.A.WithCtrl); tv.NewKeyDownEvent (Key.Space); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); var expected = @@ -2757,7 +2780,7 @@ A B C // Untoggle all again tv.NewKeyDownEvent (Key.Space); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -2798,7 +2821,7 @@ A B C Assert.True (pets.All (p => p.IsPicked)); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); var expected = @@ -2818,7 +2841,7 @@ A B C Assert.Empty (pets.Where (p => p.IsPicked)); #pragma warning restore xUnit2029 - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -2845,7 +2868,7 @@ A B C var wrapper = new CheckBoxTableSourceWrapperByIndex (tv, tv.Table); tv.Table = wrapper; - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); var expected = @@ -2865,7 +2888,7 @@ A B C Assert.Single (wrapper.CheckedRows, 0); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -2885,7 +2908,7 @@ A B C Assert.Contains (1, wrapper.CheckedRows); Assert.Equal (2, wrapper.CheckedRows.Count); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -2904,7 +2927,7 @@ A B C Assert.Single (wrapper.CheckedRows, 1); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -2936,7 +2959,7 @@ A B C wrapper.UseRadioButtons = true; tv.Table = wrapper; - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); var expected = @@ -2959,7 +2982,7 @@ A B C Assert.True (pets.First ().IsPicked); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -2980,7 +3003,7 @@ A B C Assert.True (pets.ElementAt (1).IsPicked); Assert.False (pets.ElementAt (2).IsPicked); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -3001,7 +3024,7 @@ A B C Assert.False (pets.ElementAt (1).IsPicked); Assert.False (pets.ElementAt (2).IsPicked); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -3234,19 +3257,19 @@ A B C // Pressing left should move us to the first column without changing focus Application.RaiseKeyDownEvent (Key.CursorLeft); - Assert.Same (tableView, Application.Top!.MostFocused); + Assert.Same (tableView, Application.Current!.MostFocused); Assert.True (tableView.HasFocus); // Because we are now on the leftmost cell a further left press should move focus Application.RaiseKeyDownEvent (Key.CursorLeft); - Assert.NotSame (tableView, Application.Top.MostFocused); + Assert.NotSame (tableView, Application.Current.MostFocused); Assert.False (tableView.HasFocus); - Assert.Same (tf1, Application.Top.MostFocused); + Assert.Same (tf1, Application.Current.MostFocused); Assert.True (tf1.HasFocus); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Fact] @@ -3259,19 +3282,19 @@ A B C // First press should move us up Application.RaiseKeyDownEvent (Key.CursorUp); - Assert.Same (tableView, Application.Top!.MostFocused); + Assert.Same (tableView, Application.Current!.MostFocused); Assert.True (tableView.HasFocus); // Because we are now on the top row a further press should move focus Application.RaiseKeyDownEvent (Key.CursorUp); - Assert.NotSame (tableView, Application.Top.MostFocused); + Assert.NotSame (tableView, Application.Current.MostFocused); Assert.False (tableView.HasFocus); - Assert.Same (tf1, Application.Top.MostFocused); + Assert.Same (tf1, Application.Current.MostFocused); Assert.True (tf1.HasFocus); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Fact] @@ -3284,19 +3307,19 @@ A B C // First press should move us to the rightmost column without changing focus Application.RaiseKeyDownEvent (Key.CursorRight); - Assert.Same (tableView, Application.Top!.MostFocused); + Assert.Same (tableView, Application.Current!.MostFocused); Assert.True (tableView.HasFocus); // Because we are now on the rightmost cell, a further right press should move focus Application.RaiseKeyDownEvent (Key.CursorRight); - Assert.NotSame (tableView, Application.Top.MostFocused); + Assert.NotSame (tableView, Application.Current.MostFocused); Assert.False (tableView.HasFocus); - Assert.Same (tf2, Application.Top.MostFocused); + Assert.Same (tf2, Application.Current.MostFocused); Assert.True (tf2.HasFocus); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Fact] @@ -3309,19 +3332,19 @@ A B C // First press should move us to the bottommost row without changing focus Application.RaiseKeyDownEvent (Key.CursorDown); - Assert.Same (tableView, Application.Top!.MostFocused); + Assert.Same (tableView, Application.Current!.MostFocused); Assert.True (tableView.HasFocus); // Because we are now on the bottommost cell, a further down press should move focus Application.RaiseKeyDownEvent (Key.CursorDown); - Assert.NotSame (tableView, Application.Top.MostFocused); + Assert.NotSame (tableView, Application.Current.MostFocused); Assert.False (tableView.HasFocus); - Assert.Same (tf2, Application.Top.MostFocused); + Assert.Same (tf2, Application.Current.MostFocused); Assert.True (tf2.HasFocus); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Fact] @@ -3334,7 +3357,7 @@ A B C // Pressing shift-left should give us a multi selection Application.RaiseKeyDownEvent (Key.CursorLeft.WithShift); - Assert.Same (tableView, Application.Top!.MostFocused); + Assert.Same (tableView, Application.Current!.MostFocused); Assert.True (tableView.HasFocus); Assert.Equal (2, tableView.GetAllSelectedCells ().Count ()); @@ -3345,19 +3368,19 @@ A B C // Selection 'clears' just to the single cell and we remain focused Assert.Single (tableView.GetAllSelectedCells ()); - Assert.Same (tableView, Application.Top.MostFocused); + Assert.Same (tableView, Application.Current.MostFocused); Assert.True (tableView.HasFocus); // A further left will switch focus Application.RaiseKeyDownEvent (Key.CursorLeft); - Assert.NotSame (tableView, Application.Top.MostFocused); + Assert.NotSame (tableView, Application.Current.MostFocused); Assert.False (tableView.HasFocus); - Assert.Same (tf1, Application.Top.MostFocused); + Assert.Same (tf1, Application.Current.MostFocused); Assert.True (tf1.HasFocus); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Theory] @@ -3397,17 +3420,17 @@ A B C tableView.BeginInit (); tableView.EndInit (); - Application.Navigation = new (); - Application.Top = new (); + + Application.Current = new (); tf1 = new (); tf2 = new (); - Application.Top.Add (tf1); - Application.Top.Add (tableView); - Application.Top.Add (tf2); + Application.Current.Add (tf1); + Application.Current.Add (tableView); + Application.Current.Add (tf2); tableView.SetFocus (); - Assert.Same (tableView, Application.Top.MostFocused); + Assert.Same (tableView, Application.Current.MostFocused); Assert.True (tableView.HasFocus); // Set big table @@ -3416,7 +3439,10 @@ A B C private TableView GetABCDEFTableView (out DataTable dt) { - var tableView = new TableView (); + var tableView = new TableView () + { + Driver = ApplicationImpl.Instance.Driver + }; tableView.BeginInit (); tableView.EndInit (); @@ -3445,9 +3471,12 @@ A B C private TableView GetPetTable (out EnumerableTableSource source) { - var tv = new TableView (); - tv.SchemeName = "TopLevel"; - tv.Viewport = new (0, 0, 25, 6); + var tv = new TableView () + { + Driver = ApplicationImpl.Instance.Driver, + SchemeName = "TopLevel", + Viewport = new (0, 0, 25, 6) + }; List pets = new () { @@ -3473,7 +3502,10 @@ A B C private TableView GetTwoRowSixColumnTable (out DataTable dt) { - var tableView = new TableView (); + var tableView = new TableView () + { + Driver = ApplicationImpl.Instance.Driver + }; tableView.SchemeName = "TopLevel"; // 3 columns are visible @@ -3503,7 +3535,10 @@ A B C private TableView SetUpMiniTable (out DataTable dt) { - var tv = new TableView (); + var tv = new TableView () + { + Driver = ApplicationImpl.Instance.Driver + }; tv.BeginInit (); tv.EndInit (); tv.Viewport = new (0, 0, 10, 4); diff --git a/Tests/UnitTests/Views/TextFieldTests.cs b/Tests/UnitTests/Views/TextFieldTests.cs index b4d27b93b..922178409 100644 --- a/Tests/UnitTests/Views/TextFieldTests.cs +++ b/Tests/UnitTests/Views/TextFieldTests.cs @@ -84,7 +84,7 @@ public class TextFieldTests (ITestOutputHelper output) tf.Draw (); DriverAssert.AssertDriverContentsAre (expectedRender, output); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Fact] @@ -104,7 +104,7 @@ public class TextFieldTests (ITestOutputHelper output) tf.Draw (); DriverAssert.AssertDriverContentsAre ("Misérables", output); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Theory (Skip = "Broke with ContextMenuv2")] @@ -123,16 +123,16 @@ public class TextFieldTests (ITestOutputHelper output) // Caption should appear when not focused and no text Assert.False (tf.HasFocus); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); DriverAssert.AssertDriverContentsAre ("Enter txt", output); // but disapear when text is added tf.Text = content; - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); DriverAssert.AssertDriverContentsAre (content, output); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Fact] @@ -147,17 +147,17 @@ public class TextFieldTests (ITestOutputHelper output) // Caption has no effect when focused tf.Title = "Enter txt"; Assert.True (tf.HasFocus); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); DriverAssert.AssertDriverContentsAre ("", output); Application.RaiseKeyDownEvent ('\t'); Assert.False (tf.HasFocus); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); DriverAssert.AssertDriverContentsAre ("Enter txt", output); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Fact] @@ -173,7 +173,7 @@ public class TextFieldTests (ITestOutputHelper output) Application.RaiseKeyDownEvent ('\t'); Assert.False (tf.HasFocus); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); // Verify the caption text is rendered @@ -187,7 +187,7 @@ public class TextFieldTests (ITestOutputHelper output) // All characters in "Enter text" should have the caption attribute DriverAssert.AssertDriverAttributesAre ("0000000000", output, Application.Driver, captionAttr); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Fact] @@ -203,7 +203,7 @@ public class TextFieldTests (ITestOutputHelper output) Application.RaiseKeyDownEvent ('\t'); Assert.False (tf.HasFocus); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); // The hotkey character 'F' should be rendered (without the underscore in the actual text) @@ -221,7 +221,7 @@ public class TextFieldTests (ITestOutputHelper output) // F is underlined (index 1), remaining characters use normal caption attribute (index 0) DriverAssert.AssertDriverAttributesAre ("1000", output, Application.Driver, captionAttr, hotkeyAttr); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Fact] @@ -237,7 +237,7 @@ public class TextFieldTests (ITestOutputHelper output) Application.RaiseKeyDownEvent ('\t'); Assert.False (tf.HasFocus); - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); // The underscore should not be rendered, 'T' should be underlined @@ -255,7 +255,7 @@ public class TextFieldTests (ITestOutputHelper output) // "Enter " (6 chars) + "T" (underlined) + "ext" (3 chars) DriverAssert.AssertDriverAttributesAre ("0000001000", output, Application.Driver, captionAttr, hotkeyAttr); - Application.Top.Dispose (); + Application.Current.Dispose (); } [Fact] @@ -1620,7 +1620,11 @@ public class TextFieldTests (ITestOutputHelper output) [SetupFakeApplication] public void Words_With_Accents_Incorrect_Order_Will_Result_With_Wrong_Accent_Place () { - var tf = new TextField { Width = 30, Text = "Les Misérables" }; + var tf = new TextField + { + Driver = ApplicationImpl.Instance.Driver, + Width = 30, Text = "Les Misérables" + }; tf.SetRelativeLayout (new (100, 100)); tf.Draw (); @@ -1641,7 +1645,7 @@ Les Misérables", // incorrect order will result with a wrong accent place tf.Text = "Les Mis" + char.ConvertFromUtf32 (int.Parse ("0301", NumberStyles.HexNumber)) + "erables"; - View.SetClipToScreen (); + tf.SetClipToScreen (); tf.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -1686,7 +1690,7 @@ Les Miśerables", { base.Before (methodUnderTest); - //Application.Top.Scheme = Colors.Schemes ["Base"]; + //Application.Current.Scheme = Colors.Schemes ["Base"]; _textField = new () { // 1 2 3 @@ -1701,7 +1705,11 @@ Les Miśerables", [AutoInitShutdown] public void Draw_Esc_Rune () { - var tf = new TextField { Width = 5, Text = "\u001b" }; + var tf = new TextField + { + Driver = ApplicationImpl.Instance.Driver, + Width = 5, Text = "\u001b" + }; tf.BeginInit (); tf.EndInit (); tf.Draw (); diff --git a/Tests/UnitTests/Views/TextViewTests.cs b/Tests/UnitTests/Views/TextViewTests.cs index d00000d2e..5d03983eb 100644 --- a/Tests/UnitTests/Views/TextViewTests.cs +++ b/Tests/UnitTests/Views/TextViewTests.cs @@ -109,7 +109,7 @@ public class TextViewTests Assert.Equal (leftCol, _textView.LeftColumn); } - Application.Top.Remove (_textView); + Application.Current.Remove (_textView); Application.RequestStop (); } } @@ -6805,7 +6805,10 @@ Line 2.", ); tv.WordWrap = true; - var top = new Toplevel (); + var top = new Toplevel () + { + Driver = ApplicationImpl.Instance.Driver, + }; top.Add (tv); top.Layout (); tv.Draw (); @@ -6827,7 +6830,7 @@ line. tv.CursorPosition = new (6, 2); Assert.Equal (new (5, 2), tv.CursorPosition); top.LayoutSubViews (); - View.SetClipToScreen (); + top.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -6891,7 +6894,10 @@ line. ); tv.WordWrap = true; - var top = new Toplevel (); + var top = new Toplevel () + { + Driver = ApplicationImpl.Instance.Driver, + }; top.Add (tv); top.Layout (); @@ -7005,7 +7011,11 @@ line. [SetupFakeApplication] public void Draw_Esc_Rune () { - var tv = new TextView { Width = 5, Height = 1, Text = "\u001b" }; + var tv = new TextView + { + Driver = ApplicationImpl.Instance.Driver, + Width = 5, Height = 1, Text = "\u001b" + }; tv.BeginInit (); tv.EndInit (); tv.Draw (); diff --git a/Tests/UnitTests/Views/ToplevelTests.cs b/Tests/UnitTests/Views/ToplevelTests.cs index ad47948a0..365ebd8bf 100644 --- a/Tests/UnitTests/Views/ToplevelTests.cs +++ b/Tests/UnitTests/Views/ToplevelTests.cs @@ -69,11 +69,11 @@ public class ToplevelTests #endif Application.Begin (top); - Assert.Equal (top, Application.Top); + Assert.Equal (top, Application.Current); - // Application.Top without menu and status bar. + // Application.Current without menu and status bar. View supView = View.GetLocationEnsuringFullVisibility (top, 2, 2, out int nx, out int ny /*, out StatusBar sb*/); - Assert.Equal (Application.Top, supView); + Assert.Equal (Application.Current, supView); Assert.Equal (0, nx); Assert.Equal (0, ny); @@ -82,7 +82,7 @@ public class ToplevelTests top.Add (new MenuBar ()); Assert.NotNull (top.MenuBar); - // Application.Top with a menu and without status bar. + // Application.Current with a menu and without status bar. View.GetLocationEnsuringFullVisibility (top, 2, 2, out nx, out ny /*, out sb*/); Assert.Equal (0, nx); Assert.Equal (1, ny); @@ -92,11 +92,11 @@ public class ToplevelTests //top.Add (new StatusBar ()); //Assert.NotNull (top.StatusBar); - // Application.Top with a menu and status bar. + // Application.Current with a menu and status bar. View.GetLocationEnsuringFullVisibility (top, 2, 2, out nx, out ny /*, out sb*/); Assert.Equal (0, nx); - // The available height is lower than the Application.Top height minus + // The available height is lower than the Application.Current height minus // the menu bar and status bar, then the top can go beyond the bottom // Assert.Equal (2, ny); //Assert.NotNull (sb); @@ -106,11 +106,11 @@ public class ToplevelTests Assert.Null (top.MenuBar); Assert.NotNull (menuBar); - // Application.Top without a menu and with a status bar. + // Application.Current without a menu and with a status bar. View.GetLocationEnsuringFullVisibility (top, 2, 2, out nx, out ny /*, out sb*/); Assert.Equal (0, nx); - // The available height is lower than the Application.Top height minus + // The available height is lower than the Application.Current height minus // the status bar, then the top can go beyond the bottom // Assert.Equal (2, ny); //Assert.NotNull (sb); @@ -127,11 +127,11 @@ public class ToplevelTests // The SuperView is always the same regardless of the caller. supView = View.GetLocationEnsuringFullVisibility (win, 0, 0, out nx, out ny /*, out sb*/); - Assert.Equal (Application.Top, supView); + Assert.Equal (Application.Current, supView); supView = View.GetLocationEnsuringFullVisibility (win, 0, 0, out nx, out ny /*, out sb*/); - Assert.Equal (Application.Top, supView); + Assert.Equal (Application.Current, supView); - // Application.Top without menu and status bar. + // Application.Current without menu and status bar. View.GetLocationEnsuringFullVisibility (win, 0, 0, out nx, out ny /*, out sb*/); Assert.Equal (0, nx); Assert.Equal (0, ny); @@ -141,7 +141,7 @@ public class ToplevelTests top.Add (new MenuBar ()); Assert.NotNull (top.MenuBar); - // Application.Top with a menu and without status bar. + // Application.Current with a menu and without status bar. View.GetLocationEnsuringFullVisibility (win, 2, 2, out nx, out ny /*, out sb*/); Assert.Equal (0, nx); Assert.Equal (1, ny); @@ -152,11 +152,11 @@ public class ToplevelTests //Assert.NotNull (top.StatusBar); - // Application.Top with a menu and status bar. + // Application.Current with a menu and status bar. View.GetLocationEnsuringFullVisibility (win, 30, 20, out nx, out ny /*, out sb*/); Assert.Equal (0, nx); - // The available height is lower than the Application.Top height minus + // The available height is lower than the Application.Current height minus // the menu bar and status bar, then the top can go beyond the bottom //Assert.Equal (20, ny); //Assert.NotNull (sb); @@ -177,7 +177,7 @@ public class ToplevelTests win = new () { Width = 60, Height = 15 }; top.Add (win); - // Application.Top without menu and status bar. + // Application.Current without menu and status bar. View.GetLocationEnsuringFullVisibility (win, 0, 0, out nx, out ny /*, out sb*/); Assert.Equal (0, nx); Assert.Equal (0, ny); @@ -187,7 +187,7 @@ public class ToplevelTests top.Add (new MenuBar ()); Assert.NotNull (top.MenuBar); - // Application.Top with a menu and without status bar. + // Application.Current with a menu and without status bar. View.GetLocationEnsuringFullVisibility (win, 2, 2, out nx, out ny /*, out sb*/); Assert.Equal (2, nx); Assert.Equal (2, ny); @@ -198,7 +198,7 @@ public class ToplevelTests //Assert.NotNull (top.StatusBar); - // Application.Top with a menu and status bar. + // Application.Current with a menu and status bar. View.GetLocationEnsuringFullVisibility (win, 30, 20, out nx, out ny /*, out sb*/); Assert.Equal (20, nx); // 20+60=80 @@ -249,7 +249,7 @@ public class ToplevelTests var win = new Window (); win.Add (view); - Application.Init (null, "fake"); + Application.Init ("fake"); Toplevel top = new (); top.Add (win); @@ -307,7 +307,7 @@ public class ToplevelTests } else if (iterations == 1) { - Assert.Equal (new (2, 2), Application.Top!.Frame.Location); + Assert.Equal (new (2, 2), Application.Current!.Frame.Location); } else if (iterations == 2) { @@ -316,12 +316,12 @@ public class ToplevelTests // Grab the mouse Application.RaiseMouseEvent (new () { ScreenPosition = new (3, 2), Flags = MouseFlags.Button1Pressed }); - Assert.Equal (Application.Top!.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (2, 2, 10, 3), Application.Top.Frame); + Assert.Equal (Application.Current!.Border, Application.Mouse.MouseGrabView); + Assert.Equal (new (2, 2, 10, 3), Application.Current.Frame); } else if (iterations == 3) { - Assert.Equal (Application.Top!.Border, Application.Mouse.MouseGrabView); + Assert.Equal (Application.Current!.Border, Application.Mouse.MouseGrabView); // Drag to left Application.RaiseMouseEvent ( @@ -333,38 +333,38 @@ public class ToplevelTests }); AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (Application.Top.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (1, 2, 10, 3), Application.Top.Frame); + Assert.Equal (Application.Current.Border, Application.Mouse.MouseGrabView); + Assert.Equal (new (1, 2, 10, 3), Application.Current.Frame); } else if (iterations == 4) { - Assert.Equal (Application.Top!.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (1, 2), Application.Top.Frame.Location); + Assert.Equal (Application.Current!.Border, Application.Mouse.MouseGrabView); + Assert.Equal (new (1, 2), Application.Current.Frame.Location); - Assert.Equal (Application.Top.Border, Application.Mouse.MouseGrabView); + Assert.Equal (Application.Current.Border, Application.Mouse.MouseGrabView); } else if (iterations == 5) { - Assert.Equal (Application.Top!.Border, Application.Mouse.MouseGrabView); + Assert.Equal (Application.Current!.Border, Application.Mouse.MouseGrabView); // Drag up Application.RaiseMouseEvent (new () { ScreenPosition = new (2, 1), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition }); AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (Application.Top!.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (1, 1, 10, 3), Application.Top.Frame); + Assert.Equal (Application.Current!.Border, Application.Mouse.MouseGrabView); + Assert.Equal (new (1, 1, 10, 3), Application.Current.Frame); } else if (iterations == 6) { - Assert.Equal (Application.Top!.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (1, 1), Application.Top.Frame.Location); + Assert.Equal (Application.Current!.Border, Application.Mouse.MouseGrabView); + Assert.Equal (new (1, 1), Application.Current.Frame.Location); - Assert.Equal (Application.Top.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (1, 1, 10, 3), Application.Top.Frame); + Assert.Equal (Application.Current.Border, Application.Mouse.MouseGrabView); + Assert.Equal (new (1, 1, 10, 3), Application.Current.Frame); } else if (iterations == 7) { - Assert.Equal (Application.Top!.Border, Application.Mouse.MouseGrabView); + Assert.Equal (Application.Current!.Border, Application.Mouse.MouseGrabView); // Ungrab the mouse Application.RaiseMouseEvent (new () { ScreenPosition = new (2, 1), Flags = MouseFlags.Button1Released }); @@ -739,7 +739,7 @@ public class ToplevelTests [Fact] public void Multi_Thread_Toplevels () { - Application.Init (null, "fake"); + Application.Init ("fake"); Toplevel t = new (); var w = new Window (); diff --git a/Tests/UnitTests/Views/TreeTableSourceTests.cs b/Tests/UnitTests/Views/TreeTableSourceTests.cs index 43d5a2059..0f5fd42c2 100644 --- a/Tests/UnitTests/Views/TreeTableSourceTests.cs +++ b/Tests/UnitTests/Views/TreeTableSourceTests.cs @@ -55,7 +55,7 @@ public class TreeTableSourceTests : IDisposable // when pressing right we should expand the top route tv.NewKeyDownEvent (Key.CursorRight); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -73,7 +73,7 @@ public class TreeTableSourceTests : IDisposable // when pressing left we should collapse the top route again tv.NewKeyDownEvent (Key.CursorLeft); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -97,7 +97,7 @@ public class TreeTableSourceTests : IDisposable tv.Style.GetOrCreateColumnStyle (1).MinAcceptableWidth = 1; - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); var expected = @@ -117,7 +117,7 @@ public class TreeTableSourceTests : IDisposable Assert.True (tv.NewMouseEvent (new MouseEventArgs { Position = new (2, 2), Flags = MouseFlags.Button1Clicked })); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -142,7 +142,7 @@ public class TreeTableSourceTests : IDisposable // Clicking on the + again should collapse tv.NewMouseEvent (new MouseEventArgs { Position = new (2, 2), Flags = MouseFlags.Button1Clicked }); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -195,7 +195,7 @@ public class TreeTableSourceTests : IDisposable Application.RaiseKeyDownEvent (Key.CursorRight); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -213,7 +213,7 @@ public class TreeTableSourceTests : IDisposable tv.NewKeyDownEvent (Key.CursorDown); tv.NewKeyDownEvent (Key.Space); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); expected = @@ -239,7 +239,10 @@ public class TreeTableSourceTests : IDisposable private TableView GetTreeTable (out TreeView tree) { - var tableView = new TableView (); + var tableView = new TableView () + { + Driver = ApplicationImpl.Instance.Driver, + }; tableView.SchemeName = "TopLevel"; tableView.Viewport = new Rectangle (0, 0, 40, 6); diff --git a/Tests/UnitTests/Views/TreeViewTests.cs b/Tests/UnitTests/Views/TreeViewTests.cs index 88b9cde4b..f57330054 100644 --- a/Tests/UnitTests/Views/TreeViewTests.cs +++ b/Tests/UnitTests/Views/TreeViewTests.cs @@ -93,7 +93,7 @@ public class TreeViewTests (ITestOutputHelper output) [AutoInitShutdown] public void CursorVisibility_MultiSelect () { - var tv = new TreeView { Width = 20, Height = 10 }; + var tv = new TreeView () { Driver = ApplicationImpl.Instance.Driver, Width = 20, Height = 10 }; var n1 = new TreeNode ("normal"); var n2 = new TreeNode ("pink"); @@ -698,7 +698,11 @@ public class TreeViewTests (ITestOutputHelper output) [SetupFakeApplication] public void TestBottomlessTreeView_MaxDepth_3 () { - TreeView tv = new () { Width = 20, Height = 10 }; + TreeView tv = new () + { + Driver = ApplicationImpl.Instance.Driver, + Width = 20, Height = 10 + }; tv.TreeBuilder = new DelegateTreeBuilder ( s => new [] { (int.Parse (s) + 1).ToString () } @@ -718,7 +722,7 @@ public class TreeViewTests (ITestOutputHelper output) ); tv.MaxDepth = 3; tv.ExpandAll (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); // Normal drawing of the tree view @@ -737,7 +741,7 @@ public class TreeViewTests (ITestOutputHelper output) [SetupFakeApplication] public void TestBottomlessTreeView_MaxDepth_5 () { - TreeView tv = new () { Width = 20, Height = 10 }; + TreeView tv = new () { Driver = ApplicationImpl.Instance.Driver, Width = 20, Height = 10 }; tv.TreeBuilder = new DelegateTreeBuilder ( s => new [] { (int.Parse (s) + 1).ToString () } @@ -757,7 +761,7 @@ public class TreeViewTests (ITestOutputHelper output) ); tv.MaxDepth = 5; tv.ExpandAll (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); @@ -785,7 +789,7 @@ public class TreeViewTests (ITestOutputHelper output) Assert.True (tv.CanExpand ("5")); Assert.False (tv.IsExpanded ("5")); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); @@ -806,7 +810,7 @@ public class TreeViewTests (ITestOutputHelper output) [SetupFakeApplication] public void TestGetObjectOnRow () { - var tv = new TreeView { Width = 20, Height = 10 }; + var tv = new TreeView () { Driver = ApplicationImpl.Instance.Driver, Width = 20, Height = 10 }; tv.BeginInit (); tv.EndInit (); var n1 = new TreeNode ("normal"); @@ -840,7 +844,7 @@ public class TreeViewTests (ITestOutputHelper output) Assert.Null (tv.GetObjectOnRow (4)); tv.Collapse (n1); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); @@ -862,7 +866,7 @@ public class TreeViewTests (ITestOutputHelper output) [SetupFakeApplication] public void TestGetObjectRow () { - var tv = new TreeView { Width = 20, Height = 10 }; + var tv = new TreeView () { Driver = ApplicationImpl.Instance.Driver, Width = 20, Height = 10 }; var n1 = new TreeNode ("normal"); var n1_1 = new TreeNode ("pink"); @@ -877,7 +881,7 @@ public class TreeViewTests (ITestOutputHelper output) tv.SetScheme (new Scheme ()); tv.LayoutSubViews (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsAre ( @@ -897,7 +901,7 @@ public class TreeViewTests (ITestOutputHelper output) tv.Collapse (n1); tv.LayoutSubViews (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsAre ( @@ -915,7 +919,7 @@ public class TreeViewTests (ITestOutputHelper output) tv.ScrollOffsetVertical = 1; tv.LayoutSubViews (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsAre ( @@ -933,7 +937,7 @@ public class TreeViewTests (ITestOutputHelper output) [SetupFakeApplication] public void TestTreeView_DrawLineEvent () { - var tv = new TreeView { Width = 20, Height = 10 }; + var tv = new TreeView () { Driver = ApplicationImpl.Instance.Driver, Width = 20, Height = 10 }; List> eventArgs = new (); @@ -952,7 +956,7 @@ public class TreeViewTests (ITestOutputHelper output) tv.SetScheme (new Scheme ()); tv.LayoutSubViews (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); // Normal drawing of the tree view @@ -1000,7 +1004,7 @@ public class TreeViewTests (ITestOutputHelper output) [SetupFakeApplication] public void TestTreeView_DrawLineEvent_Handled () { - var tv = new TreeView { Width = 20, Height = 10 }; + var tv = new TreeView () { Driver = ApplicationImpl.Instance.Driver, Width = 20, Height = 10 }; tv.DrawLine += (s, e) => { @@ -1046,7 +1050,7 @@ FFFFFFFFFF [SetupFakeApplication] public void TestTreeView_DrawLineEvent_WithScrolling () { - var tv = new TreeView { Width = 20, Height = 10 }; + var tv = new TreeView () { Driver = ApplicationImpl.Instance.Driver, Width = 20, Height = 10 }; List> eventArgs = new (); @@ -1109,7 +1113,7 @@ oot two [SetupFakeApplication] public void TestTreeView_Filter () { - var tv = new TreeView { Width = 20, Height = 10 }; + var tv = new TreeView () { Driver = ApplicationImpl.Instance.Driver, Width = 20, Height = 10 }; var n1 = new TreeNode ("root one"); var n1_1 = new TreeNode ("leaf 1"); @@ -1141,7 +1145,7 @@ oot two // matches nothing filter.Text = "asdfjhasdf"; - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); // Normal drawing of the tree view @@ -1152,7 +1156,7 @@ oot two // Matches everything filter.Text = "root"; - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsAre ( @@ -1167,7 +1171,7 @@ oot two // Matches 2 leaf nodes filter.Text = "leaf"; - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsAre ( @@ -1181,7 +1185,7 @@ oot two // Matches 1 leaf nodes filter.Text = "leaf 1"; - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); DriverAssert.AssertDriverContentsAre ( @@ -1197,7 +1201,7 @@ oot two [SetupFakeApplication] public void TestTreeViewColor () { - var tv = new TreeView { Width = 20, Height = 10 }; + var tv = new TreeView () { Driver = ApplicationImpl.Instance.Driver, Width = 20, Height = 10 }; tv.BeginInit (); tv.EndInit (); var n1 = new TreeNode ("normal"); @@ -1253,7 +1257,7 @@ oot two // redraw now that the custom color // delegate is registered tv.SetNeedsDraw (); - View.SetClipToScreen (); + tv.SetClipToScreen (); tv.Draw (); // Same text diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs index 5bb470c7e..914d676ee 100644 --- a/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs +++ b/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs @@ -1,5 +1,6 @@ #nullable enable using Moq; +using Terminal.Gui.App; namespace UnitTests_Parallelizable.ApplicationTests; @@ -42,6 +43,7 @@ public class ApplicationPopoverTests // Arrange var popover = new Mock ().Object; var popoverManager = new ApplicationPopover (); + popoverManager.Register (popover); // Act popoverManager.Show (popover); @@ -56,6 +58,7 @@ public class ApplicationPopoverTests // Arrange var popover = new Mock ().Object; var popoverManager = new ApplicationPopover (); + popoverManager.Register (popover); popoverManager.Show (popover); // Act @@ -72,6 +75,8 @@ public class ApplicationPopoverTests // Arrange var popover = new PopoverTestClass (); var popoverManager = new ApplicationPopover (); + popoverManager.Register (popover); + popoverManager.Show (popover); // Act @@ -88,6 +93,7 @@ public class ApplicationPopoverTests // Arrange var popover = new PopoverTestClass (); var popoverManager = new ApplicationPopover (); + popoverManager.Register (popover); popoverManager.Show (popover); // Act @@ -106,6 +112,8 @@ public class ApplicationPopoverTests var activePopover = new PopoverTestClass () { Id = "activePopover" }; var inactivePopover = new PopoverTestClass () { Id = "inactivePopover" }; ; var popoverManager = new ApplicationPopover (); + + popoverManager.Register (activePopover); popoverManager.Show (activePopover); popoverManager.Register (inactivePopover); @@ -126,6 +134,8 @@ public class ApplicationPopoverTests var activePopover = new PopoverTestClass (); var inactivePopover = new PopoverTestClass (); var popoverManager = new ApplicationPopover (); + popoverManager.Register (activePopover); + popoverManager.Show (activePopover); popoverManager.Register (inactivePopover); @@ -181,6 +191,6 @@ public class ApplicationPopoverTests } /// - public Toplevel? Toplevel { get; set; } + public Toplevel? Current { get; set; } } } diff --git a/Tests/UnitTestsParallelizable/Application/MouseInterfaceTests.cs b/Tests/UnitTestsParallelizable/Application/MouseInterfaceTests.cs index 69b64f51a..22421f90b 100644 --- a/Tests/UnitTestsParallelizable/Application/MouseInterfaceTests.cs +++ b/Tests/UnitTestsParallelizable/Application/MouseInterfaceTests.cs @@ -41,7 +41,7 @@ public class MouseInterfaceTests (ITestOutputHelper output) // Assert Assert.Equal (testPosition, mouse.LastMousePosition); - Assert.Equal (testPosition, mouse.GetLastMousePosition ()); + Assert.Equal (testPosition, mouse.LastMousePosition); } [Fact] diff --git a/Tests/UnitTestsParallelizable/Application/MouseTests.cs b/Tests/UnitTestsParallelizable/Application/MouseTests.cs index fdd3260a4..d5dba4dd4 100644 --- a/Tests/UnitTestsParallelizable/Application/MouseTests.cs +++ b/Tests/UnitTestsParallelizable/Application/MouseTests.cs @@ -32,7 +32,7 @@ public class MouseTests (ITestOutputHelper output) // Act mouse.LastMousePosition = expectedPosition; - Point? actualPosition = mouse.GetLastMousePosition (); + Point? actualPosition = mouse.LastMousePosition; // Assert Assert.Equal (expectedPosition, actualPosition); diff --git a/Tests/UnitTestsParallelizable/Application/PopoverBaseImplTests.cs b/Tests/UnitTestsParallelizable/Application/PopoverBaseImplTests.cs index 69b795479..7d2d27d50 100644 --- a/Tests/UnitTestsParallelizable/Application/PopoverBaseImplTests.cs +++ b/Tests/UnitTestsParallelizable/Application/PopoverBaseImplTests.cs @@ -27,8 +27,8 @@ public class PopoverBaseImplTests { var popover = new TestPopover (); var top = new Toplevel (); - popover.Toplevel = top; - Assert.Same (top, popover.Toplevel); + popover.Current = top; + Assert.Same (top, popover.Current); } [Fact] @@ -59,11 +59,11 @@ public class PopoverBaseImplTests } [Fact] - public void Show_DoesNotThrow_BasePopoverImpl () + public void Show_Throw_If_Not_Registered () { var popover = new TestPopover (); var popoverManager = new ApplicationPopover (); - popoverManager.Show (popover); + Assert.Throws (() => popoverManager.Show (popover)); } } diff --git a/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs b/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs index 253acc8c0..aa3017d51 100644 --- a/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs @@ -218,6 +218,33 @@ public class SourcesManagerTests Assert.Contains (source, sourcesManager.Sources.Values); } + + //[Fact] + //public void Update_WithValidJson_UpdatesThemeScope () + //{ + // // Arrange + // var sourcesManager = new SourcesManager (); + // var themeScope = new ThemeScope (); + // themeScope.LoadHardCodedDefaults (); + // themeScope ["Button.DefaultShadowStyle"].PropertyValue = ShadowStyle.Opaque; + + // var json = """ + // { + // "Button.DefaultShadowStyle": "None" + // } + // """; + // var source = "test.json"; + // var location = ConfigLocations.HardCoded; + + // // Act + // bool result = sourcesManager.Load (themeScope, json, source, location); + + // // Assert + // Assert.True (result); + // Assert.Equal (Key.Z.WithCtrl, themeScope ["Application.QuitKey"].PropertyValue as Key); + // Assert.Contains (source, sourcesManager.Sources.Values); + //} + #endregion #region Load diff --git a/Tests/UnitTestsParallelizable/Drawing/RulerTests.cs b/Tests/UnitTestsParallelizable/Drawing/RulerTests.cs index cb059d639..9e2d9d320 100644 --- a/Tests/UnitTestsParallelizable/Drawing/RulerTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/RulerTests.cs @@ -54,7 +54,7 @@ public class RulerTests (ITestOutputHelper output) : FakeDriverBase IDriver driver = CreateFakeDriver (); var r = new Ruler (); - r.Draw (Point.Empty, driver: driver); + r.Draw (driver: driver, location: Point.Empty); DriverAssert.AssertDriverContentsWithFrameAre (@"", output, driver); } @@ -69,7 +69,7 @@ public class RulerTests (ITestOutputHelper output) : FakeDriverBase Assert.Equal (Orientation.Horizontal, r.Orientation); r.Length = len; - r.Draw (Point.Empty, driver: driver); + r.Draw (driver: driver, location: Point.Empty); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -79,7 +79,7 @@ public class RulerTests (ITestOutputHelper output) : FakeDriverBase ); // Postive offset - r.Draw (new (1, 1), driver: driver); + r.Draw (driver: driver, location: new (1, 1)); DriverAssert.AssertDriverContentsAre ( @" @@ -91,7 +91,7 @@ public class RulerTests (ITestOutputHelper output) : FakeDriverBase ); // Negative offset - r.Draw (new (-1, 3), driver: driver); + r.Draw (driver: driver, location: new (-1, 3)); DriverAssert.AssertDriverContentsAre ( @" @@ -114,7 +114,7 @@ public class RulerTests (ITestOutputHelper output) : FakeDriverBase var r = new Ruler (); r.Orientation = Orientation.Vertical; r.Length = len; - r.Draw (Point.Empty, driver: driver); + r.Draw (driver: driver, location: Point.Empty); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -137,7 +137,7 @@ public class RulerTests (ITestOutputHelper output) : FakeDriverBase driver ); - r.Draw (new (1, 1), driver: driver); + r.Draw (driver: driver, location: new (1, 1)); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -162,7 +162,7 @@ public class RulerTests (ITestOutputHelper output) : FakeDriverBase ); // Negative offset - r.Draw (new (2, -1), driver: driver); + r.Draw (driver: driver, location: new (2, -1)); DriverAssert.AssertDriverContentsWithFrameAre ( @" diff --git a/Tests/UnitTestsParallelizable/Drawing/ThicknessTests.cs b/Tests/UnitTestsParallelizable/Drawing/ThicknessTests.cs index a5cfc0436..65105cb89 100644 --- a/Tests/UnitTestsParallelizable/Drawing/ThicknessTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/ThicknessTests.cs @@ -1,4 +1,5 @@ using System.Text; +using Terminal.Gui.Drivers; using UnitTests; using Xunit.Abstractions; @@ -634,7 +635,7 @@ public class ThicknessTests (ITestOutputHelper output) : FakeDriverBase new (0, 0, driver!.Cols, driver!.Rows), (Rune)' ' ); - t.Draw (r, ViewDiagnosticFlags.Thickness, "Test", driver); + t.Draw (driver, r, ViewDiagnosticFlags.Thickness, "Test"); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -650,7 +651,7 @@ public class ThicknessTests (ITestOutputHelper output) : FakeDriverBase new (0, 0, driver!.Cols, driver!.Rows), (Rune)' ' ); - t.Draw (r, ViewDiagnosticFlags.Thickness, "Test", driver); + t.Draw (driver, r, ViewDiagnosticFlags.Thickness, "Test"); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -680,7 +681,7 @@ public class ThicknessTests (ITestOutputHelper output) : FakeDriverBase new (0, 0, driver!.Cols, driver!.Rows), (Rune)' ' ); - t.Draw (r, ViewDiagnosticFlags.Thickness, "Test", driver); + t.Draw (driver, r, ViewDiagnosticFlags.Thickness, "Test"); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -710,7 +711,7 @@ public class ThicknessTests (ITestOutputHelper output) : FakeDriverBase new (0, 0, driver!.Cols, driver!.Rows), (Rune)' ' ); - t.Draw (r, ViewDiagnosticFlags.Thickness, "Test", driver); + t.Draw (driver, r, ViewDiagnosticFlags.Thickness, "Test"); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -754,7 +755,8 @@ public class ThicknessTests (ITestOutputHelper output) : FakeDriverBase var r = new Rectangle (2, 2, 40, 15); top.Draw (); - t.Draw (r, ViewDiagnosticFlags.Ruler, "Test", driver); + top.SetClipToScreen (); + t.Draw (driver, r, ViewDiagnosticFlags.Ruler, "Test"); DriverAssert.AssertDriverContentsAre ( @" @@ -786,7 +788,8 @@ public class ThicknessTests (ITestOutputHelper output) : FakeDriverBase r = new (1, 1, 40, 15); top.SetNeedsDraw (); top.Draw (); - t.Draw (r, ViewDiagnosticFlags.Ruler, "Test", driver); + top.SetClipToScreen (); + t.Draw (driver, r, ViewDiagnosticFlags.Ruler, "Test"); DriverAssert.AssertDriverContentsAre ( @" @@ -818,7 +821,8 @@ public class ThicknessTests (ITestOutputHelper output) : FakeDriverBase r = new (2, 2, 40, 15); top.SetNeedsDraw (); top.Draw (); - t.Draw (r, ViewDiagnosticFlags.Ruler, "Test", driver); + top.SetClipToScreen (); + t.Draw (driver, r, ViewDiagnosticFlags.Ruler, "Test"); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -850,7 +854,8 @@ public class ThicknessTests (ITestOutputHelper output) : FakeDriverBase r = new (5, 5, 40, 15); top.SetNeedsDraw (); top.Draw (); - t.Draw (r, ViewDiagnosticFlags.Ruler, "Test", driver); + top.SetClipToScreen (); + t.Draw (driver, r, ViewDiagnosticFlags.Ruler, "Test"); DriverAssert.AssertDriverContentsWithFrameAre ( @" diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiRequestSchedulerTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiRequestSchedulerTests.cs index d643c5112..aafa8243f 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiRequestSchedulerTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiRequestSchedulerTests.cs @@ -34,7 +34,7 @@ public class AnsiRequestSchedulerTests _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), null, false)).Verifiable (Times.Once); // Act - bool result = _scheduler.SendOrSchedule (request); + bool result = _scheduler.SendOrSchedule (null, request); // Assert Assert.Empty (_scheduler.QueuedRequests); // We sent it i.e. we did not queue it for later @@ -57,7 +57,7 @@ public class AnsiRequestSchedulerTests _parserMock.Setup (p => p.IsExpecting ("c")).Returns (true).Verifiable (Times.Once); // Act - bool result = _scheduler.SendOrSchedule (request1); + bool result = _scheduler.SendOrSchedule (null, request1); // Assert Assert.Single (_scheduler.QueuedRequests); // Ensure only one request is in the queue @@ -80,7 +80,7 @@ public class AnsiRequestSchedulerTests _parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable (Times.Exactly (2)); _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), null, false)).Verifiable (Times.Exactly (2)); - _scheduler.SendOrSchedule (request); + _scheduler.SendOrSchedule (null, request); // Simulate time passing beyond throttle SetTime (101); // Exceed throttle limit @@ -88,7 +88,7 @@ public class AnsiRequestSchedulerTests // Act // Send another request after the throttled time limit - bool result = _scheduler.SendOrSchedule (request); + bool result = _scheduler.SendOrSchedule (null, request); // Assert Assert.Empty (_scheduler.QueuedRequests); // Should send and clear the request @@ -111,7 +111,7 @@ public class AnsiRequestSchedulerTests _parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable (Times.Exactly (2)); _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), null, false)).Verifiable (Times.Exactly (2)); - _scheduler.SendOrSchedule (request); + _scheduler.SendOrSchedule (null, request); // Simulate time passing SetTime (55); // Does not exceed throttle limit @@ -119,24 +119,24 @@ public class AnsiRequestSchedulerTests // Act // Send another request after the throttled time limit - bool result = _scheduler.SendOrSchedule (request); + bool result = _scheduler.SendOrSchedule (null, request); // Assert Assert.Single (_scheduler.QueuedRequests); // Should have been queued Assert.False (result); // Should have been queued // Throttle still not exceeded - Assert.False (_scheduler.RunSchedule ()); + Assert.False (_scheduler.RunSchedule (null)); SetTime (90); // Throttle still not exceeded - Assert.False (_scheduler.RunSchedule ()); + Assert.False (_scheduler.RunSchedule (null)); SetTime (105); // Throttle exceeded - so send the request - Assert.True (_scheduler.RunSchedule ()); + Assert.True (_scheduler.RunSchedule (null)); _parserMock.Verify (); } @@ -156,13 +156,13 @@ public class AnsiRequestSchedulerTests _parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable (Times.Once); _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), null, false)).Verifiable (Times.Exactly (2)); - Assert.True (_scheduler.SendOrSchedule (request1)); + Assert.True (_scheduler.SendOrSchedule (null, request1)); // Parser already has an ongoing request for "c" _parserMock.Setup (p => p.IsExpecting ("c")).Returns (true).Verifiable (Times.Exactly (2)); // Cannot send because there is already outstanding request - Assert.False (_scheduler.SendOrSchedule (request1)); + Assert.False (_scheduler.SendOrSchedule (null, request1)); Assert.Single (_scheduler.QueuedRequests); // Simulate request going stale @@ -178,7 +178,7 @@ public class AnsiRequestSchedulerTests .Verifiable (); // When we send again the evicted one should be - bool evicted = _scheduler.RunSchedule (); + bool evicted = _scheduler.RunSchedule (null); Assert.True (evicted); // Stale request should be evicted Assert.Empty (_scheduler.QueuedRequests); @@ -191,7 +191,7 @@ public class AnsiRequestSchedulerTests public void RunSchedule_DoesNothing_WhenQueueIsEmpty () { // Act - bool result = _scheduler.RunSchedule (); + bool result = _scheduler.RunSchedule (null); // Assert Assert.False (result); // No requests to process @@ -213,8 +213,8 @@ public class AnsiRequestSchedulerTests _parserMock.Setup (p => p.ExpectResponse ("x", It.IsAny> (), null, false)).Verifiable (Times.Once); // Act - bool a = _scheduler.SendOrSchedule (request1); - bool b = _scheduler.SendOrSchedule (request2); + bool a = _scheduler.SendOrSchedule (null, request1); + bool b = _scheduler.SendOrSchedule (null, request2); // Assert Assert.False (a); diff --git a/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs b/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs new file mode 100644 index 000000000..e17c28cdf --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs @@ -0,0 +1,342 @@ +using System.Text; +using UnitTests; +using Xunit.Abstractions; + +namespace UnitTests_Parallelizable.DriverTests; + +/// +/// Tests for the ToAnsi functionality that generates ANSI escape sequences from buffer contents. +/// +public class ToAnsiTests : FakeDriverBase +{ + [Fact] + public void ToAnsi_Empty_Buffer () + { + IDriver driver = CreateFakeDriver (10, 5); + string ansi = driver.ToAnsi (); + + // Empty buffer should have newlines for each row + Assert.Contains ("\n", ansi); + // Should have 5 newlines (one per row) + Assert.Equal (5, ansi.Count (c => c == '\n')); + } + + [Fact] + public void ToAnsi_Simple_Text () + { + IDriver driver = CreateFakeDriver (10, 3); + driver.AddStr ("Hello"); + driver.Move (0, 1); + driver.AddStr ("World"); + + string ansi = driver.ToAnsi (); + + // Should contain the text + Assert.Contains ("Hello", ansi); + Assert.Contains ("World", ansi); + + // Should have proper structure with newlines + string[] lines = ansi.Split (['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + Assert.Equal (3, lines.Length); + } + + [Fact] + public void ToAnsi_With_Colors () + { + IDriver driver = CreateFakeDriver (10, 2); + + // Set red foreground + driver.CurrentAttribute = new Attribute (Color.Red, Color.Black); + driver.AddStr ("Red"); + driver.Move (0, 1); + + // Set blue foreground + driver.CurrentAttribute = new Attribute (Color.Blue, Color.Black); + driver.AddStr ("Blue"); + + string ansi = driver.ToAnsi (); + + // Should contain ANSI color codes + Assert.Contains ("\u001b[31m", ansi); // Red foreground + Assert.Contains ("\u001b[34m", ansi); // Blue foreground + Assert.Contains ("Red", ansi); + Assert.Contains ("Blue", ansi); + } + + [Fact] + public void ToAnsi_With_Background_Colors () + { + IDriver driver = CreateFakeDriver (10, 2); + + // Set background color + driver.CurrentAttribute = new Attribute (Color.White, Color.Red); + driver.AddStr ("WhiteOnRed"); + + string ansi = driver.ToAnsi (); + + // Should contain ANSI background color code + Assert.Contains ("\u001b[41m", ansi); // Red background + Assert.Contains ("WhiteOnRed", ansi); + } + + [Fact] + public void ToAnsi_With_Text_Styles () + { + IDriver driver = CreateFakeDriver (10, 3); + + // Bold text + driver.CurrentAttribute = new Attribute (Color.White, Color.Black, TextStyle.Bold); + driver.AddStr ("Bold"); + driver.Move (0, 1); + + // Italic text + driver.CurrentAttribute = new Attribute (Color.White, Color.Black, TextStyle.Italic); + driver.AddStr ("Italic"); + driver.Move (0, 2); + + // Underline text + driver.CurrentAttribute = new Attribute (Color.White, Color.Black, TextStyle.Underline); + driver.AddStr ("Underline"); + + string ansi = driver.ToAnsi (); + + // Should contain ANSI style codes + Assert.Contains ("\u001b[1m", ansi); // Bold + Assert.Contains ("\u001b[3m", ansi); // Italic + Assert.Contains ("\u001b[4m", ansi); // Underline + } + + [Fact] + public void ToAnsi_With_Wide_Characters () + { + IDriver driver = CreateFakeDriver (10, 2); + + // Add a wide character (Chinese character) + driver.AddStr ("??"); + driver.Move (0, 1); + driver.AddStr ("??"); + + string ansi = driver.ToAnsi (); + + Assert.Contains ("??", ansi); + Assert.Contains ("??", ansi); + } + + [Fact] + public void ToAnsi_With_Unicode_Characters () + { + IDriver driver = CreateFakeDriver (10, 2); + + // Add various Unicode characters + driver.AddStr ("???"); // Greek letters + driver.Move (0, 1); + driver.AddStr ("???"); // Emoji + + string ansi = driver.ToAnsi (); + + Assert.Contains ("???", ansi); + Assert.Contains ("???", ansi); + } + + [Fact] + public void ToAnsi_Attribute_Changes_Within_Line () + { + IDriver driver = CreateFakeDriver (20, 1); + + driver.AddStr ("Normal"); + driver.CurrentAttribute = new Attribute (Color.Red, Color.Black); + driver.AddStr ("Red"); + driver.CurrentAttribute = new Attribute (Color.Blue, Color.Black); + driver.AddStr ("Blue"); + + string ansi = driver.ToAnsi (); + + // Should contain color changes within the line + Assert.Contains ("Normal", ansi); + Assert.Contains ("\u001b[31m", ansi); // Red + Assert.Contains ("\u001b[34m", ansi); // Blue + } + + [Fact] + public void ToAnsi_Large_Buffer () + { + // Test with a larger buffer to stress performance + IDriver driver = CreateFakeDriver (200, 50); + + // Fill with some content + for (int row = 0; row < 50; row++) + { + driver.Move (0, row); + driver.CurrentAttribute = new Attribute ((ColorName16)(row % 16), Color.Black); + driver.AddStr ($"Row {row:D2} content"); + } + + string ansi = driver.ToAnsi (); + + // Should contain all rows + Assert.Contains ("Row 00", ansi); + Assert.Contains ("Row 49", ansi); + + // Should have proper newlines (50 content lines + 50 newlines) + Assert.Equal (50, ansi.Count (c => c == '\n')); + } + + [Fact] + public void ToAnsi_RGB_Colors () + { + IDriver driver = CreateFakeDriver (10, 1); + + // Use RGB colors (when not forcing 16 colors) + Application.Force16Colors = false; + try + { + driver.CurrentAttribute = new Attribute (new Color (255, 0, 0), new Color (0, 255, 0)); + driver.AddStr ("RGB"); + + string ansi = driver.ToAnsi (); + + // Should contain RGB color codes + Assert.Contains ("\u001b[38;2;255;0;0m", ansi); // Red foreground RGB + Assert.Contains ("\u001b[48;2;0;255;0m", ansi); // Green background RGB + } + finally + { + Application.Force16Colors = true; // Reset + } + } + + [Fact] + public void ToAnsi_Force16Colors () + { + IDriver driver = CreateFakeDriver (10, 1); + + // Force 16 colors + Application.Force16Colors = true; + driver.CurrentAttribute = new Attribute (Color.Red, Color.Blue); + driver.AddStr ("16Color"); + + string ansi = driver.ToAnsi (); + + // Should contain 16-color codes, not RGB + Assert.Contains ("\u001b[31m", ansi); // Red foreground (16-color) + Assert.Contains ("\u001b[44m", ansi); // Blue background (16-color) + Assert.DoesNotContain ("\u001b[38;2;", ansi); // No RGB codes + } + + [Fact] + public void ToAnsi_Multiple_Attributes_Per_Line () + { + IDriver driver = CreateFakeDriver (50, 1); + + // Create a line with many attribute changes + string[] colors = { "Red", "Green", "Blue", "Yellow", "Magenta", "Cyan" }; + + foreach (string colorName in colors) + { + Color fg = colorName switch + { + "Red" => Color.Red, + "Green" => Color.Green, + "Blue" => Color.Blue, + "Yellow" => Color.Yellow, + "Magenta" => Color.Magenta, + "Cyan" => Color.Cyan, + _ => Color.White + }; + + driver.CurrentAttribute = new Attribute (fg, Color.Black); + driver.AddStr (colorName); + } + + string ansi = driver.ToAnsi (); + + // Should contain multiple color codes + Assert.Contains ("\u001b[31m", ansi); // Red + Assert.Contains ("\u001b[32m", ansi); // Green + Assert.Contains ("\u001b[34m", ansi); // Blue + Assert.Contains ("\u001b[33m", ansi); // Yellow + Assert.Contains ("\u001b[35m", ansi); // Magenta + Assert.Contains ("\u001b[36m", ansi); // Cyan + } + + [Fact] + public void ToAnsi_Special_Characters () + { + IDriver driver = CreateFakeDriver (20, 1); + + // Test backslash character + driver.AddStr ("Backslash:"); + driver.AddRune ('\\'); + + string ansi = driver.ToAnsi (); + + Assert.Contains ("Backslash:", ansi); + Assert.Contains ("\\", ansi); + } + + [Fact] + public void ToAnsi_Buffer_Boundary_Conditions () + { + // Test with minimum buffer size + IDriver driver = CreateFakeDriver (1, 1); + driver.AddStr ("X"); + + string ansi = driver.ToAnsi (); + + Assert.Contains ("X", ansi); + Assert.Contains ("\n", ansi); + + // Test with very wide buffer + driver = CreateFakeDriver (1000, 1); + driver.AddStr ("Wide"); + + ansi = driver.ToAnsi (); + + Assert.Contains ("Wide", ansi); + Assert.True (ansi.Length > 1000); // Should have many spaces + } + + [Fact] + public void ToAnsi_Empty_Lines () + { + IDriver driver = CreateFakeDriver (10, 3); + + // Only write to first and third lines + driver.AddStr ("First"); + driver.Move (0, 2); + driver.AddStr ("Third"); + + string ansi = driver.ToAnsi (); + + string[] lines = ansi.Split ('\n'); + Assert.Equal (4, lines.Length); // 3 content lines + 1 empty line at end + Assert.Contains ("First", lines[0]); + Assert.Contains ("Third", lines[2]); + } + + [Fact] + public void ToAnsi_Performance_Stress_Test () + { + // Create a large buffer and fill it completely + const int width = 200; + const int height = 100; + IDriver driver = CreateFakeDriver (width, height); + + // Fill every cell with different content and colors + for (int row = 0; row < height; row++) + { + for (int col = 0; col < width; col++) + { + driver.Move (col, row); + driver.CurrentAttribute = new Attribute ((ColorName16)((row + col) % 16), Color.Black); + driver.AddRune ((char)('A' + ((row + col) % 26))); + } + } + + // This should complete in reasonable time and not throw + string ansi = driver.ToAnsi (); + + Assert.NotNull (ansi); + Assert.True (ansi.Length > width * height); // Should contain all characters plus ANSI codes + } +} \ No newline at end of file diff --git a/Tests/UnitTestsParallelizable/TestSetup.cs b/Tests/UnitTestsParallelizable/TestSetup.cs index 2a6402c11..a8d37578e 100644 --- a/Tests/UnitTestsParallelizable/TestSetup.cs +++ b/Tests/UnitTestsParallelizable/TestSetup.cs @@ -39,7 +39,7 @@ public class GlobalTestSetup : IDisposable // Check that all Application fields and properties are set to their default values // Public Properties - Assert.Null (Application.Top); + Assert.Null (Application.Current); Assert.Null (Application.Mouse.MouseGrabView); // Don't check Application.ForceDriver @@ -59,7 +59,7 @@ public class GlobalTestSetup : IDisposable Assert.Equal (Application.GetSupportedCultures (), Application.SupportedCultures); Assert.Equal (Application.GetAvailableCulturesFromEmbeddedResources (), Application.SupportedCultures); Assert.Null (Application.MainThreadId); - Assert.Empty (Application.TopLevels); + Assert.Empty (Application.SessionStack); Assert.Empty (Application.CachedViewsUnderMouse); // Mouse @@ -67,10 +67,10 @@ public class GlobalTestSetup : IDisposable //Assert.Null (Application._lastMousePosition); // Navigation - Assert.Null (Application.Navigation); + // Assert.Null (Application.Navigation); // Popover - Assert.Null (Application.Popover); + //Assert.Null (Application.Popover); // Events - Can't check //Assert.Null (Application.SessionBegun); diff --git a/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs b/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs index 0c142f08b..17ac27ab5 100644 --- a/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs +++ b/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs @@ -36,7 +36,7 @@ public class TextFormatterDrawTests (ITestOutputHelper output) : FakeDriverBase tf.ConstrainToWidth = width; tf.ConstrainToHeight = height; - tf.Draw (new (0, 0, width, height), Attribute.Default, Attribute.Default, driver: driver); + tf.Draw (driver: driver, screen: new (0, 0, width, height), normalColor: Attribute.Default, hotColor: Attribute.Default); DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver); } @@ -65,7 +65,7 @@ public class TextFormatterDrawTests (ITestOutputHelper output) : FakeDriverBase tf.ConstrainToWidth = width; tf.ConstrainToHeight = height; - tf.Draw (new (0, 0, width, height), Attribute.Default, Attribute.Default, driver: driver); + tf.Draw (driver: driver, screen: new (0, 0, width, height), normalColor: Attribute.Default, hotColor: Attribute.Default); DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver); } @@ -105,7 +105,7 @@ public class TextFormatterDrawTests (ITestOutputHelper output) : FakeDriverBase tf.ConstrainToWidth = width; tf.ConstrainToHeight = height; - tf.Draw (new (Point.Empty, new (width, height)), Attribute.Default, Attribute.Default, driver: driver); + tf.Draw (driver: driver, screen: new (Point.Empty, new (width, height)), normalColor: Attribute.Default, hotColor: Attribute.Default); Rectangle rect = DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver); Assert.Equal (expectedY, rect.Y); } @@ -134,7 +134,7 @@ public class TextFormatterDrawTests (ITestOutputHelper output) : FakeDriverBase tf.ConstrainToWidth = width; tf.ConstrainToHeight = height; - tf.Draw (new (0, 0, width, height), Attribute.Default, Attribute.Default, driver: driver); + tf.Draw (driver: driver, screen: new (0, 0, width, height), normalColor: Attribute.Default, hotColor: Attribute.Default); DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver); } @@ -163,7 +163,7 @@ public class TextFormatterDrawTests (ITestOutputHelper output) : FakeDriverBase tf.ConstrainToWidth = width; tf.ConstrainToHeight = height; - tf.Draw (new (0, 0, width, height), Attribute.Default, Attribute.Default, driver: driver); + tf.Draw (driver: driver, screen: new (0, 0, width, height), normalColor: Attribute.Default, hotColor: Attribute.Default); DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver); } @@ -217,7 +217,7 @@ s")] tf.ConstrainToWidth = width; tf.ConstrainToHeight = height; - tf.Draw (new (0, 0, 20, 20), Attribute.Default, Attribute.Default, driver: driver); + tf.Draw (driver: driver, screen: new (0, 0, 20, 20), normalColor: Attribute.Default, hotColor: Attribute.Default); DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver); } @@ -267,7 +267,7 @@ s")] tf.ConstrainToWidth = width; tf.ConstrainToHeight = height; - tf.Draw (new (0, 0, 5, height), Attribute.Default, Attribute.Default, driver: driver); + tf.Draw (driver: driver, screen: new (0, 0, 5, height), normalColor: Attribute.Default, hotColor: Attribute.Default); Rectangle rect = DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver); Assert.Equal (expectedY, rect.Y); @@ -328,7 +328,7 @@ B ")] tf.ConstrainToWidth = 5; tf.ConstrainToHeight = height; - tf.Draw (new (0, 0, 5, height), Attribute.Default, Attribute.Default, driver: driver); + tf.Draw (driver: driver, screen: new (0, 0, 5, height), normalColor: Attribute.Default, hotColor: Attribute.Default); DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver); } @@ -346,11 +346,7 @@ B ")] }; var tf = new TextFormatter { ConstrainToSize = new (14, 3), Text = "Test\nTest long\nTest long long\n", MultiLine = true }; - tf.Draw ( - new (1, 1, 19, 3), - attrs [1], - attrs [2], - driver: driver); + tf.Draw (driver: driver, screen: new (1, 1, 19, 3), normalColor: attrs [1], hotColor: attrs [2]); Assert.False (tf.FillRemaining); @@ -375,11 +371,7 @@ B ")] tf.FillRemaining = true; - tf.Draw ( - new (1, 1, 19, 3), - attrs [1], - attrs [2], - driver: driver); + tf.Draw (driver: driver, screen: new (1, 1, 19, 3), normalColor: attrs [1], hotColor: attrs [2]); DriverAssert.AssertDriverAttributesAre ( @" @@ -422,7 +414,7 @@ Nice Work")] MultiLine = true }; - tf.Draw (new (0, 0, width, height), Attribute.Default, Attribute.Default, driver: driver); + tf.Draw (driver: driver, screen: new (0, 0, width, height), normalColor: Attribute.Default, hotColor: Attribute.Default); DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver); } @@ -448,7 +440,7 @@ Nice Work")] driver!.SetScreenSize (tfSize.Width, tfSize.Height); driver.FillRect (driver.Screen, (Rune)'*'); - tf.Draw (driver.Screen, Attribute.Default, Attribute.Default, driver: driver); + tf.Draw (driver: driver, screen: driver.Screen, normalColor: Attribute.Default, hotColor: Attribute.Default); var expectedText = """ UI Catalog: A comprehensive sample library and test app for @@ -575,7 +567,7 @@ Nice Work")] Size size = tf.FormatAndGetSize (); Assert.Equal (new (expectedWidth, expectedHeight), size); - tf.Draw (new (0, 0, width, height), Attribute.Default, Attribute.Default, driver: driver); + tf.Draw (driver: driver, screen: new (0, 0, width, height), normalColor: Attribute.Default, hotColor: Attribute.Default); DriverAssert.AssertDriverContentsWithFrameAre (expectedDraw, output, driver); } @@ -660,7 +652,7 @@ Nice Work")] Size size = tf.FormatAndGetSize (); Assert.Equal (new (expectedWidth, expectedHeight), size); - tf.Draw (new (0, 0, width, height), Attribute.Default, Attribute.Default, driver: driver); + tf.Draw (driver: driver, screen: new (0, 0, width, height), normalColor: Attribute.Default, hotColor: Attribute.Default); DriverAssert.AssertDriverContentsWithFrameAre (expectedDraw, output, driver); } diff --git a/Tests/UnitTestsParallelizable/Text/TextFormatterJustificationTests.cs b/Tests/UnitTestsParallelizable/Text/TextFormatterJustificationTests.cs index 4bae12df3..8990e1698 100644 --- a/Tests/UnitTestsParallelizable/Text/TextFormatterJustificationTests.cs +++ b/Tests/UnitTestsParallelizable/Text/TextFormatterJustificationTests.cs @@ -3423,7 +3423,7 @@ public class TextFormatterJustificationTests (ITestOutputHelper output) : FakeDr }; driver.FillRect (new (0, 0, 7, 7), (Rune)'*'); - tf.Draw (new (0, 0, 7, 7), Attribute.Default, Attribute.Default, driver: driver); + tf.Draw (driver: driver, screen: new (0, 0, 7, 7), normalColor: Attribute.Default, hotColor: Attribute.Default); DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver); } } diff --git a/Tests/UnitTestsParallelizable/Text/TextFormatterTests.cs b/Tests/UnitTestsParallelizable/Text/TextFormatterTests.cs index 88e95fd7f..8fc71180c 100644 --- a/Tests/UnitTestsParallelizable/Text/TextFormatterTests.cs +++ b/Tests/UnitTestsParallelizable/Text/TextFormatterTests.cs @@ -1142,6 +1142,7 @@ public class TextFormatterTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void NeedsFormat_Sets () { + IDriver driver = CreateFakeDriver (); var testText = "test"; var testBounds = new Rectangle (0, 0, 100, 1); var tf = new TextFormatter (); @@ -1151,7 +1152,7 @@ public class TextFormatterTests (ITestOutputHelper output) : FakeDriverBase Assert.NotEmpty (tf.GetLines ()); Assert.False (tf.NeedsFormat); // get_Lines causes a Format Assert.Equal (testText, tf.Text); - tf.Draw (testBounds, new (), new ()); + tf.Draw (driver: driver, screen: testBounds, normalColor: new (), hotColor: new ()); Assert.False (tf.NeedsFormat); tf.ConstrainToSize = new (1, 1); @@ -2987,7 +2988,7 @@ public class TextFormatterTests (ITestOutputHelper output) : FakeDriverBase ConstrainToHeight = 1 }; - tf.Draw (new Rectangle (0, 0, width, 1), Attribute.Default, Attribute.Default, default, driver); + tf.Draw (driver, new Rectangle (0, 0, width, 1), Attribute.Default, Attribute.Default, default); DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver); } @@ -3016,7 +3017,7 @@ public class TextFormatterTests (ITestOutputHelper output) : FakeDriverBase ConstrainToHeight = 1 }; - tf.Draw (new Rectangle (0, 0, width, 1), Attribute.Default, Attribute.Default, default, driver); + tf.Draw (driver, new Rectangle (0, 0, width, 1), Attribute.Default, Attribute.Default, default); DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver); } @@ -3042,7 +3043,7 @@ public class TextFormatterTests (ITestOutputHelper output) : FakeDriverBase ConstrainToHeight = 1 }; - tf.Draw (new Rectangle (0, 0, width, 1), Attribute.Default, Attribute.Default, default, driver); + tf.Draw (driver, new Rectangle (0, 0, width, 1), Attribute.Default, Attribute.Default, default); DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver); } @@ -3068,7 +3069,7 @@ public class TextFormatterTests (ITestOutputHelper output) : FakeDriverBase ConstrainToHeight = 1 }; - tf.Draw (new Rectangle (0, 0, width, 1), Attribute.Default, Attribute.Default, default, driver); + tf.Draw (driver, new Rectangle (0, 0, width, 1), Attribute.Default, Attribute.Default, default); DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver); } @@ -3100,12 +3101,11 @@ ssb tf.ConstrainToSize = new (width, height); tf.Draw ( + driver, new (0, 0, width, height), new (ColorName16.White, ColorName16.Black), new (ColorName16.Blue, ColorName16.Black), - default (Rectangle), - driver - ); + default (Rectangle)); DriverAssert.AssertDriverContentsWithFrameAre (expected, output, driver); driver.End (); @@ -3139,12 +3139,11 @@ ssb Assert.True (tf.WordWrap); tf.Draw ( + driver, new (0, 0, width, height), new (ColorName16.White, ColorName16.Black), new (ColorName16.Blue, ColorName16.Black), - default (Rectangle), - driver - ); + default (Rectangle)); DriverAssert.AssertDriverContentsWithFrameAre (expected, output, driver); driver.End (); @@ -3178,12 +3177,11 @@ ssb Assert.False (tf.PreserveTrailingSpaces); tf.Draw ( + driver, new (0, 0, width, height), new (ColorName16.White, ColorName16.Black), new (ColorName16.Blue, ColorName16.Black), - default (Rectangle), - driver - ); + default (Rectangle)); DriverAssert.AssertDriverContentsWithFrameAre (expected, output, driver); driver.End (); @@ -3217,12 +3215,11 @@ ssb Assert.False (tf.PreserveTrailingSpaces); tf.Draw ( + driver, new (0, 0, width, height), new (ColorName16.White, ColorName16.Black), new (ColorName16.Blue, ColorName16.Black), - default (Rectangle), - driver - ); + default (Rectangle)); DriverAssert.AssertDriverContentsWithFrameAre (expected, output, driver); driver.End (); diff --git a/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj b/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj index cf0f77f7c..35233cc03 100644 --- a/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj +++ b/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj @@ -69,4 +69,7 @@ + + + \ No newline at end of file diff --git a/Tests/UnitTestsParallelizable/View/ArrangementTests.cs b/Tests/UnitTestsParallelizable/View/ArrangementTests.cs index 842a7070b..cc156b796 100644 --- a/Tests/UnitTestsParallelizable/View/ArrangementTests.cs +++ b/Tests/UnitTestsParallelizable/View/ArrangementTests.cs @@ -28,11 +28,11 @@ public class ArrangementTests (ITestOutputHelper output) [Fact] public void ViewArrangement_Resizable_IsCombinationOfAllResizableFlags () { - ViewArrangement expected = ViewArrangement.LeftResizable - | ViewArrangement.RightResizable - | ViewArrangement.TopResizable + ViewArrangement expected = ViewArrangement.LeftResizable + | ViewArrangement.RightResizable + | ViewArrangement.TopResizable | ViewArrangement.BottomResizable; - + Assert.Equal (ViewArrangement.Resizable, expected); } @@ -40,7 +40,7 @@ public class ArrangementTests (ITestOutputHelper output) public void ViewArrangement_CanCombineFlags () { ViewArrangement arrangement = ViewArrangement.Movable | ViewArrangement.LeftResizable; - + Assert.True (arrangement.HasFlag (ViewArrangement.Movable)); Assert.True (arrangement.HasFlag (ViewArrangement.LeftResizable)); Assert.False (arrangement.HasFlag (ViewArrangement.RightResizable)); @@ -67,11 +67,11 @@ public class ArrangementTests (ITestOutputHelper output) [Fact] public void View_Arrangement_CanSetMultipleFlags () { - var view = new View - { - Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable + var view = new View + { + Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable }; - + Assert.True (view.Arrangement.HasFlag (ViewArrangement.Movable)); Assert.True (view.Arrangement.HasFlag (ViewArrangement.LeftResizable)); Assert.True (view.Arrangement.HasFlag (ViewArrangement.RightResizable)); @@ -83,7 +83,7 @@ public class ArrangementTests (ITestOutputHelper output) public void View_Arrangement_Overlapped_CanBeSetIndependently () { var view = new View { Arrangement = ViewArrangement.Overlapped }; - + Assert.True (view.Arrangement.HasFlag (ViewArrangement.Overlapped)); Assert.False (view.Arrangement.HasFlag (ViewArrangement.Movable)); Assert.False (view.Arrangement.HasFlag (ViewArrangement.Resizable)); @@ -92,11 +92,11 @@ public class ArrangementTests (ITestOutputHelper output) [Fact] public void View_Arrangement_CanCombineOverlappedWithOtherFlags () { - var view = new View - { - Arrangement = ViewArrangement.Overlapped | ViewArrangement.Movable + var view = new View + { + Arrangement = ViewArrangement.Overlapped | ViewArrangement.Movable }; - + Assert.True (view.Arrangement.HasFlag (ViewArrangement.Overlapped)); Assert.True (view.Arrangement.HasFlag (ViewArrangement.Movable)); } @@ -108,12 +108,12 @@ public class ArrangementTests (ITestOutputHelper output) [Fact] public void TopResizable_WithoutMovable_IsAllowed () { - var view = new View - { + var view = new View + { Arrangement = ViewArrangement.TopResizable, BorderStyle = LineStyle.Single }; - + Assert.True (view.Arrangement.HasFlag (ViewArrangement.TopResizable)); Assert.False (view.Arrangement.HasFlag (ViewArrangement.Movable)); } @@ -123,16 +123,16 @@ public class ArrangementTests (ITestOutputHelper output) { // According to docs and Border.Arrangment.cs line 569: // TopResizable is only checked if NOT Movable - var view = new View - { + var view = new View + { Arrangement = ViewArrangement.Movable | ViewArrangement.TopResizable, BorderStyle = LineStyle.Single }; - + // Both flags can be set on the property Assert.True (view.Arrangement.HasFlag (ViewArrangement.Movable)); Assert.True (view.Arrangement.HasFlag (ViewArrangement.TopResizable)); - + // But the behavior in Border.DetermineArrangeModeFromClick // will prioritize Movable over TopResizable } @@ -140,12 +140,12 @@ public class ArrangementTests (ITestOutputHelper output) [Fact] public void Resizable_WithMovable_IncludesTopResizable () { - var view = new View - { + var view = new View + { Arrangement = ViewArrangement.Resizable | ViewArrangement.Movable, BorderStyle = LineStyle.Single }; - + Assert.True (view.Arrangement.HasFlag (ViewArrangement.Movable)); Assert.True (view.Arrangement.HasFlag (ViewArrangement.TopResizable)); Assert.True (view.Arrangement.HasFlag (ViewArrangement.LeftResizable)); @@ -160,12 +160,12 @@ public class ArrangementTests (ITestOutputHelper output) [Fact] public void Border_WithNoArrangement_HasNoArrangementOptions () { - var view = new View - { + var view = new View + { Arrangement = ViewArrangement.Fixed, BorderStyle = LineStyle.Single }; - + Assert.NotNull (view.Border); Assert.Equal (ViewArrangement.Fixed, view.Arrangement); } @@ -174,8 +174,8 @@ public class ArrangementTests (ITestOutputHelper output) public void Border_WithMovableArrangement_CanEnterArrangeMode () { var superView = new View (); - var view = new View - { + var view = new View + { Arrangement = ViewArrangement.Movable, BorderStyle = LineStyle.Single, X = 0, @@ -184,7 +184,7 @@ public class ArrangementTests (ITestOutputHelper output) Height = 10 }; superView.Add (view); - + Assert.NotNull (view.Border); Assert.True (view.Arrangement.HasFlag (ViewArrangement.Movable)); } @@ -192,12 +192,12 @@ public class ArrangementTests (ITestOutputHelper output) [Fact] public void Border_WithResizableArrangement_HasResizableOptions () { - var view = new View - { + var view = new View + { Arrangement = ViewArrangement.Resizable, BorderStyle = LineStyle.Single }; - + Assert.NotNull (view.Border); Assert.True (view.Arrangement.HasFlag (ViewArrangement.LeftResizable)); Assert.True (view.Arrangement.HasFlag (ViewArrangement.RightResizable)); @@ -212,15 +212,15 @@ public class ArrangementTests (ITestOutputHelper output) [InlineData (ViewArrangement.BottomResizable)] public void Border_WithSingleResizableDirection_OnlyHasThatOption (ViewArrangement arrangement) { - var view = new View - { + var view = new View + { Arrangement = arrangement, BorderStyle = LineStyle.Single }; - + Assert.NotNull (view.Border); Assert.True (view.Arrangement.HasFlag (arrangement)); - + // Verify other directions are not set if (arrangement != ViewArrangement.LeftResizable) Assert.False (view.Arrangement.HasFlag (ViewArrangement.LeftResizable)); @@ -239,12 +239,12 @@ public class ArrangementTests (ITestOutputHelper output) [Fact] public void Border_BottomRightResizable_CombinesBothFlags () { - var view = new View - { + var view = new View + { Arrangement = ViewArrangement.BottomResizable | ViewArrangement.RightResizable, BorderStyle = LineStyle.Single }; - + Assert.True (view.Arrangement.HasFlag (ViewArrangement.BottomResizable)); Assert.True (view.Arrangement.HasFlag (ViewArrangement.RightResizable)); Assert.False (view.Arrangement.HasFlag (ViewArrangement.TopResizable)); @@ -254,12 +254,12 @@ public class ArrangementTests (ITestOutputHelper output) [Fact] public void Border_BottomLeftResizable_CombinesBothFlags () { - var view = new View - { + var view = new View + { Arrangement = ViewArrangement.BottomResizable | ViewArrangement.LeftResizable, BorderStyle = LineStyle.Single }; - + Assert.True (view.Arrangement.HasFlag (ViewArrangement.BottomResizable)); Assert.True (view.Arrangement.HasFlag (ViewArrangement.LeftResizable)); Assert.False (view.Arrangement.HasFlag (ViewArrangement.TopResizable)); @@ -269,12 +269,12 @@ public class ArrangementTests (ITestOutputHelper output) [Fact] public void Border_TopRightResizable_CombinesBothFlags () { - var view = new View - { + var view = new View + { Arrangement = ViewArrangement.TopResizable | ViewArrangement.RightResizable, BorderStyle = LineStyle.Single }; - + Assert.True (view.Arrangement.HasFlag (ViewArrangement.TopResizable)); Assert.True (view.Arrangement.HasFlag (ViewArrangement.RightResizable)); Assert.False (view.Arrangement.HasFlag (ViewArrangement.BottomResizable)); @@ -284,12 +284,12 @@ public class ArrangementTests (ITestOutputHelper output) [Fact] public void Border_TopLeftResizable_CombinesBothFlags () { - var view = new View - { + var view = new View + { Arrangement = ViewArrangement.TopResizable | ViewArrangement.LeftResizable, BorderStyle = LineStyle.Single }; - + Assert.True (view.Arrangement.HasFlag (ViewArrangement.TopResizable)); Assert.True (view.Arrangement.HasFlag (ViewArrangement.LeftResizable)); Assert.False (view.Arrangement.HasFlag (ViewArrangement.BottomResizable)); @@ -326,8 +326,8 @@ public class ArrangementTests (ITestOutputHelper output) [Fact] public void Overlapped_AllowsSubViewsToOverlap () { - var superView = new View - { + var superView = new View + { Arrangement = ViewArrangement.Overlapped, Width = 20, Height = 20 @@ -351,7 +351,7 @@ public class ArrangementTests (ITestOutputHelper output) public void LeftResizable_CanBeUsedForHorizontalSplitter () { var container = new View { Width = 80, Height = 25 }; - + var leftPane = new View { X = 0, @@ -359,7 +359,7 @@ public class ArrangementTests (ITestOutputHelper output) Width = 40, Height = Dim.Fill () }; - + var rightPane = new View { X = 40, @@ -369,9 +369,9 @@ public class ArrangementTests (ITestOutputHelper output) Arrangement = ViewArrangement.LeftResizable, BorderStyle = LineStyle.Single }; - + container.Add (leftPane, rightPane); - + Assert.True (rightPane.Arrangement.HasFlag (ViewArrangement.LeftResizable)); Assert.NotNull (rightPane.Border); } @@ -380,7 +380,7 @@ public class ArrangementTests (ITestOutputHelper output) public void TopResizable_CanBeUsedForVerticalSplitter () { var container = new View { Width = 80, Height = 25 }; - + var topPane = new View { X = 0, @@ -388,7 +388,7 @@ public class ArrangementTests (ITestOutputHelper output) Width = Dim.Fill (), Height = 10 }; - + var bottomPane = new View { X = 0, @@ -398,9 +398,9 @@ public class ArrangementTests (ITestOutputHelper output) Arrangement = ViewArrangement.TopResizable, BorderStyle = LineStyle.Single }; - + container.Add (topPane, bottomPane); - + Assert.True (bottomPane.Arrangement.HasFlag (ViewArrangement.TopResizable)); Assert.NotNull (bottomPane.Border); } @@ -412,11 +412,11 @@ public class ArrangementTests (ITestOutputHelper output) [Fact] public void View_WithoutBorderStyle_CanHaveArrangement () { - var view = new View - { + var view = new View + { Arrangement = ViewArrangement.Movable }; - + // Arrangement can be set even without a border style // Border object still exists but has no visible style Assert.Equal (ViewArrangement.Movable, view.Arrangement); @@ -427,11 +427,11 @@ public class ArrangementTests (ITestOutputHelper output) [Fact] public void View_WithNoBorderStyle_ResizableCanBeSet () { - var view = new View - { + var view = new View + { Arrangement = ViewArrangement.Resizable }; - + // Arrangement is set but has limited effect without a visible border style Assert.Equal (ViewArrangement.Resizable, view.Arrangement); Assert.NotNull (view.Border); @@ -448,8 +448,8 @@ public class ArrangementTests (ITestOutputHelper output) // This test verifies the documented behavior that TopResizable is ignored // when Movable is also set (line 569 in Border.Arrangment.cs) var superView = new View { Width = 80, Height = 25 }; - var view = new View - { + var view = new View + { Arrangement = ViewArrangement.TopResizable | ViewArrangement.Movable, BorderStyle = LineStyle.Single, X = 10, @@ -458,11 +458,11 @@ public class ArrangementTests (ITestOutputHelper output) Height = 10 }; superView.Add (view); - + // The view has both flags set Assert.True (view.Arrangement.HasFlag (ViewArrangement.TopResizable)); Assert.True (view.Arrangement.HasFlag (ViewArrangement.Movable)); - + // But Movable takes precedence in Border.DetermineArrangeModeFromClick // This is verified by the code at line 569 checking !Parent!.Arrangement.HasFlag(ViewArrangement.Movable) } @@ -471,8 +471,8 @@ public class ArrangementTests (ITestOutputHelper output) public void DetermineArrangeModeFromClick_TopResizableWorksWithoutMovable () { var superView = new View { Width = 80, Height = 25 }; - var view = new View - { + var view = new View + { Arrangement = ViewArrangement.TopResizable, BorderStyle = LineStyle.Single, X = 10, @@ -481,7 +481,7 @@ public class ArrangementTests (ITestOutputHelper output) Height = 10 }; superView.Add (view); - + // Only TopResizable is set Assert.True (view.Arrangement.HasFlag (ViewArrangement.TopResizable)); Assert.False (view.Arrangement.HasFlag (ViewArrangement.Movable)); @@ -491,20 +491,20 @@ public class ArrangementTests (ITestOutputHelper output) public void DetermineArrangeModeFromClick_AllCornerCombinationsSupported () { var superView = new View { Width = 80, Height = 25 }; - + // Test that all 4 corner combinations are recognized - var cornerCombinations = new[] + var cornerCombinations = new [] { ViewArrangement.BottomResizable | ViewArrangement.RightResizable, ViewArrangement.BottomResizable | ViewArrangement.LeftResizable, ViewArrangement.TopResizable | ViewArrangement.RightResizable, ViewArrangement.TopResizable | ViewArrangement.LeftResizable }; - + foreach (var arrangement in cornerCombinations) { - var view = new View - { + var view = new View + { Arrangement = arrangement, BorderStyle = LineStyle.Single, X = 10, @@ -513,10 +513,10 @@ public class ArrangementTests (ITestOutputHelper output) Height = 10 }; superView.Add (view); - + // Verify the flags are set correctly Assert.True (view.Arrangement == arrangement); - + superView.Remove (view); } } @@ -530,10 +530,10 @@ public class ArrangementTests (ITestOutputHelper output) { var view = new View { Arrangement = ViewArrangement.Fixed }; Assert.Equal (ViewArrangement.Fixed, view.Arrangement); - + view.Arrangement = ViewArrangement.Movable; Assert.Equal (ViewArrangement.Movable, view.Arrangement); - + view.Arrangement = ViewArrangement.Resizable; Assert.Equal (ViewArrangement.Resizable, view.Arrangement); } @@ -542,7 +542,7 @@ public class ArrangementTests (ITestOutputHelper output) public void View_Arrangement_CanAddFlags () { var view = new View { Arrangement = ViewArrangement.Movable }; - + view.Arrangement |= ViewArrangement.LeftResizable; Assert.True (view.Arrangement.HasFlag (ViewArrangement.Movable)); Assert.True (view.Arrangement.HasFlag (ViewArrangement.LeftResizable)); @@ -551,11 +551,11 @@ public class ArrangementTests (ITestOutputHelper output) [Fact] public void View_Arrangement_CanRemoveFlags () { - var view = new View - { - Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable + var view = new View + { + Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable }; - + view.Arrangement &= ~ViewArrangement.Movable; Assert.False (view.Arrangement.HasFlag (ViewArrangement.Movable)); Assert.True (view.Arrangement.HasFlag (ViewArrangement.Resizable)); @@ -568,15 +568,15 @@ public class ArrangementTests (ITestOutputHelper output) [Fact] public void SuperView_CanHaveMultipleArrangeableSubViews () { - var superView = new View - { + var superView = new View + { Arrangement = ViewArrangement.Overlapped, Width = 80, Height = 25 }; - - var movableView = new View - { + + var movableView = new View + { Arrangement = ViewArrangement.Movable, BorderStyle = LineStyle.Single, X = 0, @@ -584,9 +584,9 @@ public class ArrangementTests (ITestOutputHelper output) Width = 20, Height = 10 }; - - var resizableView = new View - { + + var resizableView = new View + { Arrangement = ViewArrangement.Resizable, BorderStyle = LineStyle.Single, X = 25, @@ -594,9 +594,9 @@ public class ArrangementTests (ITestOutputHelper output) Width = 20, Height = 10 }; - - var fixedView = new View - { + + var fixedView = new View + { Arrangement = ViewArrangement.Fixed, BorderStyle = LineStyle.Single, X = 50, @@ -604,9 +604,9 @@ public class ArrangementTests (ITestOutputHelper output) Width = 20, Height = 10 }; - + superView.Add (movableView, resizableView, fixedView); - + Assert.Equal (3, superView.SubViews.Count); Assert.Equal (ViewArrangement.Movable, movableView.Arrangement); Assert.Equal (ViewArrangement.Resizable, resizableView.Arrangement); @@ -618,9 +618,9 @@ public class ArrangementTests (ITestOutputHelper output) { var superView = new View { Arrangement = ViewArrangement.Fixed }; var subView = new View { Arrangement = ViewArrangement.Movable }; - + superView.Add (subView); - + // SubView arrangement is independent of SuperView arrangement Assert.Equal (ViewArrangement.Fixed, superView.Arrangement); Assert.Equal (ViewArrangement.Movable, subView.Arrangement); @@ -633,30 +633,30 @@ public class ArrangementTests (ITestOutputHelper output) [Fact] public void Border_WithDefaultThickness_SupportsArrangement () { - var view = new View - { + var view = new View + { Arrangement = ViewArrangement.Movable, BorderStyle = LineStyle.Single }; - + Assert.NotNull (view.Border); // Default thickness should be (1,1,1,1) for Single line style - Assert.True (view.Border.Thickness.Left > 0 || view.Border.Thickness.Right > 0 + Assert.True (view.Border.Thickness.Left > 0 || view.Border.Thickness.Right > 0 || view.Border.Thickness.Top > 0 || view.Border.Thickness.Bottom > 0); } [Fact] public void Border_WithCustomThickness_SupportsArrangement () { - var view = new View - { + var view = new View + { Arrangement = ViewArrangement.LeftResizable, BorderStyle = LineStyle.Single }; - + // Set custom thickness - only left border view.Border!.Thickness = new Thickness (2, 0, 0, 0); - + Assert.Equal (2, view.Border.Thickness.Left); Assert.Equal (0, view.Border.Thickness.Top); Assert.Equal (0, view.Border.Thickness.Right); @@ -696,7 +696,7 @@ public class ArrangementTests (ITestOutputHelper output) // View.Navigation.cs checks Arrangement.HasFlag(ViewArrangement.Overlapped) var overlappedView = new View { Arrangement = ViewArrangement.Overlapped }; var tiledView = new View { Arrangement = ViewArrangement.Fixed }; - + Assert.True (overlappedView.Arrangement.HasFlag (ViewArrangement.Overlapped)); Assert.False (tiledView.Arrangement.HasFlag (ViewArrangement.Overlapped)); } @@ -708,9 +708,9 @@ public class ArrangementTests (ITestOutputHelper output) var parent = new View { Arrangement = ViewArrangement.Overlapped }; var child1 = new View { X = 0, Y = 0, Width = 10, Height = 10 }; var child2 = new View { X = 5, Y = 5, Width = 10, Height = 10 }; - + parent.Add (child1, child2); - + Assert.True (parent.Arrangement.HasFlag (ViewArrangement.Overlapped)); Assert.Equal (2, parent.SubViews.Count); } @@ -719,209 +719,7 @@ public class ArrangementTests (ITestOutputHelper output) #region Mouse Interaction Tests - [Fact] - public void MouseGrabHandler_WorksWithMovableView_UsingNewMouseEvent () - { - // This test proves that MouseGrabHandler works correctly with concurrent unit tests - // using NewMouseEvent directly on views, without requiring Application.Init - - var superView = new View - { - Width = 80, - Height = 25 - }; - - var movableView = new View - { - Arrangement = ViewArrangement.Movable, - BorderStyle = LineStyle.Single, - X = 10, - Y = 10, - Width = 20, - Height = 10 - }; - - superView.Add (movableView); - - // Verify initial state - Assert.NotNull (movableView.Border); - Assert.Null (Application.Mouse.MouseGrabView); - - // Simulate mouse press on the border to start dragging - var pressEvent = new MouseEventArgs - { - Position = new (1, 0), // Top border area - Flags = MouseFlags.Button1Pressed - }; - - bool? result = movableView.Border.NewMouseEvent (pressEvent); - - // The border should have grabbed the mouse - Assert.True (result); - Assert.Equal (movableView.Border, Application.Mouse.MouseGrabView); - - // Simulate mouse drag - var dragEvent = new MouseEventArgs - { - Position = new (5, 2), - Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition - }; - - result = movableView.Border.NewMouseEvent (dragEvent); - Assert.True (result); - - // Mouse should still be grabbed - Assert.Equal (movableView.Border, Application.Mouse.MouseGrabView); - - // Simulate mouse release to end dragging - var releaseEvent = new MouseEventArgs - { - Position = new (5, 2), - Flags = MouseFlags.Button1Released - }; - - result = movableView.Border.NewMouseEvent (releaseEvent); - Assert.True (result); - - // Mouse should be released - Assert.Null (Application.Mouse.MouseGrabView); - } - - [Fact] - public void MouseGrabHandler_WorksWithResizableView_UsingNewMouseEvent () - { - // This test proves MouseGrabHandler works for resizing operations - - var superView = new View - { - Width = 80, - Height = 25 - }; - - var resizableView = new View - { - Arrangement = ViewArrangement.RightResizable, - BorderStyle = LineStyle.Single, - X = 10, - Y = 10, - Width = 20, - Height = 10 - }; - - superView.Add (resizableView); - - // Verify initial state - Assert.NotNull (resizableView.Border); - Assert.Null (Application.Mouse.MouseGrabView); - - // Calculate position on right border (border is at right edge) - // Border.Frame.X is relative to parent, so we use coordinates relative to the border - var pressEvent = new MouseEventArgs - { - Position = new (resizableView.Border.Frame.Width - 1, 5), // Right border area - Flags = MouseFlags.Button1Pressed - }; - - bool? result = resizableView.Border.NewMouseEvent (pressEvent); - - // The border should have grabbed the mouse for resizing - Assert.True (result); - Assert.Equal (resizableView.Border, Application.Mouse.MouseGrabView); - - // Simulate dragging to resize - var dragEvent = new MouseEventArgs - { - Position = new (resizableView.Border.Frame.Width + 3, 5), - Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition - }; - - result = resizableView.Border.NewMouseEvent (dragEvent); - Assert.True (result); - Assert.Equal (resizableView.Border, Application.Mouse.MouseGrabView); - - // Simulate mouse release - var releaseEvent = new MouseEventArgs - { - Position = new (resizableView.Border.Frame.Width + 3, 5), - Flags = MouseFlags.Button1Released - }; - - result = resizableView.Border.NewMouseEvent (releaseEvent); - Assert.True (result); - - // Mouse should be released - Assert.Null (Application.Mouse.MouseGrabView); - } - - [Fact] - public void MouseGrabHandler_ReleasesOnMultipleViews () - { - // This test verifies MouseGrabHandler properly releases when switching between views - - var superView = new View { Width = 80, Height = 25 }; - - var view1 = new View - { - Arrangement = ViewArrangement.Movable, - BorderStyle = LineStyle.Single, - X = 10, - Y = 10, - Width = 15, - Height = 8 - }; - - var view2 = new View - { - Arrangement = ViewArrangement.Movable, - BorderStyle = LineStyle.Single, - X = 30, - Y = 10, - Width = 15, - Height = 8 - }; - - superView.Add (view1, view2); - - // Grab mouse on first view - var pressEvent1 = new MouseEventArgs - { - Position = new (1, 0), - Flags = MouseFlags.Button1Pressed - }; - - view1.Border!.NewMouseEvent (pressEvent1); - Assert.Equal (view1.Border, Application.Mouse.MouseGrabView); - - // Release on first view - var releaseEvent1 = new MouseEventArgs - { - Position = new (1, 0), - Flags = MouseFlags.Button1Released - }; - - view1.Border.NewMouseEvent (releaseEvent1); - Assert.Null (Application.Mouse.MouseGrabView); - - // Grab mouse on second view - var pressEvent2 = new MouseEventArgs - { - Position = new (1, 0), - Flags = MouseFlags.Button1Pressed - }; - - view2.Border!.NewMouseEvent (pressEvent2); - Assert.Equal (view2.Border, Application.Mouse.MouseGrabView); - - // Release on second view - var releaseEvent2 = new MouseEventArgs - { - Position = new (1, 0), - Flags = MouseFlags.Button1Released - }; - - view2.Border.NewMouseEvent (releaseEvent2); - Assert.Null (Application.Mouse.MouseGrabView); - } + // Not parallelizable due to Application.Mouse dependency in MouseGrabHandler #endregion @@ -954,13 +752,13 @@ public class ArrangementTests (ITestOutputHelper output) // This test verifies that all the arrangement tests in this file // can run without Application.Init, making them parallelizable. // If this test passes, it confirms no Application dependencies leaked in. - - var view = new View - { + + var view = new View + { Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable, BorderStyle = LineStyle.Single }; - + Assert.NotNull (view); Assert.NotNull (view.Border); Assert.True (view.Arrangement.HasFlag (ViewArrangement.Movable)); @@ -974,9 +772,9 @@ public class ArrangementTests (ITestOutputHelper output) [Fact] public void ViewArrangement_CanCombineAllResizableDirections () { - ViewArrangement arrangement = ViewArrangement.TopResizable - | ViewArrangement.BottomResizable - | ViewArrangement.LeftResizable + ViewArrangement arrangement = ViewArrangement.TopResizable + | ViewArrangement.BottomResizable + | ViewArrangement.LeftResizable | ViewArrangement.RightResizable; Assert.True (arrangement.HasFlag (ViewArrangement.TopResizable)); @@ -1090,7 +888,7 @@ public class ArrangementTests (ITestOutputHelper output) public void View_MultipleSubviewsWithDifferentArrangements_EachIndependent () { var container = new View (); - + var fixedView = new View { Id = "fixed", Arrangement = ViewArrangement.Fixed }; var movableView = new View { Id = "movable", Arrangement = ViewArrangement.Movable }; var resizableView = new View { Id = "resizable", Arrangement = ViewArrangement.Resizable }; @@ -1112,7 +910,7 @@ public class ArrangementTests (ITestOutputHelper output) public void Overlapped_ViewCanBeMovedToFront () { var container = new View { Arrangement = ViewArrangement.Overlapped }; - + var view1 = new View { Id = "view1" }; var view2 = new View { Id = "view2" }; var view3 = new View { Id = "view3" }; @@ -1133,7 +931,7 @@ public class ArrangementTests (ITestOutputHelper output) public void Overlapped_ViewCanBeMovedToBack () { var container = new View { Arrangement = ViewArrangement.Overlapped }; - + var view1 = new View { Id = "view1" }; var view2 = new View { Id = "view2" }; var view3 = new View { Id = "view3" }; @@ -1142,12 +940,12 @@ public class ArrangementTests (ITestOutputHelper output) // Initial order: [view1, view2, view3] Assert.Equal ([view1, view2, view3], container.SubViews.ToArray ()); - + // Move view3 to end (top of Z-order) container.MoveSubViewToEnd (view3); Assert.Equal (view3, container.SubViews.ToArray () [^1]); Assert.Equal ([view1, view2, view3], container.SubViews.ToArray ()); - + // Now move view1 to end (making it on top, pushing view3 down) container.MoveSubViewToEnd (view1); Assert.Equal ([view2, view3, view1], container.SubViews.ToArray ()); @@ -1157,7 +955,7 @@ public class ArrangementTests (ITestOutputHelper output) public void Fixed_ViewAddOrderMattersForLayout () { var container = new View { Arrangement = ViewArrangement.Fixed }; - + var view1 = new View { Id = "view1", X = 0, Y = 0, Width = 10, Height = 5 }; var view2 = new View { Id = "view2", X = 5, Y = 2, Width = 10, Height = 5 }; diff --git a/Tests/UnitTestsParallelizable/View/Draw/NeedsDrawTests.cs b/Tests/UnitTestsParallelizable/View/Draw/NeedsDrawTests.cs index cf3b7a0c8..b8b3e7d43 100644 --- a/Tests/UnitTestsParallelizable/View/Draw/NeedsDrawTests.cs +++ b/Tests/UnitTestsParallelizable/View/Draw/NeedsDrawTests.cs @@ -1,8 +1,10 @@ #nullable enable +using UnitTests; + namespace UnitTests_Parallelizable.ViewTests; [Trait ("Category", "Output")] -public class NeedsDrawTests +public class NeedsDrawTests : FakeDriverBase { [Fact] public void NeedsDraw_False_If_Width_Height_Zero () @@ -18,7 +20,7 @@ public class NeedsDrawTests [Fact] public void NeedsDraw_True_Initially_If_Width_Height_Not_Zero () { - View superView = new () { Width = 1, Height = 1 }; + View superView = new () { Driver = CreateFakeDriver (), Width = 1, Height = 1 }; View view1 = new () { Width = 1, Height = 1 }; View view2 = new () { Width = 1, Height = 1 }; @@ -54,7 +56,7 @@ public class NeedsDrawTests var view = new View { Width = 2, Height = 2 }; Assert.True (view.NeedsDraw); - view = new() { Width = 2, Height = 2, BorderStyle = LineStyle.Single }; + view = new () { Width = 2, Height = 2, BorderStyle = LineStyle.Single }; Assert.True (view.NeedsDraw); } @@ -90,7 +92,7 @@ public class NeedsDrawTests view.EndInit (); Assert.True (view.NeedsDraw); - view = new() { Width = 2, Height = 2, BorderStyle = LineStyle.Single }; + view = new () { Width = 2, Height = 2, BorderStyle = LineStyle.Single }; view.BeginInit (); view.NeedsDraw = false; view.EndInit (); @@ -100,7 +102,7 @@ public class NeedsDrawTests [Fact] public void NeedsDraw_After_SetLayoutNeeded_And_Layout () { - var view = new View { Width = 2, Height = 2 }; + var view = new View { Driver = CreateFakeDriver (), Width = 2, Height = 2 }; Assert.True (view.NeedsDraw); Assert.False (view.NeedsLayout); @@ -120,7 +122,7 @@ public class NeedsDrawTests [Fact] public void NeedsDraw_False_After_SetRelativeLayout_Absolute_Dims () { - var view = new View { Width = 2, Height = 2 }; + var view = new View { Driver = CreateFakeDriver (), Width = 2, Height = 2 }; Assert.True (view.NeedsDraw); view.Draw (); @@ -224,7 +226,7 @@ public class NeedsDrawTests [Fact] public void NeedsDraw_False_After_Draw () { - var view = new View { Width = 2, Height = 2, BorderStyle = LineStyle.Single }; + var view = new View { Driver = CreateFakeDriver (), Width = 2, Height = 2, BorderStyle = LineStyle.Single }; Assert.True (view.NeedsDraw); view.BeginInit (); diff --git a/Tests/UnitTestsParallelizable/View/Draw/ViewClearViewportTests.cs b/Tests/UnitTestsParallelizable/View/Draw/ViewClearViewportTests.cs new file mode 100644 index 000000000..1ef96e203 --- /dev/null +++ b/Tests/UnitTestsParallelizable/View/Draw/ViewClearViewportTests.cs @@ -0,0 +1,371 @@ +using System.Text; +using UnitTests; +using Xunit.Abstractions; + +namespace UnitTests_Parallelizable.ViewTests; + +public class ViewClearViewportTests () : FakeDriverBase +{ + [Fact] + public void ClearViewport_FillsViewportArea () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 1, + Y = 1, + Width = 20, + Height = 20, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + // Clear the driver contents first + driver.FillRect (driver.Screen, new Rune ('X')); + + view.ClearViewport (); + + // The viewport area should be filled with spaces + Rectangle viewportScreen = view.ViewportToScreen (view.Viewport with { Location = new (0, 0) }); + + for (int y = viewportScreen.Y; y < viewportScreen.Y + viewportScreen.Height; y++) + { + for (int x = viewportScreen.X; x < viewportScreen.X + viewportScreen.Width; x++) + { + Assert.Equal (new Rune (' '), driver.Contents [y, x].Rune); + } + } + } + + [Fact] + public void ClearViewport_WithClearContentOnly_LimitsToVisibleContent () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 1, + Y = 1, + Width = 20, + Height = 20, + Driver = driver + }; + view.SetContentSize (new Size (100, 100)); // Content larger than viewport + view.ViewportSettings = ViewportSettingsFlags.ClearContentOnly; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + // Clear the driver contents first + driver.FillRect (driver.Screen, new Rune ('X')); + + view.ClearViewport (); + + // The visible content area should be cleared + Rectangle visibleContent = view.ViewportToScreen (new Rectangle (new (-view.Viewport.X, -view.Viewport.Y), view.GetContentSize ())); + Rectangle viewportScreen = view.ViewportToScreen (view.Viewport with { Location = new (0, 0) }); + Rectangle toClear = Rectangle.Intersect (viewportScreen, visibleContent); + + for (int y = toClear.Y; y < toClear.Y + toClear.Height; y++) + { + for (int x = toClear.X; x < toClear.X + toClear.Width; x++) + { + Assert.Equal (new Rune (' '), driver.Contents [y, x].Rune); + } + } + } + + [Fact] + public void ClearViewport_NullDriver_DoesNotThrow () + { + var view = new View + { + X = 1, + Y = 1, + Width = 20, + Height = 20 + }; + view.BeginInit (); + view.EndInit (); + var exception = Record.Exception (() => view.ClearViewport ()); + Assert.Null (exception); + } + + [Fact] + public void ClearViewport_SetsNeedsDraw () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + // Clear NeedsDraw first + view.Draw (); + Assert.False (view.NeedsDraw); + + view.ClearViewport (); + + Assert.True (view.NeedsDraw); + } + + [Fact] + public void ClearViewport_WithTransparentFlag_DoesNotClear () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 1, + Y = 1, + Width = 20, + Height = 20, + Driver = driver, + ViewportSettings = ViewportSettingsFlags.Transparent + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + // Fill driver with a character + driver.FillRect (driver.Screen, new Rune ('X')); + + view.Draw (); + + // The viewport area should still have 'X' (not cleared) + Rectangle viewportScreen = view.ViewportToScreen (view.Viewport with { Location = new (0, 0) }); + + for (int y = viewportScreen.Y; y < viewportScreen.Y + viewportScreen.Height; y++) + { + for (int x = viewportScreen.X; x < viewportScreen.X + viewportScreen.Width; x++) + { + Assert.Equal (new Rune ('X'), driver.Contents [y, x].Rune); + } + } + } + + [Fact] + public void ClearingViewport_Event_Raised () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + bool eventRaised = false; + Rectangle? receivedRect = null; + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.ClearingViewport += (s, e) => + { + eventRaised = true; + receivedRect = e.NewViewport; + }; + + view.Draw (); + + Assert.True (eventRaised); + Assert.NotNull (receivedRect); + Assert.Equal (view.Viewport, receivedRect); + } + + [Fact] + public void ClearedViewport_Event_Raised () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + bool eventRaised = false; + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.ClearedViewport += (s, e) => eventRaised = true; + + view.Draw (); + + Assert.True (eventRaised); + } + + [Fact] + public void OnClearingViewport_CanPreventClear () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + bool clearedCalled = false; + + var view = new TestView + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver, + PreventClear = true + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.ClearedViewport += (s, e) => clearedCalled = true; + + view.Draw (); + + Assert.False (clearedCalled); + } + + [Fact] + public void ClearViewport_EmptyViewport_DoesNotThrow () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 10, + Y = 10, + Width = 1, + Height = 1, + Driver = driver + }; + view.Border!.Thickness = new Thickness (1); + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + // With border of 1, viewport should be empty + Assert.True (view.Viewport.Width == 0 || view.Viewport.Height == 0); + + var exception = Record.Exception (() => view.ClearViewport ()); + + Assert.Null (exception); + } + + [Fact] + public void ClearViewport_WithScrolledViewport_ClearsCorrectArea () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 1, + Y = 1, + Width = 20, + Height = 20, + Driver = driver + }; + view.SetContentSize (new Size (100, 100)); + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + // Scroll the viewport + view.Viewport = view.Viewport with { X = 10, Y = 10 }; + + // Fill driver with a character + driver.FillRect (driver.Screen, new Rune ('X')); + + view.ClearViewport (); + + // The viewport area should be cleared (not the scrolled content area) + Rectangle viewportScreen = view.ViewportToScreen (view.Viewport with { Location = new (0, 0) }); + + for (int y = viewportScreen.Y; y < viewportScreen.Y + viewportScreen.Height; y++) + { + for (int x = viewportScreen.X; x < viewportScreen.X + viewportScreen.Width; x++) + { + Assert.Equal (new Rune (' '), driver.Contents [y, x].Rune); + } + } + } + + [Fact] + public void ClearViewport_WithClearContentOnly_AndScrolledViewport_ClearsOnlyVisibleContent () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver + }; + view.SetContentSize (new Size (15, 15)); // Content smaller than viewport + view.ViewportSettings = ViewportSettingsFlags.ClearContentOnly; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + // Scroll past the content + view.Viewport = view.Viewport with { X = 5, Y = 5 }; + + // Fill driver with a character + driver.FillRect (driver.Screen, new Rune ('X')); + + view.ClearViewport (); + + // Only the visible part of the content should be cleared + Rectangle visibleContent = view.ViewportToScreen (new Rectangle (new (-view.Viewport.X, -view.Viewport.Y), view.GetContentSize ())); + Rectangle viewportScreen = view.ViewportToScreen (view.Viewport with { Location = new (0, 0) }); + Rectangle toClear = Rectangle.Intersect (viewportScreen, visibleContent); + + if (toClear != Rectangle.Empty) + { + for (int y = toClear.Y; y < toClear.Y + toClear.Height; y++) + { + for (int x = toClear.X; x < toClear.X + toClear.Width; x++) + { + Assert.Equal (new Rune (' '), driver.Contents [y, x].Rune); + } + } + } + } + + private class TestView : View + { + public bool PreventClear { get; set; } + + protected override bool OnClearingViewport () + { + return PreventClear || base.OnClearingViewport (); + } + } +} diff --git a/Tests/UnitTestsParallelizable/View/Draw/ViewDrawTextAndLineCanvasTests.cs b/Tests/UnitTestsParallelizable/View/Draw/ViewDrawTextAndLineCanvasTests.cs new file mode 100644 index 000000000..5f2af2055 --- /dev/null +++ b/Tests/UnitTestsParallelizable/View/Draw/ViewDrawTextAndLineCanvasTests.cs @@ -0,0 +1,495 @@ +using System.Text; +using UnitTests; +using Xunit.Abstractions; + +namespace UnitTests_Parallelizable.ViewTests; + +public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase +{ + #region DrawText Tests + + [Fact] + public void DrawText_EmptyText_DoesNotThrow () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver, + Text = "" + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + var exception = Record.Exception (() => view.Draw ()); + + Assert.Null (exception); + } + + [Fact] + public void DrawText_NullText_DoesNotThrow () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver, + Text = null! + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + var exception = Record.Exception (() => view.Draw ()); + + Assert.Null (exception); + } + + [Fact] + public void DrawText_DrawsTextToDriver () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 1, + Y = 1, + Width = 20, + Height = 20, + Driver = driver, + Text = "Test" + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.Draw (); + + // Text should appear at the content location + Point screenPos = view.ContentToScreen (Point.Empty); + + Assert.Equal ('T', (char)driver.Contents [screenPos.Y, screenPos.X].Rune.Value); + Assert.Equal ('e', (char)driver.Contents [screenPos.Y, screenPos.X + 1].Rune.Value); + Assert.Equal ('s', (char)driver.Contents [screenPos.Y, screenPos.X + 2].Rune.Value); + Assert.Equal ('t', (char)driver.Contents [screenPos.Y, screenPos.X + 3].Rune.Value); + } + + [Fact] + public void DrawText_WithFocus_UsesFocusAttribute () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver, + Text = "Test", + CanFocus = true + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + view.SetFocus (); + + view.Draw (); + + // Text should use focus attribute + Point screenPos = view.ContentToScreen (Point.Empty); + Attribute expectedAttr = view.GetAttributeForRole (VisualRole.Focus); + + Assert.Equal (expectedAttr, driver.Contents [screenPos.Y, screenPos.X].Attribute); + } + + [Fact] + public void DrawText_WithoutFocus_UsesNormalAttribute () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver, + Text = "Test", + CanFocus = true + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.Draw (); + + // Text should use normal attribute + Point screenPos = view.ContentToScreen (Point.Empty); + Attribute expectedAttr = view.GetAttributeForRole (VisualRole.Normal); + + Assert.Equal (expectedAttr, driver.Contents [screenPos.Y, screenPos.X].Attribute); + } + + [Fact] + public void DrawText_SetsSubViewNeedsDraw () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver, + Text = "Test" + }; + var child = new View { X = 0, Y = 0, Width = 10, Height = 10 }; + view.Add (child); + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + // Clear SubViewNeedsDraw + view.Draw (); + Assert.False (view.SubViewNeedsDraw); + + // Call DrawText directly which should set SubViewNeedsDraw + view.DrawText (); + + // SubViews need to be redrawn since text was drawn over them + Assert.True (view.SubViewNeedsDraw); + } + + [Fact] + public void DrawingText_Event_Raised () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + bool eventRaised = false; + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver, + Text = "Test" + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.DrawingText += (s, e) => eventRaised = true; + + view.Draw (); + + Assert.True (eventRaised); + } + + [Fact] + public void DrewText_Event_Raised () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + bool eventRaised = false; + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver, + Text = "Test" + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.DrewText += (s, e) => eventRaised = true; + + view.Draw (); + + Assert.True (eventRaised); + } + + #endregion + + #region LineCanvas Tests + + [Fact] + public void LineCanvas_InitiallyEmpty () + { + var view = new View (); + + Assert.NotNull (view.LineCanvas); + Assert.Equal (Rectangle.Empty, view.LineCanvas.Bounds); + } + + [Fact] + public void RenderLineCanvas_DrawsLines () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + // Add a line to the canvas + Point screenPos = new Point (15, 15); + view.LineCanvas.AddLine (screenPos, 5, Orientation.Horizontal, LineStyle.Single); + + view.RenderLineCanvas (); + + // Verify the line was drawn (check for horizontal line character) + for (int i = 0; i < 5; i++) + { + Assert.NotEqual (new Rune (' '), driver.Contents [screenPos.Y, screenPos.X + i].Rune); + } + } + + [Fact] + public void RenderLineCanvas_ClearsAfterRendering () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + // Add a line to the canvas + view.LineCanvas.AddLine (new Point (15, 15), 5, Orientation.Horizontal, LineStyle.Single); + + Assert.NotEqual (Rectangle.Empty, view.LineCanvas.Bounds); + + view.RenderLineCanvas (); + + // LineCanvas should be cleared after rendering + Assert.Equal (Rectangle.Empty, view.LineCanvas.Bounds); + } + + [Fact] + public void RenderLineCanvas_WithSuperViewRendersLineCanvas_DoesNotClear () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver, + SuperViewRendersLineCanvas = true + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + // Add a line to the canvas + view.LineCanvas.AddLine (new Point (15, 15), 5, Orientation.Horizontal, LineStyle.Single); + + Rectangle boundsBefore = view.LineCanvas.Bounds; + + view.RenderLineCanvas (); + + // LineCanvas should NOT be cleared when SuperViewRendersLineCanvas is true + Assert.Equal (boundsBefore, view.LineCanvas.Bounds); + } + + [Fact] + public void SuperViewRendersLineCanvas_MergesWithParentCanvas () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var parent = new View + { + X = 10, + Y = 10, + Width = 50, + Height = 50, + Driver = driver + }; + var child = new View + { + X = 5, + Y = 5, + Width = 30, + Height = 30, + SuperViewRendersLineCanvas = true + }; + parent.Add (child); + parent.BeginInit (); + parent.EndInit (); + parent.LayoutSubViews (); + + // Add a line to child's canvas + child.LineCanvas.AddLine (new Point (20, 20), 5, Orientation.Horizontal, LineStyle.Single); + + Assert.NotEqual (Rectangle.Empty, child.LineCanvas.Bounds); + Assert.Equal (Rectangle.Empty, parent.LineCanvas.Bounds); + + parent.Draw (); + + // Child's canvas should have been merged into parent's + // and child's canvas should be cleared + Assert.Equal (Rectangle.Empty, child.LineCanvas.Bounds); + } + + [Fact] + public void OnRenderingLineCanvas_CanPreventRendering () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new TestView + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver, + PreventRenderLineCanvas = true + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + // Add a line to the canvas + Point screenPos = new Point (15, 15); + view.LineCanvas.AddLine (screenPos, 5, Orientation.Horizontal, LineStyle.Single); + + view.Draw (); + + // When OnRenderingLineCanvas returns true, RenderLineCanvas is not called + // So the LineCanvas should still have lines (not cleared) + // BUT because SuperViewRendersLineCanvas is false (default), the LineCanvas + // gets cleared during the draw cycle anyway. We need to check that the + // line was NOT actually rendered to the driver. + bool lineRendered = true; + for (int i = 0; i < 5; i++) + { + if (driver.Contents [screenPos.Y, screenPos.X + i].Rune.Value == ' ') + { + lineRendered = false; + break; + } + } + + Assert.False (lineRendered); + } + + #endregion + + #region SuperViewRendersLineCanvas Tests + + [Fact] + public void SuperViewRendersLineCanvas_DefaultFalse () + { + var view = new View (); + + Assert.False (view.SuperViewRendersLineCanvas); + } + + [Fact] + public void SuperViewRendersLineCanvas_CanBeSet () + { + var view = new View { SuperViewRendersLineCanvas = true }; + + Assert.True (view.SuperViewRendersLineCanvas); + } + + [Fact] + public void Draw_WithSuperViewRendersLineCanvas_SetsNeedsDraw () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var parent = new View + { + X = 10, + Y = 10, + Width = 50, + Height = 50, + Driver = driver + }; + var child = new View + { + X = 5, + Y = 5, + Width = 30, + Height = 30, + SuperViewRendersLineCanvas = true + }; + parent.Add (child); + parent.BeginInit (); + parent.EndInit (); + parent.LayoutSubViews (); + + // Draw once to clear NeedsDraw + parent.Draw (); + Assert.False (child.NeedsDraw); + + // Draw again - child with SuperViewRendersLineCanvas should be redrawn + parent.Draw (); + + // The child should have been set to NeedsDraw during DrawSubViews + // This is verified by the fact that it was drawn (we can't check NeedsDraw after Draw) + } + + #endregion + + #region Helper Test View + + private class TestView : View + { + public bool PreventRenderLineCanvas { get; set; } + + protected override bool OnRenderingLineCanvas () + { + return PreventRenderLineCanvas || base.OnRenderingLineCanvas (); + } + } + + #endregion +} diff --git a/Tests/UnitTestsParallelizable/View/Draw/ViewDrawingClippingTests.cs b/Tests/UnitTestsParallelizable/View/Draw/ViewDrawingClippingTests.cs new file mode 100644 index 000000000..ba5941efc --- /dev/null +++ b/Tests/UnitTestsParallelizable/View/Draw/ViewDrawingClippingTests.cs @@ -0,0 +1,754 @@ +#nullable enable +using UnitTests; +using Xunit.Abstractions; + +namespace UnitTests_Parallelizable.ViewTests; + +public class ViewDrawingClippingTests () : FakeDriverBase +{ + #region GetClip / SetClip Tests + + + [Fact] + public void GetClip_ReturnsDriverClip () + { + IDriver driver = CreateFakeDriver (80, 25); + var region = new Region (new Rectangle (10, 10, 20, 20)); + driver.Clip = region; + View view = new () { Driver = driver }; + + Region? result = view.GetClip (); + + Assert.NotNull (result); + Assert.Equal (region, result); + } + + [Fact] + public void SetClip_NullRegion_DoesNothing () + { + IDriver driver = CreateFakeDriver (80, 25); + var original = new Region (new Rectangle (5, 5, 10, 10)); + driver.Clip = original; + + View view = new () { Driver = driver }; + + view.SetClip (null); + + Assert.Equal (original, driver.Clip); + } + + [Fact] + public void SetClip_ValidRegion_SetsDriverClip () + { + IDriver driver = CreateFakeDriver (80, 25); + var region = new Region (new Rectangle (10, 10, 30, 30)); + View view = new () { Driver = driver }; + + view.SetClip (region); + + Assert.Equal (region, driver.Clip); + } + + #endregion + + #region SetClipToScreen Tests + + [Fact] + public void SetClipToScreen_ReturnsPreviousClip () + { + IDriver driver = CreateFakeDriver (80, 25); + var original = new Region (new Rectangle (5, 5, 10, 10)); + driver.Clip = original; + View view = new () { Driver = driver }; + + Region? previous = view.SetClipToScreen (); + + Assert.Equal (original, previous); + Assert.NotEqual (original, driver.Clip); + + Application.ResetState (true); + } + + [Fact] + public void SetClipToScreen_SetsClipToScreen () + { + IDriver driver = CreateFakeDriver (80, 25); + Application.Driver = driver; + View view = new () { Driver = driver }; + + view.SetClipToScreen (); + + Assert.NotNull (driver.Clip); + Assert.Equal (driver.Screen, driver.Clip.GetBounds ()); + + Application.ResetState (true); + } + + #endregion + + #region ExcludeFromClip Tests + + [Fact] + public void ExcludeFromClip_Rectangle_NullDriver_DoesNotThrow () + { + View view = new () { Driver = null }; + var exception = Record.Exception (() => view.ExcludeFromClip (new Rectangle (5, 5, 10, 10))); + Assert.Null (exception); + + Application.ResetState (true); + } + + [Fact] + public void ExcludeFromClip_Rectangle_ExcludesArea () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (new Rectangle (0, 0, 80, 25)); + Application.Driver = driver; + View view = new () { Driver = driver }; + + var toExclude = new Rectangle (10, 10, 20, 20); + view.ExcludeFromClip (toExclude); + + // Verify the region was excluded + Assert.NotNull (driver.Clip); + Assert.False (driver.Clip.Contains (15, 15)); + + Application.ResetState (true); + } + + [Fact] + public void ExcludeFromClip_Region_NullDriver_DoesNotThrow () + { + View view = new () { Driver = null }; + + var exception = Record.Exception (() => view.ExcludeFromClip (new Region (new Rectangle (5, 5, 10, 10)))); + Assert.Null (exception); + + Application.ResetState (true); + } + + [Fact] + public void ExcludeFromClip_Region_ExcludesArea () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (new Rectangle (0, 0, 80, 25)); + View view = new () { Driver = driver }; + + + var toExclude = new Region (new Rectangle (10, 10, 20, 20)); + view.ExcludeFromClip (toExclude); + + // Verify the region was excluded + Assert.NotNull (driver.Clip); + Assert.False (driver.Clip.Contains (15, 15)); + + Application.ResetState (true); + } + + #endregion + + #region AddFrameToClip Tests + + [Fact] + public void AddFrameToClip_NullDriver_ReturnsNull () + { + var view = new View { X = 0, Y = 0, Width = 10, Height = 10 }; + view.BeginInit (); + view.EndInit (); + + Region? result = view.AddFrameToClip (); + + Assert.Null (result); + } + + [Fact] + public void AddFrameToClip_IntersectsWithFrame () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 1, + Y = 1, + Width = 20, + Height = 20, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + Region? previous = view.AddFrameToClip (); + + Assert.NotNull (previous); + Assert.NotNull (driver.Clip); + + // The clip should now be the intersection of the screen and the view's frame + Rectangle expectedBounds = new Rectangle (1, 1, 20, 20); + Assert.Equal (expectedBounds, driver.Clip.GetBounds ()); + } + + #endregion + + #region AddViewportToClip Tests + + [Fact] + public void AddViewportToClip_NullDriver_ReturnsNull () + { + var view = new View { X = 0, Y = 0, Width = 10, Height = 10 }; + view.BeginInit (); + view.EndInit (); + + Region? result = view.AddViewportToClip (); + + Assert.Null (result); + } + + [Fact] + public void AddViewportToClip_IntersectsWithViewport () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 1, + Y = 1, + Width = 20, + Height = 20, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + Region? previous = view.AddViewportToClip (); + + Assert.NotNull (previous); + Assert.NotNull (driver.Clip); + + // The clip should be the viewport area + Rectangle viewportScreen = view.ViewportToScreen (new Rectangle (Point.Empty, view.Viewport.Size)); + Assert.Equal (viewportScreen, driver.Clip.GetBounds ()); + } + + [Fact] + public void AddViewportToClip_WithClipContentOnly_LimitsToVisibleContent () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 1, + Y = 1, + Width = 20, + Height = 20, + Driver = driver + }; + view.SetContentSize (new Size (100, 100)); + view.ViewportSettings = ViewportSettingsFlags.ClipContentOnly; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + Region? previous = view.AddViewportToClip (); + + Assert.NotNull (previous); + Assert.NotNull (driver.Clip); + + // The clip should be limited to visible content + Rectangle visibleContent = view.ViewportToScreen (new Rectangle (new (-view.Viewport.X, -view.Viewport.Y), view.GetContentSize ())); + Rectangle viewport = view.ViewportToScreen (new Rectangle (Point.Empty, view.Viewport.Size)); + Rectangle expected = Rectangle.Intersect (viewport, visibleContent); + + Assert.Equal (expected, driver.Clip.GetBounds ()); + } + + #endregion + + #region Clip Interaction Tests + + [Fact] + public void ClipRegions_StackCorrectly_WithNestedViews () + { + IDriver driver = CreateFakeDriver (100, 100); + driver.Clip = new Region (driver.Screen); + + var superView = new View + { + X = 1, + Y = 1, + Width = 50, + Height = 50, + Driver = driver + }; + superView.BeginInit (); + superView.EndInit (); + + var view = new View + { + X = 5, + Y = 5, + Width = 30, + Height = 30, + }; + superView.Add (view); + superView.LayoutSubViews (); + + // Set clip to superView's frame + Region? superViewClip = superView.AddFrameToClip (); + Rectangle superViewBounds = driver.Clip.GetBounds (); + + // Now set clip to view's frame + Region? viewClip = view.AddFrameToClip (); + Rectangle viewBounds = driver.Clip.GetBounds (); + + // Child clip should be within superView clip + Assert.True (superViewBounds.Contains (viewBounds.Location)); + + // Restore superView clip + view.SetClip (superViewClip); + // Assert.Equal (superViewBounds, driver.Clip.GetBounds ()); + } + + [Fact] + public void ClipRegions_RespectPreviousClip () + { + IDriver driver = CreateFakeDriver (80, 25); + var initialClip = new Region (new Rectangle (20, 20, 40, 40)); + driver.Clip = initialClip; + + var view = new View + { + X = 1, + Y = 1, + Width = 60, + Height = 60, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + Region? previous = view.AddFrameToClip (); + + // The new clip should be the intersection of the initial clip and the view's frame + Rectangle expected = Rectangle.Intersect ( + initialClip.GetBounds (), + view.FrameToScreen () + ); + + Assert.Equal (expected, driver.Clip.GetBounds ()); + + // Restore should give us back the original + view.SetClip (previous); + Assert.Equal (initialClip.GetBounds (), driver.Clip.GetBounds ()); + } + + #endregion + + #region Edge Cases + + [Fact] + public void AddFrameToClip_EmptyFrame_WorksCorrectly () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 1, + Y = 1, + Width = 0, + Height = 0, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + Region? previous = view.AddFrameToClip (); + + Assert.NotNull (previous); + Assert.NotNull (driver.Clip); + } + + [Fact] + public void AddViewportToClip_EmptyViewport_WorksCorrectly () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 1, + Y = 1, + Width = 1, // Minimal size to have adornments + Height = 1, + Driver = driver + }; + view.Border!.Thickness = new Thickness (1); + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + // With border thickness of 1, the viewport should be empty + Assert.True (view.Viewport.Size.Width == 0 || view.Viewport.Size.Height == 0); + + Region? previous = view.AddViewportToClip (); + + Assert.NotNull (previous); + } + + [Fact] + public void ClipRegions_OutOfBounds_HandledCorrectly () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 100, // Outside screen bounds + Y = 100, + Width = 20, + Height = 20, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + Region? previous = view.AddFrameToClip (); + + Assert.NotNull (previous); + // The clip should be empty since the view is outside the screen + Assert.True (driver.Clip.IsEmpty () || !driver.Clip.Contains (100, 100)); + } + + #endregion + + #region Drawing Tests + + [Fact] + public void Clip_Set_BeforeDraw_ClipsDrawing () + { + IDriver driver = CreateFakeDriver (80, 25); + var clip = new Region (new Rectangle (10, 10, 10, 10)); + driver.Clip = clip; + + var view = new View + { + X = 0, + Y = 0, + Width = 50, + Height = 50, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.Draw (); + + // Verify clip was used + Assert.NotNull (driver.Clip); + } + + [Fact] + public void Draw_UpdatesDriverClip () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 1, + Y = 1, + Width = 20, + Height = 20, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.Draw (); + + // Clip should be updated to exclude the drawn view + Assert.NotNull (driver.Clip); + // Assert.False (driver.Clip.Contains (15, 15)); // Point inside the view should be excluded + } + + [Fact] + public void Draw_WithSubViews_ClipsCorrectly () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var superView = new View + { + X = 1, + Y = 1, + Width = 50, + Height = 50, + Driver = driver + }; + var view = new View { X = 5, Y = 5, Width = 20, Height = 20 }; + superView.Add (view); + superView.BeginInit (); + superView.EndInit (); + superView.LayoutSubViews (); + + superView.Draw (); + + // Both superView and view should be excluded from clip + Assert.NotNull (driver.Clip); + // Assert.False (driver.Clip.Contains (15, 15)); // Point in superView should be excluded + } + + [Fact] + public void Draw_NonVisibleView_DoesNotUpdateClip () + { + IDriver driver = CreateFakeDriver (80, 25); + var originalClip = new Region (driver.Screen); + driver.Clip = originalClip.Clone (); + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Visible = false, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + + view.Draw (); + + // Clip should not be modified for invisible views + Assert.True (driver.Clip.Equals (originalClip)); + } + + [Fact] + public void ExcludeFromClip_ExcludesRegion () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + Application.Driver = driver; + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + var excludeRect = new Rectangle (15, 15, 10, 10); + view.ExcludeFromClip (excludeRect); + + Assert.NotNull (driver.Clip); + Assert.False (driver.Clip.Contains (20, 20)); // Point inside excluded rect should not be in clip + + Application.ResetState (true); + } + + [Fact] + public void ExcludeFromClip_WithNullClip_DoesNotThrow () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = null!; + Application.Driver = driver; + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver + }; + + var exception = Record.Exception (() => view.ExcludeFromClip (new Rectangle (15, 15, 10, 10))); + + Assert.Null (exception); + + Application.ResetState (true); + } + + #endregion + + #region Misc Tests + + [Fact] + public void SetClip_SetsDriverClip () + { + IDriver driver = CreateFakeDriver (80, 25); + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver + }; + + var newClip = new Region (new Rectangle (5, 5, 30, 30)); + view.SetClip (newClip); + + Assert.Equal (newClip, driver.Clip); + } + + [Fact (Skip = "See BUGBUG in SetClip")] + public void SetClip_WithNullClip_ClearsClip () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (new Rectangle (10, 10, 20, 20)); + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver + }; + + view.SetClip (null); + + Assert.Null (driver.Clip); + } + + [Fact] + public void Draw_Excludes_View_From_Clip () + { + IDriver driver = CreateFakeDriver (80, 25); + var originalClip = new Region (driver.Screen); + driver.Clip = originalClip.Clone (); + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + Region clipWithViewExcluded = originalClip.Clone (); + clipWithViewExcluded.Exclude (view.Frame); + + view.Draw (); + + Assert.Equal (clipWithViewExcluded, driver.Clip); + Assert.NotNull (driver.Clip); + } + + [Fact] + public void Draw_EmptyViewport_DoesNotCrash () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 10, + Y = 10, + Width = 1, + Height = 1, + Driver = driver + }; + view.Border!.Thickness = new Thickness (1); + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + // With border of 1, viewport should be empty (0x0 or negative) + var exception = Record.Exception (() => view.Draw ()); + + Assert.Null (exception); + } + + [Fact] + public void Draw_VeryLargeView_HandlesClippingCorrectly () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 0, + Y = 0, + Width = 1000, + Height = 1000, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + var exception = Record.Exception (() => view.Draw ()); + + Assert.Null (exception); + } + + [Fact] + public void Draw_NegativeCoordinates_HandlesClippingCorrectly () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = -10, + Y = -10, + Width = 50, + Height = 50, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + var exception = Record.Exception (() => view.Draw ()); + + Assert.Null (exception); + } + + [Fact] + public void Draw_OutOfScreenBounds_HandlesClippingCorrectly () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 100, + Y = 100, + Width = 50, + Height = 50, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + var exception = Record.Exception (() => view.Draw ()); + + Assert.Null (exception); + } + + #endregion +} diff --git a/Tests/UnitTestsParallelizable/View/Draw/ViewDrawingFlowTests.cs b/Tests/UnitTestsParallelizable/View/Draw/ViewDrawingFlowTests.cs new file mode 100644 index 000000000..58f2bbb18 --- /dev/null +++ b/Tests/UnitTestsParallelizable/View/Draw/ViewDrawingFlowTests.cs @@ -0,0 +1,701 @@ +#nullable enable +using UnitTests; +using Xunit.Abstractions; + +namespace UnitTests_Parallelizable.ViewTests; + +public class ViewDrawingFlowTests () : FakeDriverBase +{ + #region NeedsDraw Tests + + [Fact] + public void NeedsDraw_InitiallyFalse_WhenNotVisible () + { + var view = new View { Visible = false }; + view.BeginInit (); + view.EndInit (); + + Assert.False (view.NeedsDraw); + } + + [Fact] + public void NeedsDraw_TrueAfterSetNeedsDraw () + { + var view = new View { X = 0, Y = 0, Width = 10, Height = 10 }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.SetNeedsDraw (); + + Assert.True (view.NeedsDraw); + } + + [Fact] + public void NeedsDraw_ClearedAfterDraw () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 0, + Y = 0, + Width = 10, + Height = 10, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.SetNeedsDraw (); + Assert.True (view.NeedsDraw); + + view.Draw (); + + Assert.False (view.NeedsDraw); + } + + [Fact] + public void SetNeedsDraw_WithRectangle_UpdatesNeedsDrawRect () + { + var view = new View { Driver = CreateFakeDriver (), X = 0, Y = 0, Width = 20, Height = 20 }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + // After layout, view will have NeedsDrawRect set to the viewport + // We need to clear it first + view.Draw (); + Assert.False (view.NeedsDraw); + Assert.Equal (Rectangle.Empty, view.NeedsDrawRect); + + var rect = new Rectangle (5, 5, 10, 10); + view.SetNeedsDraw (rect); + + Assert.True (view.NeedsDraw); + Assert.Equal (rect, view.NeedsDrawRect); + } + + [Fact] + public void SetNeedsDraw_MultipleRectangles_Expands () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View { X = 0, Y = 0, Width = 30, Height = 30, Driver = driver }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + // After layout, clear NeedsDraw + view.Draw (); + Assert.False (view.NeedsDraw); + + view.SetNeedsDraw (new Rectangle (5, 5, 10, 10)); + view.SetNeedsDraw (new Rectangle (15, 15, 10, 10)); + + // Should expand to cover the entire viewport when we have overlapping regions + // The current implementation expands to viewport size + Rectangle expected = new Rectangle (0, 0, 30, 30); + Assert.Equal (expected, view.NeedsDrawRect); + } + + [Fact] + public void SetNeedsDraw_NotVisible_DoesNotSet () + { + var view = new View + { + X = 0, + Y = 0, + Width = 10, + Height = 10, + Visible = false + }; + view.BeginInit (); + view.EndInit (); + + view.SetNeedsDraw (); + + Assert.False (view.NeedsDraw); + } + + [Fact] + public void SetNeedsDraw_PropagatesToSuperView () + { + var parent = new View { X = 0, Y = 0, Width = 50, Height = 50 }; + var child = new View { X = 10, Y = 10, Width = 20, Height = 20 }; + parent.Add (child); + parent.BeginInit (); + parent.EndInit (); + parent.LayoutSubViews (); + + child.SetNeedsDraw (); + + Assert.True (child.NeedsDraw); + Assert.True (parent.SubViewNeedsDraw); + } + + [Fact] + public void SetNeedsDraw_SetsAdornmentsNeedsDraw () + { + var view = new View { X = 0, Y = 0, Width = 20, Height = 20 }; + view.Border!.Thickness = new Thickness (1); + view.Padding!.Thickness = new Thickness (1); + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.SetNeedsDraw (); + + Assert.True (view.Border!.NeedsDraw); + Assert.True (view.Padding!.NeedsDraw); + } + + #endregion + + #region SubViewNeedsDraw Tests + + [Fact] + public void SubViewNeedsDraw_InitiallyFalse () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View { Width = 10, Height = 10, Driver = driver }; + view.BeginInit (); + view.EndInit (); + view.Draw (); // Draw once to clear initial NeedsDraw + + Assert.False (view.SubViewNeedsDraw); + } + + [Fact] + public void SetSubViewNeedsDraw_PropagatesUp () + { + var grandparent = new View { X = 0, Y = 0, Width = 100, Height = 100 }; + var parent = new View { X = 10, Y = 10, Width = 50, Height = 50 }; + var child = new View { X = 5, Y = 5, Width = 20, Height = 20 }; + + grandparent.Add (parent); + parent.Add (child); + grandparent.BeginInit (); + grandparent.EndInit (); + grandparent.LayoutSubViews (); + + child.SetSubViewNeedsDraw (); + + Assert.True (child.SubViewNeedsDraw); + Assert.True (parent.SubViewNeedsDraw); + Assert.True (grandparent.SubViewNeedsDraw); + } + + [Fact] + public void SubViewNeedsDraw_ClearedAfterDraw () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var parent = new View + { + X = 0, + Y = 0, + Width = 50, + Height = 50, + Driver = driver + }; + var child = new View { X = 10, Y = 10, Width = 20, Height = 20 }; + parent.Add (child); + parent.BeginInit (); + parent.EndInit (); + parent.LayoutSubViews (); + + child.SetNeedsDraw (); + Assert.True (parent.SubViewNeedsDraw); + + parent.Draw (); + + Assert.False (parent.SubViewNeedsDraw); + Assert.False (child.SubViewNeedsDraw); + } + + #endregion + + #region Draw Visibility Tests + + [Fact] + public void Draw_NotVisible_DoesNotDraw () + { + IDriver driver = CreateFakeDriver (80, 25); + + var view = new View + { + X = 0, + Y = 0, + Width = 10, + Height = 10, + Visible = false, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + + view.SetNeedsDraw (); + view.Draw (); + + // NeedsDraw should still be false (view wasn't drawn) + Assert.False (view.NeedsDraw); + } + + [Fact] + public void Draw_SuperViewNotVisible_DoesNotDraw () + { + IDriver driver = CreateFakeDriver (80, 25); + + var parent = new View + { + X = 0, + Y = 0, + Width = 50, + Height = 50, + Visible = false, + Driver = driver + }; + var child = new View { X = 10, Y = 10, Width = 20, Height = 20 }; + parent.Add (child); + parent.BeginInit (); + parent.EndInit (); + + child.SetNeedsDraw (); + child.Draw (); + + // Child should not have been drawn + Assert.True (child.NeedsDraw); // Still needs draw + } + + [Fact] + public void Draw_Enabled_False_UsesDisabledAttribute () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + bool drawingTextCalled = false; + Attribute? usedAttribute = null; + + var view = new TestView + { + X = 0, + Y = 0, + Width = 10, + Height = 10, + Enabled = false, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.DrawingText += (s, e) => + { + drawingTextCalled = true; + usedAttribute = driver.CurrentAttribute; + }; + + view.Draw (); + + Assert.True (drawingTextCalled); + Assert.NotNull (usedAttribute); + // The disabled attribute should have been used + Assert.Equal (view.GetAttributeForRole (VisualRole.Disabled), usedAttribute); + } + + #endregion + + #region Draw Order Tests + + [Fact] + public void Draw_CallsMethodsInCorrectOrder () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var callOrder = new List (); + + var view = new TestView + { + X = 0, + Y = 0, + Width = 20, + Height = 20, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.DrawingAdornmentsCallback = () => callOrder.Add ("DrawingAdornments"); + view.ClearingViewportCallback = () => callOrder.Add ("ClearingViewport"); + view.DrawingSubViewsCallback = () => callOrder.Add ("DrawingSubViews"); + view.DrawingTextCallback = () => callOrder.Add ("DrawingText"); + view.DrawingContentCallback = () => callOrder.Add ("DrawingContent"); + view.RenderingLineCanvasCallback = () => callOrder.Add ("RenderingLineCanvas"); + view.DrawCompleteCallback = () => callOrder.Add ("DrawComplete"); + + view.Draw (); + + Assert.Equal ( + new [] { "DrawingAdornments", "ClearingViewport", "DrawingSubViews", "DrawingText", "DrawingContent", "RenderingLineCanvas", "DrawComplete" }, + callOrder + ); + } + + [Fact] + public void Draw_WithSubViews_DrawsInReverseOrder () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var drawOrder = new List (); + + var parent = new View + { + X = 0, + Y = 0, + Width = 50, + Height = 50, + Driver = driver + }; + + var child1 = new TestView { X = 0, Y = 0, Width = 10, Height = 10, Id = "Child1" }; + var child2 = new TestView { X = 0, Y = 10, Width = 10, Height = 10, Id = "Child2" }; + var child3 = new TestView { X = 0, Y = 20, Width = 10, Height = 10, Id = "Child3" }; + + parent.Add (child1); + parent.Add (child2); + parent.Add (child3); + + parent.BeginInit (); + parent.EndInit (); + parent.LayoutSubViews (); + + child1.DrawingContentCallback = () => drawOrder.Add ("Child1"); + child2.DrawingContentCallback = () => drawOrder.Add ("Child2"); + child3.DrawingContentCallback = () => drawOrder.Add ("Child3"); + + parent.Draw (); + + // SubViews are drawn in reverse order for clipping optimization + Assert.Equal (new [] { "Child3", "Child2", "Child1" }, drawOrder); + } + + #endregion + + #region DrawContext Tests + + [Fact] + public void Draw_WithContext_PassesContext () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + DrawContext? receivedContext = null; + + var view = new TestView + { + X = 0, + Y = 0, + Width = 20, + Height = 20, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.DrawingContentCallback = () => { }; + view.DrawingContent += (s, e) => + { + receivedContext = e.DrawContext; + }; + + var context = new DrawContext (); + view.Draw (context); + + Assert.NotNull (receivedContext); + Assert.Equal (context, receivedContext); + } + + [Fact] + public void Draw_WithoutContext_CreatesContext () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + DrawContext? receivedContext = null; + + var view = new TestView + { + X = 0, + Y = 0, + Width = 20, + Height = 20, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.DrawingContentCallback = () => { }; + view.DrawingContent += (s, e) => + { + receivedContext = e.DrawContext; + }; + + view.Draw (); + + Assert.NotNull (receivedContext); + } + + #endregion + + #region Event Tests + + [Fact] + public void ClearingViewport_CanCancel () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 0, + Y = 0, + Width = 20, + Height = 20, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + bool clearedCalled = false; + + view.ClearingViewport += (s, e) => e.Cancel = true; + view.ClearedViewport += (s, e) => clearedCalled = true; + + view.Draw (); + + Assert.False (clearedCalled); + } + + [Fact] + public void DrawingText_CanCancel () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var view = new View + { + X = 0, + Y = 0, + Width = 20, + Height = 20, + Driver = driver, + Text = "Test" + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + bool drewTextCalled = false; + + view.DrawingText += (s, e) => e.Cancel = true; + view.DrewText += (s, e) => drewTextCalled = true; + + view.Draw (); + + Assert.False (drewTextCalled); + } + + [Fact] + public void DrawingSubViews_CanCancel () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + var parent = new TestView + { + X = 0, + Y = 0, + Width = 50, + Height = 50, + Driver = driver + }; + var child = new TestView { X = 10, Y = 10, Width = 20, Height = 20 }; + parent.Add (child); + parent.BeginInit (); + parent.EndInit (); + parent.LayoutSubViews (); + + bool childDrawn = false; + child.DrawingContentCallback = () => childDrawn = true; + + parent.DrawingSubViews += (s, e) => e.Cancel = true; + + parent.Draw (); + + Assert.False (childDrawn); + } + + [Fact] + public void DrawComplete_AlwaysCalled () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + bool drawCompleteCalled = false; + + var view = new View + { + X = 0, + Y = 0, + Width = 20, + Height = 20, + Driver = driver + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.DrawComplete += (s, e) => drawCompleteCalled = true; + + view.Draw (); + + Assert.True (drawCompleteCalled); + } + + #endregion + + #region Transparent View Tests + + [Fact] + public void Draw_TransparentView_DoesNotClearViewport () + { + IDriver driver = CreateFakeDriver (80, 25); + driver.Clip = new Region (driver.Screen); + + bool clearedViewport = false; + + var view = new View + { + X = 0, + Y = 0, + Width = 20, + Height = 20, + Driver = driver, + ViewportSettings = ViewportSettingsFlags.Transparent + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.ClearedViewport += (s, e) => clearedViewport = true; + + view.Draw (); + + Assert.False (clearedViewport); + } + + [Fact] + public void Draw_TransparentView_ExcludesDrawnRegionFromClip () + { + IDriver driver = CreateFakeDriver (80, 25); + var initialClip = new Region (driver.Screen); + driver.Clip = initialClip; + Application.Driver = driver; + + var view = new View + { + X = 10, + Y = 10, + Width = 20, + Height = 20, + Driver = driver, + ViewportSettings = ViewportSettingsFlags.Transparent + }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + view.Draw (); + + // The drawn area should be excluded from the clip + Rectangle viewportScreen = view.ViewportToScreen (view.Viewport); + + // Points inside the view should be excluded + // Note: This test depends on the DrawContext tracking, which may not exclude if nothing was actually drawn + // We're verifying the mechanism exists, not that it necessarily excludes in this specific case + + Application.ResetState (true); + } + + #endregion + + #region Helper Test View + + private class TestView : View + { + public Action? DrawingAdornmentsCallback { get; set; } + public Action? ClearingViewportCallback { get; set; } + public Action? DrawingSubViewsCallback { get; set; } + public Action? DrawingTextCallback { get; set; } + public Action? DrawingContentCallback { get; set; } + public Action? RenderingLineCanvasCallback { get; set; } + public Action? DrawCompleteCallback { get; set; } + + protected override bool OnDrawingAdornments () + { + DrawingAdornmentsCallback?.Invoke (); + return base.OnDrawingAdornments (); + } + + protected override bool OnClearingViewport () + { + ClearingViewportCallback?.Invoke (); + return base.OnClearingViewport (); + } + + protected override bool OnDrawingSubViews (DrawContext? context) + { + DrawingSubViewsCallback?.Invoke (); + return base.OnDrawingSubViews (context); + } + + protected override bool OnDrawingText (DrawContext? context) + { + DrawingTextCallback?.Invoke (); + return base.OnDrawingText (context); + } + + protected override bool OnDrawingContent (DrawContext? context) + { + DrawingContentCallback?.Invoke (); + return base.OnDrawingContent (context); + } + + protected override bool OnRenderingLineCanvas () + { + RenderingLineCanvasCallback?.Invoke (); + return base.OnRenderingLineCanvas (); + } + + protected override void OnDrawComplete (DrawContext? context) + { + DrawCompleteCallback?.Invoke (); + base.OnDrawComplete (context); + } + } + + #endregion +} diff --git a/Tests/UnitTestsParallelizable/View/Layout/GetViewsUnderLocationTests.cs b/Tests/UnitTestsParallelizable/View/Layout/GetViewsUnderLocationTests.cs index 4bb0506c1..dd5cede33 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/GetViewsUnderLocationTests.cs +++ b/Tests/UnitTestsParallelizable/View/Layout/GetViewsUnderLocationTests.cs @@ -20,7 +20,7 @@ public class GetViewsUnderLocationTests var location = new Point (testX, testY); // Act - List viewsUnderMouse = View.GetViewsUnderLocation (location, ViewportSettingsFlags.None); + List viewsUnderMouse = view.GetViewsUnderLocation (location, ViewportSettingsFlags.None); // Assert Assert.Empty (viewsUnderMouse); @@ -42,7 +42,7 @@ public class GetViewsUnderLocationTests var location = new Point (testX, testY); // Act - List viewsUnderMouse = View.GetViewsUnderLocation (location, ViewportSettingsFlags.None); + List viewsUnderMouse = view.GetViewsUnderLocation (location, ViewportSettingsFlags.None); // Assert Assert.Empty (viewsUnderMouse); diff --git a/Tests/UnitTestsParallelizable/Views/AllViewsDrawTests.cs b/Tests/UnitTestsParallelizable/Views/AllViewsDrawTests.cs index 35ebdf600..1926538c8 100644 --- a/Tests/UnitTestsParallelizable/Views/AllViewsDrawTests.cs +++ b/Tests/UnitTestsParallelizable/Views/AllViewsDrawTests.cs @@ -21,6 +21,7 @@ public class AllViewsDrawTests (ITestOutputHelper output) : TestsAllViews return; } + view.Driver = driver; output.WriteLine ($"Testing {viewType}"); if (view is IDesignable designable) diff --git a/Tests/UnitTestsParallelizable/Views/LineTests.cs b/Tests/UnitTestsParallelizable/Views/LineTests.cs index 8fa44f4a1..a0aa17ca9 100644 --- a/Tests/UnitTestsParallelizable/Views/LineTests.cs +++ b/Tests/UnitTestsParallelizable/Views/LineTests.cs @@ -1,6 +1,8 @@ +using UnitTests; + namespace UnitTests_Parallelizable.ViewsTests; -public class LineTests +public class LineTests : FakeDriverBase { [Fact] public void Line_DefaultConstructor_Horizontal () @@ -87,7 +89,7 @@ public class LineTests [Fact] public void Line_DrawsCalled_Successfully () { - var app = new Window (); + var app = new Window () { Driver = CreateFakeDriver () }; var line = new Line { Y = 1, Width = 10 }; app.Add (line); @@ -103,7 +105,7 @@ public class LineTests [Fact] public void Line_WithBorder_DrawsSuccessfully () { - var app = new Window { Width = 20, Height = 10, BorderStyle = LineStyle.Single }; + var app = new Window { Driver = CreateFakeDriver (), Width = 20, Height = 10, BorderStyle = LineStyle.Single }; // Add a line that intersects with the window border var line = new Line { X = 5, Y = 0, Height = Dim.Fill (), Orientation = Orientation.Vertical }; @@ -121,7 +123,7 @@ public class LineTests [Fact] public void Line_MultipleIntersecting_DrawsSuccessfully () { - var app = new Window { Width = 30, Height = 15 }; + var app = new Window { Driver = CreateFakeDriver (), Width = 30, Height = 15 }; // Create intersecting lines var hLine = new Line { X = 5, Y = 5, Width = 15, Style = LineStyle.Single }; @@ -258,7 +260,7 @@ public class LineTests // Test: new Line { Height = 9, Orientation = Orientation.Vertical } // Expected: Width=1, Height=9 - line = new() { Height = 9, Orientation = Orientation.Vertical }; + line = new () { Height = 9, Orientation = Orientation.Vertical }; Assert.Equal (1, line.Width.GetAnchor (0)); Assert.Equal (9, line.Height.GetAnchor (0)); diff --git a/Tests/UnitTestsParallelizable/Views/ScrollSliderTests.cs b/Tests/UnitTestsParallelizable/Views/ScrollSliderTests.cs index 89d9649c1..2f510d9a4 100644 --- a/Tests/UnitTestsParallelizable/Views/ScrollSliderTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ScrollSliderTests.cs @@ -1,8 +1,9 @@ -using Xunit.Abstractions; +using UnitTests; +using Xunit.Abstractions; namespace UnitTests_Parallelizable.ViewsTests; -public class ScrollSliderTests +public class ScrollSliderTests (ITestOutputHelper output) : FakeDriverBase { [Fact] public void Constructor_Initializes_Correctly () @@ -675,4 +676,340 @@ public class ScrollSliderTests Assert.True (scrollSlider.Position <= 5); } + + + [Theory] + [SetupFakeApplication] + [InlineData ( + 3, + 10, + 1, + 0, + Orientation.Vertical, + @" +┌───┐ +│███│ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└───┘")] + [InlineData ( + 10, + 1, + 3, + 0, + Orientation.Horizontal, + @" +┌──────────┐ +│███ │ +└──────────┘")] + [InlineData ( + 3, + 10, + 3, + 0, + Orientation.Vertical, + @" +┌───┐ +│███│ +│███│ +│███│ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└───┘")] + + + + [InlineData ( + 3, + 10, + 5, + 0, + Orientation.Vertical, + @" +┌───┐ +│███│ +│███│ +│███│ +│███│ +│███│ +│ │ +│ │ +│ │ +│ │ +│ │ +└───┘")] + + [InlineData ( + 3, + 10, + 5, + 1, + Orientation.Vertical, + @" +┌───┐ +│ │ +│███│ +│███│ +│███│ +│███│ +│███│ +│ │ +│ │ +│ │ +│ │ +└───┘")] + [InlineData ( + 3, + 10, + 5, + 4, + Orientation.Vertical, + @" +┌───┐ +│ │ +│ │ +│ │ +│ │ +│███│ +│███│ +│███│ +│███│ +│███│ +│ │ +└───┘")] + [InlineData ( + 3, + 10, + 5, + 5, + Orientation.Vertical, + @" +┌───┐ +│ │ +│ │ +│ │ +│ │ +│ │ +│███│ +│███│ +│███│ +│███│ +│███│ +└───┘")] + [InlineData ( + 3, + 10, + 5, + 6, + Orientation.Vertical, + @" +┌───┐ +│ │ +│ │ +│ │ +│ │ +│ │ +│███│ +│███│ +│███│ +│███│ +│███│ +└───┘")] + + [InlineData ( + 3, + 10, + 10, + 0, + Orientation.Vertical, + @" +┌───┐ +│███│ +│███│ +│███│ +│███│ +│███│ +│███│ +│███│ +│███│ +│███│ +│███│ +└───┘")] + + [InlineData ( + 3, + 10, + 10, + 5, + Orientation.Vertical, + @" +┌───┐ +│███│ +│███│ +│███│ +│███│ +│███│ +│███│ +│███│ +│███│ +│███│ +│███│ +└───┘")] + [InlineData ( + 3, + 10, + 11, + 0, + Orientation.Vertical, + @" +┌───┐ +│███│ +│███│ +│███│ +│███│ +│███│ +│███│ +│███│ +│███│ +│███│ +│███│ +└───┘")] + + [InlineData ( + 10, + 3, + 5, + 0, + Orientation.Horizontal, + @" +┌──────────┐ +│█████ │ +│█████ │ +│█████ │ +└──────────┘")] + + [InlineData ( + 10, + 3, + 5, + 1, + Orientation.Horizontal, + @" +┌──────────┐ +│ █████ │ +│ █████ │ +│ █████ │ +└──────────┘")] + [InlineData ( + 10, + 3, + 5, + 4, + Orientation.Horizontal, + @" +┌──────────┐ +│ █████ │ +│ █████ │ +│ █████ │ +└──────────┘")] + [InlineData ( + 10, + 3, + 5, + 5, + Orientation.Horizontal, + @" +┌──────────┐ +│ █████│ +│ █████│ +│ █████│ +└──────────┘")] + [InlineData ( + 10, + 3, + 5, + 6, + Orientation.Horizontal, + @" +┌──────────┐ +│ █████│ +│ █████│ +│ █████│ +└──────────┘")] + + [InlineData ( + 10, + 3, + 10, + 0, + Orientation.Horizontal, + @" +┌──────────┐ +│██████████│ +│██████████│ +│██████████│ +└──────────┘")] + + [InlineData ( + 10, + 3, + 10, + 5, + Orientation.Horizontal, + @" +┌──────────┐ +│██████████│ +│██████████│ +│██████████│ +└──────────┘")] + [InlineData ( + 10, + 3, + 11, + 0, + Orientation.Horizontal, + @" +┌──────────┐ +│██████████│ +│██████████│ +│██████████│ +└──────────┘")] + public void Draws_Correctly (int superViewportWidth, int superViewportHeight, int sliderSize, int position, Orientation orientation, string expected) + { + IDriver driver = CreateFakeDriver (); + var super = new Window + { + Driver = driver, + Id = "super", + Width = superViewportWidth + 2, + Height = superViewportHeight + 2 + }; + + var scrollSlider = new ScrollSlider + { + Orientation = orientation, + Size = sliderSize, + //Position = position, + }; + Assert.Equal (sliderSize, scrollSlider.Size); + super.Add (scrollSlider); + scrollSlider.Position = position; + + super.Layout (); + super.Draw (); + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output, driver); + } } diff --git a/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs b/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs index 7866055ca..fc740c63a 100644 --- a/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs @@ -108,7 +108,7 @@ public class ShortcutTests // | C H K | Assert.Equal (expectedWidth, shortcut.Frame.Width); - shortcut = new() + shortcut = new () { HelpText = help, Title = command, @@ -118,7 +118,7 @@ public class ShortcutTests shortcut.Layout (); Assert.Equal (expectedWidth, shortcut.Frame.Width); - shortcut = new() + shortcut = new () { HelpText = help, Key = key, @@ -128,7 +128,7 @@ public class ShortcutTests shortcut.Layout (); Assert.Equal (expectedWidth, shortcut.Frame.Width); - shortcut = new() + shortcut = new () { Key = key, HelpText = help, @@ -314,13 +314,16 @@ public class ShortcutTests shortcut.Key = Key.A; Assert.True (shortcut.HotKeyBindings.TryGet (Key.A, out _)); + shortcut.App = Application.Create (); shortcut.BindKeyToApplication = true; + shortcut.BeginInit (); + shortcut.EndInit (); Assert.False (shortcut.HotKeyBindings.TryGet (Key.A, out _)); - Assert.True (Application.KeyBindings.TryGet (Key.A, out _)); + Assert.True (shortcut.App?.Keyboard.KeyBindings.TryGet (Key.A, out _)); shortcut.BindKeyToApplication = false; Assert.True (shortcut.HotKeyBindings.TryGet (Key.A, out _)); - Assert.False (Application.KeyBindings.TryGet (Key.A, out _)); + Assert.False (shortcut.App?.Keyboard.KeyBindings.TryGet (Key.A, out _)); } [Theory] diff --git a/codecov.yml b/codecov.yml index ea178f7c1..b71daa005 100644 --- a/codecov.yml +++ b/codecov.yml @@ -11,7 +11,7 @@ coverage: # Overall project coverage project: default: - target: 70% # Minimum target coverage + target: 75% # Minimum target coverage threshold: 1% # Allow 1% decrease without failing base: auto # Compare against base branch (v2_develop) if_ci_failed: error # Fail if CI fails diff --git a/docfx/docs/View.md b/docfx/docs/View.md index f65ab2de5..33ac685fd 100644 --- a/docfx/docs/View.md +++ b/docfx/docs/View.md @@ -32,6 +32,8 @@ See the [Views Overview](views.md) for a catalog of all built-in View subclasses - [View.SuperView](~/api/Terminal.Gui.ViewBase.View.yml#Terminal_Gui_ViewBase_View_SuperView) - The View's container (null if the View has no container) - [View.Id](~/api/Terminal.Gui.ViewBase.View.yml#Terminal_Gui_ViewBase_View_Id) - Unique identifier for the View (should be unique among siblings) - [View.Data](~/api/Terminal.Gui.ViewBase.View.yml#Terminal_Gui_ViewBase_View_Data) - Arbitrary data attached to the View +- [View.App](~/api/Terminal.Gui.ViewBase.View.yml#Terminal_Gui_ViewBase_View_App) - The application context this View belongs to +- [View.Driver](~/api/Terminal.Gui.ViewBase.View.yml#Terminal_Gui_ViewBase_View_Driver) - The driver used for rendering (derived from App). This is a shortcut to `App.Driver` for convenience. --- @@ -103,6 +105,8 @@ Views implement [ISupportInitializeNotification](https://docs.microsoft.com/en-u 3. **[EndInit](~/api/Terminal.Gui.ViewBase.View.yml#Terminal_Gui_ViewBase_View_EndInit)** - Signals initialization is complete; raises [View.Initialized](~/api/Terminal.Gui.ViewBase.View.yml) event 4. **[IsInitialized](~/api/Terminal.Gui.ViewBase.View.yml#Terminal_Gui_ViewBase_View_IsInitialized)** - Property indicating if initialization is complete +During initialization, [View.App](~/api/Terminal.Gui.ViewBase.View.yml#Terminal_Gui_ViewBase_View_App) is set to reference the application context, enabling views to access application services like the driver and current session. + ### Disposal Views are [IDisposable](https://docs.microsoft.com/en-us/dotnet/api/system.idisposable): @@ -678,6 +682,7 @@ view.ShadowStyle = ShadowStyle.Transparent; ## See Also +- **[Application Deep Dive](application.md)** - Instance-based application architecture - **[Views Overview](views.md)** - Complete list of all built-in Views - **[Layout Deep Dive](layout.md)** - Detailed layout system documentation - **[Drawing Deep Dive](drawing.md)** - Drawing system and color management diff --git a/docfx/docs/application.md b/docfx/docs/application.md new file mode 100644 index 000000000..b999b1fc0 --- /dev/null +++ b/docfx/docs/application.md @@ -0,0 +1,552 @@ +# Application Architecture + +Terminal.Gui v2 uses an instance-based application architecture that decouples views from the global application state, improving testability and enabling multiple application contexts. + +## View Hierarchy and Run Stack + +```mermaid +graph TB + subgraph ViewTree["View Hierarchy (SuperView/SubView)"] + direction TB + Top[Application.Current
Window] + Menu[MenuBar] + Status[StatusBar] + Content[Content View] + Button1[Button] + Button2[Button] + + Top --> Menu + Top --> Status + Top --> Content + Content --> Button1 + Content --> Button2 + end + + subgraph Stack["Application.SessionStack"] + direction TB + S1[Window
Currently Active] + S2[Previous Toplevel
Waiting] + S3[Base Toplevel
Waiting] + + S1 -.-> S2 -.-> S3 + end + + Top -.->|"same instance"| S1 + + style Top fill:#ccffcc,stroke:#339933,stroke-width:3px + style S1 fill:#ccffcc,stroke:#339933,stroke-width:3px +``` + +## Usage Example Flow + +```mermaid +sequenceDiagram + participant App as Application + participant Main as Main Window + participant Dialog as Dialog + + Note over App: Initially empty SessionStack + + App->>Main: Run(mainWindow) + activate Main + Note over App: SessionStack: [Main]
Current: Main + + Main->>Dialog: Run(dialog) + activate Dialog + Note over App: SessionStack: [Dialog, Main]
Current: Dialog + + Dialog->>App: RequestStop() + deactivate Dialog + Note over App: SessionStack: [Main]
Current: Main + + Main->>App: RequestStop() + deactivate Main + Note over App: SessionStack: []
Current: null +``` + +## Key Concepts + +### Instance-Based vs Static + +**Terminal.Gui v2** has transitioned from a static singleton pattern to an instance-based architecture: + +```csharp +// OLD (v1 / early v2 - now obsolete): +Application.Init(); +Application.Top.Add(myView); +Application.Run(); +Application.Shutdown(); + +// NEW (v2 instance-based): +var app = Application.Create (); +app.Init(); +var top = new Toplevel(); +top.Add(myView); +app.Run(top); +app.Shutdown(); +``` + +### View.App Property + +Every view now has an `App` property that references its application context: + +```csharp +public class View +{ + /// + /// Gets the application context for this view. + /// + public IApplication? App { get; internal set; } + + /// + /// Gets the application context, checking parent hierarchy if needed. + /// Override to customize application resolution. + /// + public virtual IApplication? GetApp() => App ?? SuperView?.GetApp(); +} +``` + +**Benefits:** +- Views can be tested without `Application.Init()` +- Multiple applications can coexist +- Clear ownership: views know their context +- Reduced global state dependencies + +### Accessing Application from Views + +**Recommended pattern:** + +```csharp +public class MyView : View +{ + public override void OnEnter(View view) + { + // Use View.App instead of static Application + App?.Current?.SetNeedsDraw(); + + // Access SessionStack + if (App?.SessionStack.Count > 0) + { + // Work with sessions + } + } +} +``` + +**Alternative - dependency injection:** + +```csharp +public class MyView : View +{ + private readonly IApplication _app; + + public MyView(IApplication app) + { + _app = app; + // Now completely decoupled from static Application + } + + public void DoWork() + { + _app.Current?.SetNeedsDraw(); + } +} +``` + +## IApplication Interface + +The `IApplication` interface defines the application contract: + +```csharp +public interface IApplication +{ + /// + /// Gets the currently running Toplevel (the "current session"). + /// Renamed from "Top" for clarity. + /// + Toplevel? Current { get; } + + /// + /// Gets the stack of running sessions. + /// Renamed from "TopLevels" to align with SessionToken terminology. + /// + ConcurrentStack SessionStack { get; } + + IDriver? Driver { get; } + IMainLoopCoordinator? MainLoop { get; } + + void Init(string? driverName = null); + void Shutdown(); + SessionToken? Begin(Toplevel toplevel); + void End(SessionToken sessionToken); + // ... other members +} +``` + +## Terminology Changes + +Terminal.Gui v2 modernized its terminology for clarity: + +### Application.Current (formerly "Top") + +The `Current` property represents the currently running Toplevel (the active session): + +```csharp +// Access the current session +Toplevel? current = app.Current; + +// From within a view +Toplevel? current = App?.Current; +``` + +**Why "Current" instead of "Top"?** +- Follows .NET patterns (`Thread.CurrentThread`, `HttpContext.Current`) +- Self-documenting: immediately clear it's the "current" active view +- Less confusing than "Top" which could mean "topmost in Z-order" + +### Application.SessionStack (formerly "TopLevels") + +The `SessionStack` property is the stack of running sessions: + +```csharp +// Access all running sessions +foreach (var toplevel in app.SessionStack) +{ + // Process each session +} + +// From within a view +int sessionCount = App?.SessionStack.Count ?? 0; +``` + +**Why "SessionStack" instead of "TopLevels"?** +- Describes both content (sessions) and structure (stack) +- Aligns with `SessionToken` terminology +- Follows .NET naming patterns (descriptive + collection type) + +## Migration from Static Application + +The static `Application` class now delegates to `ApplicationImpl.Instance` and is marked obsolete: + +```csharp +public static class Application +{ + [Obsolete("Use ApplicationImpl.Instance.Current or view.App?.Current")] + public static Toplevel? Current => Instance?.Current; + + [Obsolete("Use ApplicationImpl.Instance.SessionStack or view.App?.SessionStack")] + public static ConcurrentStack SessionStack => Instance?.SessionStack ?? new(); +} +``` + +### Migration Strategies + +**Strategy 1: Use View.App** + +```csharp +// OLD: +void MyMethod() +{ + Application.Current?.SetNeedsDraw(); +} + +// NEW: +void MyMethod(View view) +{ + view.App?.Current?.SetNeedsDraw(); +} +``` + +**Strategy 2: Pass IApplication** + +```csharp +// OLD: +void ProcessSessions() +{ + foreach (var toplevel in Application.SessionStack) + { + // Process + } +} + +// NEW: +void ProcessSessions(IApplication app) +{ + foreach (var toplevel in app.SessionStack) + { + // Process + } +} +``` + +**Strategy 3: Store IApplication Reference** + +```csharp +public class MyService +{ + private readonly IApplication _app; + + public MyService(IApplication app) + { + _app = app; + } + + public void DoWork() + { + _app.Current?.Title = "Processing..."; + } +} +``` + +## Session Management + +### Begin and End + +Applications manage sessions through `Begin()` and `End()`: + +```csharp +var app = Application.Create (); +app.Init(); + +var toplevel = new Toplevel(); + +// Begin a new session - pushes to SessionStack +SessionToken? token = app.Begin(toplevel); + +// Current now points to this toplevel +Debug.Assert(app.Current == toplevel); + +// End the session - pops from SessionStack +if (token != null) +{ + app.End(token); +} + +// Current restored to previous toplevel (if any) +``` + +### Nested Sessions + +Multiple sessions can run nested: + +```csharp +var app = Application.Create (); +app.Init(); + +// Session 1 +var main = new Toplevel { Title = "Main" }; +var token1 = app.Begin(main); +// app.Current == main, SessionStack.Count == 1 + +// Session 2 (nested) +var dialog = new Dialog { Title = "Dialog" }; +var token2 = app.Begin(dialog); +// app.Current == dialog, SessionStack.Count == 2 + +// End dialog +app.End(token2); +// app.Current == main, SessionStack.Count == 1 + +// End main +app.End(token1); +// app.Current == null, SessionStack.Count == 0 +``` + +## View.Driver Property + +Similar to `View.App`, views now have a `Driver` property: + +```csharp +public class View +{ + /// + /// Gets the driver for this view. + /// + public IDriver? Driver => GetDriver(); + + /// + /// Gets the driver, checking application context if needed. + /// Override to customize driver resolution. + /// + public virtual IDriver? GetDriver() => App?.Driver; +} +``` + +**Usage:** + +```csharp +public override void OnDrawContent(Rectangle viewport) +{ + // Use view's driver instead of Application.Driver + Driver?.Move(0, 0); + Driver?.AddStr("Hello"); +} +``` + +## Testing with the New Architecture + +The instance-based architecture dramatically improves testability: + +### Testing Views in Isolation + +```csharp +[Fact] +public void MyView_DisplaysCorrectly() +{ + // Create mock application + var mockApp = new Mock(); + mockApp.Setup(a => a.Current).Returns(new Toplevel()); + + // Create view with mock app + var view = new MyView { App = mockApp.Object }; + + // Test without Application.Init()! + view.SetNeedsDraw(); + Assert.True(view.NeedsDraw); + + // No Application.Shutdown() needed! +} +``` + +### Testing with Real ApplicationImpl + +```csharp +[Fact] +public void MyView_WorksWithRealApplication() +{ + var app = Application.Create (); + try + { + app.Init(new FakeDriver()); + + var view = new MyView(); + var top = new Toplevel(); + top.Add(view); + + app.Begin(top); + + // View.App automatically set + Assert.NotNull(view.App); + Assert.Same(app, view.App); + + // Test view behavior + view.DoSomething(); + + } + finally + { + app.Shutdown(); + } +} +``` + +## Best Practices + +### DO: Use View.App + +```csharp +✅ GOOD: +public void Refresh() +{ + App?.Current?.SetNeedsDraw(); +} +``` + +### DON'T: Use Static Application + +```csharp +❌ AVOID: +public void Refresh() +{ + Application.Current?.SetNeedsDraw(); // Obsolete! +} +``` + +### DO: Pass IApplication as Dependency + +```csharp +✅ GOOD: +public class Service +{ + public Service(IApplication app) { } +} +``` + +### DON'T: Assume Application.Instance Exists + +```csharp +❌ AVOID: +public class Service +{ + public void DoWork() + { + var app = Application.Instance; // Might be null! + } +} +``` + +### DO: Override GetApp() for Custom Resolution + +```csharp +✅ GOOD: +public class SpecialView : View +{ + private IApplication? _customApp; + + public override IApplication? GetApp() + { + return _customApp ?? base.GetApp(); + } +} +``` + +## Advanced Scenarios + +### Multiple Applications + +The instance-based architecture enables multiple applications: + +```csharp +// Application 1 +var app1 = Application.Create (); +app1.Init(new WindowsDriver()); +var top1 = new Toplevel { Title = "App 1" }; +// ... configure top1 + +// Application 2 (different driver!) +var app2 = Application.Create (); +app2.Init(new CursesDriver()); +var top2 = new Toplevel { Title = "App 2" }; +// ... configure top2 + +// Views in top1 use app1 +// Views in top2 use app2 +``` + +### Application-Agnostic Views + +Create views that work with any application: + +```csharp +public class UniversalView : View +{ + public void ShowMessage(string message) + { + // Works regardless of which application context + var app = GetApp(); + if (app != null) + { + var msg = new MessageBox(message); + app.Begin(msg); + } + } +} +``` + +## See Also + +- [Navigation](navigation.md) - Navigation with the instance-based architecture +- [Keyboard](keyboard.md) - Keyboard handling through View.App +- [Mouse](mouse.md) - Mouse handling through View.App +- [Drivers](drivers.md) - Driver access through View.Driver +- [Multitasking](multitasking.md) - Session management with SessionStack diff --git a/docfx/docs/config.md b/docfx/docs/config.md index 48a993033..4a549d5ce 100644 --- a/docfx/docs/config.md +++ b/docfx/docs/config.md @@ -459,7 +459,7 @@ ThemeManager.ThemeChanged += (sender, e) => { // Theme has changed // Refresh all views to use new theme - Application.Top?.SetNeedsDraw(); + Application.Current?.SetNeedsDraw(); }; ``` diff --git a/docfx/docs/drawing.md b/docfx/docs/drawing.md index 598ec7a38..a74edf432 100644 --- a/docfx/docs/drawing.md +++ b/docfx/docs/drawing.md @@ -8,7 +8,7 @@ Terminal.Gui provides a set of APIs for formatting text, line drawing, and chara # View Drawing API -Terminal.Gui apps draw using the @Terminal.Gui.ViewBase.View.Move(System.Int32,System.Int32) and @Terminal.Gui.ViewBase.View.AddRune(System.Text.Rune) APIs. Move selects the column and row of the cell and AddRune places the specified glyph in that cell using the @Terminal.Gui.Drawing.Attribute that was most recently set via @Terminal.Gui.ViewBase.View.SetAttribute(Terminal.Gui.Drawing.Attribute). The @Terminal.Gui.Drivers.ConsoleDriver caches all changed Cells and efficiently outputs them to the terminal each iteration of the Application. In other words, Terminal.Gui uses deferred rendering. +Terminal.Gui apps draw using the @Terminal.Gui.ViewBase.View.Move(System.Int32,System.Int32) and @Terminal.Gui.ViewBase.View.AddRune(System.Text.Rune) APIs. Move selects the column and row of the cell and AddRune places the specified glyph in that cell using the @Terminal.Gui.Drawing.Attribute that was most recently set via @Terminal.Gui.ViewBase.View.SetAttribute(Terminal.Gui.Drawing.Attribute). The driver caches all changed Cells and efficiently outputs them to the terminal each iteration of the Application. In other words, Terminal.Gui uses deferred rendering. ## Coordinate System for Drawing @@ -26,7 +26,7 @@ See [Layout](layout.md) for more details of the Terminal.Gui coordinate system. 1) Adding the text to a @Terminal.Gui.Text.TextFormatter object. 2) Setting formatting options, such as @Terminal.Gui.Text.TextFormatter.Alignment. -3) Calling @Terminal.Gui.Text.TextFormatter.Draw(System.Drawing.Rectangle,Terminal.Gui.Drawing.Attribute,Terminal.Gui.Drawing.Attribute,System.Drawing.Rectangle,Terminal.Gui.Drivers.IConsoleDriver). +3) Calling @Terminal.Gui.Text.TextFormatter.Draw(Terminal.Gui.Drivers.IDriver, System.Drawing.Rectangle,Terminal.Gui.Drawing.Attribute,Terminal.Gui.Drawing.Attribute,System.Drawing.Rectangle). ## Line drawing @@ -62,19 +62,17 @@ If a View need to redraw because something changed within it's Content Area it c ## Clipping -Clipping enables better performance and features like transparent margins by ensuring regions of the terminal that need to be drawn actually get drawn by the @Terminal.Gui.Drivers.ConsoleDriver. Terminal.Gui supports non-rectangular clip regions with @Terminal.Gui.Drawing.Region. @Terminal.Gui.Drivers.ConsoleDriver.Clip is the application managed clip region and is managed by @Terminal.Gui.App.Application. Developers cannot change this directly, but can use @Terminal.Gui.ViewBase.View.SetClipToScreen, @Terminal.Gui.ViewBase.View.SetClip(Terminal.Gui.Drawing.Region), @Terminal.Gui.ViewBase.View.SetClipToFrame, etc... +Clipping enables better performance and features like transparent margins by ensuring regions of the terminal that need to be drawn actually get drawn by the driver. Terminal.Gui supports non-rectangular clip regions with @Terminal.Gui.Drawing.Region. The driver.Clip is the application managed clip region and is managed by @Terminal.Gui.App.Application. Developers cannot change this directly, but can use @Terminal.Gui.ViewBase.View.SetClipToScreen, @Terminal.Gui.ViewBase.View.SetClip(Terminal.Gui.Drawing.Region), @Terminal.Gui.ViewBase.View.SetClipToFrame, etc... ## Cell The @Terminal.Gui.Drawing.Cell class represents a single cell on the screen. It contains a character and an attribute. The character is of type `Rune` and the attribute is of type @Terminal.Gui.Drawing.Attribute. -`Cell` is not exposed directly to the developer. Instead, the @Terminal.Gui.Drivers.ConsoleDriver classes manage the `Cell` array that represents the screen. +`Cell` is not exposed directly to the developer. Instead, the driver classes manage the `Cell` array that represents the screen. To draw a `Cell` to the screen, use @Terminal.Gui.ViewBase.View.Move(System.Int32,System.Int32) to specify the row and column coordinates and then use the @Terminal.Gui.ViewBase.View.AddRune(System.Int32,System.Int32,System.Text.Rune) method to draw a single glyph. -// ... existing code ... - ## Attribute The @Terminal.Gui.Drawing.Attribute class represents the formatting attributes of a `Cell`. It exposes properties for the foreground and background colors as well as the text style. The foreground and background colors are of type @Terminal.Gui.Drawing.Color. Bold, underline, and other formatting attributes are supported via the @Terminal.Gui.Drawing.Attribute.Style property. @@ -100,8 +98,6 @@ SetAttributeForRole (VisualRole.Focus); AddStr ("Red on Black Underlined."); ``` -// ... existing code ... - ## VisualRole Represents the semantic visual role of a visual element rendered by a View (e.g., Normal text, Focused item, Active selection). @@ -141,4 +137,30 @@ See [View Deep Dive](View.md) for details. ## Diagnostics -The @Terminal.Gui.ViewBase.ViewDiagnosticFlags.DrawIndicator flag can be set on @Terminal.Gui.ViewBase.View.Diagnostics to cause an animated glyph to appear in the `Border` of each View. The glyph will animate each time that View's `Draw` method is called where either @Terminal.Gui.ViewBase.View.NeedsDraw or @Terminal.Gui.ViewBase.View.SubViewNeedsDraw is set. \ No newline at end of file +The @Terminal.Gui.ViewBase.ViewDiagnosticFlags.DrawIndicator flag can be set on @Terminal.Gui.ViewBase.View.Diagnostics to cause an animated glyph to appear in the `Border` of each View. The glyph will animate each time that View's `Draw` method is called where either @Terminal.Gui.ViewBase.View.NeedsDraw or @Terminal.Gui.ViewBase.View.SubViewNeedsDraw is set. + +## Accessing Application Drawing Context + +Views can access application-level drawing functionality through `View.App`: + +```csharp +public class CustomView : View +{ + protected override bool OnDrawingContent() + { + // Access driver capabilities through View.App + if (App?.Driver?.SupportsTrueColor == true) + { + // Use true color features + SetAttribute(new Attribute(Color.FromRgb(255, 0, 0), Color.FromRgb(0, 0, 255))); + } + else + { + // Fallback to 16-color mode + SetAttributeForRole(VisualRole.Normal); + } + + AddStr("Custom drawing with application context"); + return true; + } +} \ No newline at end of file diff --git a/docfx/docs/drivers.md b/docfx/docs/drivers.md index e8322ebf2..a7f523347 100644 --- a/docfx/docs/drivers.md +++ b/docfx/docs/drivers.md @@ -34,7 +34,7 @@ Application.Init(); // Method 2: Pass driver name to Init Application.Init(driverName: "unix"); -// Method 3: Pass a custom IConsoleDriver instance +// Method 3: Pass a custom IDriver instance var customDriver = new MyCustomDriver(); Application.Init(driver: customDriver); ``` @@ -56,7 +56,7 @@ The v2 driver architecture uses the **Component Factory** pattern to create plat Each driver is composed of specialized components, each with a single responsibility: -#### IConsoleInput<T> +#### IInput<T> Reads raw console input events from the terminal. The generic type `T` represents the platform-specific input type: - `ConsoleKeyInfo` for DotNetDriver and FakeDriver - `WindowsConsole.InputRecord` for WindowsDriver @@ -64,7 +64,7 @@ Reads raw console input events from the terminal. The generic type `T` represent Runs on a dedicated input thread to avoid blocking the UI. -#### IConsoleOutput +#### IOutput Renders the output buffer to the terminal. Handles: - Writing text and ANSI escape sequences - Setting cursor position @@ -88,8 +88,8 @@ Manages the screen buffer and drawing operations: #### IWindowSizeMonitor Detects terminal size changes and raises `SizeChanged` events when the terminal is resized. -#### ConsoleDriverFacade<T> -A unified facade that implements `IConsoleDriver` and coordinates all the components. This is what gets assigned to `Application.Driver`. +#### DriverFacade<T> +A unified facade that implements `IDriver` and coordinates all the components. This is what gets assigned to `Application.Driver`. ### Threading Model @@ -105,22 +105,22 @@ The driver architecture employs a **multi-threaded design** for optimal responsi ├──────────────────┬───────────────────┐ │ │ │ ┌────────▼────────┐ ┌──────▼─────────┐ ┌──────▼──────────┐ - │ Input Thread │ │ Main UI Thread│ │ ConsoleDriver │ + │ Input Thread │ │ Main UI Thread│ │ Driver │ │ │ │ │ │ Facade │ - │ IConsoleInput │ │ ApplicationMain│ │ │ + │ IInput │ │ ApplicationMain│ │ │ │ reads console │ │ Loop processes │ │ Coordinates all │ │ input async │ │ events, layout,│ │ components │ │ into queue │ │ and rendering │ │ │ └─────────────────┘ └────────────────┘ └─────────────────┘ ``` -- **Input Thread**: Started by `MainLoopCoordinator`, runs `IConsoleInput.Run()` which continuously reads console input and queues it into a thread-safe `ConcurrentQueue`. +- **Input Thread**: Started by `MainLoopCoordinator`, runs `IInput.Run()` which continuously reads console input and queues it into a thread-safe `ConcurrentQueue`. - **Main UI Thread**: Runs `ApplicationMainLoop.Iteration()` which: 1. Processes input from the queue via `IInputProcessor` 2. Executes timeout callbacks 3. Checks for UI changes (layout/drawing) - 4. Renders updates via `IConsoleOutput` + 4. Renders updates via `IOutput` This separation ensures that input is never lost and the UI remains responsive during intensive operations. @@ -131,25 +131,25 @@ When you call `Application.Init()`: 1. **ApplicationImpl.Init()** is invoked 2. Creates a `MainLoopCoordinator` with the appropriate `ComponentFactory` 3. **MainLoopCoordinator.StartAsync()** begins: - - Starts the input thread which creates `IConsoleInput` - - Initializes the main UI loop which creates `IConsoleOutput` - - Creates `ConsoleDriverFacade` and assigns to `Application.Driver` + - Starts the input thread which creates `IInput` + - Initializes the main UI loop which creates `IOutput` + - Creates `DriverFacade` and assigns to `IApplication.Driver` - Waits for both threads to be ready 4. Returns control to the application ### Shutdown Flow -When `Application.Shutdown()` is called: +When `IApplication.Shutdown()` is called: 1. Cancellation token is triggered 2. Input thread exits its read loop -3. `IConsoleOutput` is disposed +3. `IOutput` is disposed 4. Main thread waits for input thread to complete 5. All resources are cleaned up ## Component Interfaces -### IConsoleDriver +### IDriver The main driver interface that the framework uses internally. Provides: @@ -167,16 +167,6 @@ The main driver interface that the framework uses internally. Provides: - Use @Terminal.Gui.ViewBase.View.AddRune and @Terminal.Gui.ViewBase.View.AddStr for drawing - ViewBase infrastructure classes (in `Terminal.Gui/ViewBase/`) can access Driver when needed for framework implementation -### IConsoleDriverFacade - -Extended interface for v2 drivers that exposes the internal components: - -- `IInputProcessor InputProcessor` -- `IOutputBuffer OutputBuffer` -- `IWindowSizeMonitor WindowSizeMonitor` - -This interface allows advanced scenarios and testing. - ## Platform-Specific Details ### DotNetDriver (NetComponentFactory) @@ -219,79 +209,13 @@ This ensures Terminal.Gui applications can be debugged directly in Visual Studio - Uses `FakeConsole` for all operations - Allows injection of predefined input - Captures output for verification -- Always used when `Application._forceFakeConsole` is true - -## Example: Checking Driver Capabilities - -```csharp -Application.Init(); - -// The driver is internal - access through Application properties -// Check screen dimensions -var screenWidth = Application.Screen.Width; -var screenHeight = Application.Screen.Height; - -// Check if 24-bit color is supported -bool supportsTrueColor = Application.Driver?.SupportsTrueColor ?? false; - -// Access advanced components (for framework/infrastructure code only) -if (Application.Driver is IConsoleDriverFacade facade) -{ - // Access individual components for advanced scenarios - IInputProcessor inputProcessor = facade.InputProcessor; - IOutputBuffer outputBuffer = facade.OutputBuffer; - IWindowSizeMonitor sizeMonitor = facade.WindowSizeMonitor; - - // Use components for advanced scenarios - sizeMonitor.SizeChanging += (s, e) => - { - Console.WriteLine($"Terminal resized to {e.Size}"); - }; -} -``` +- Always used when `IApplication.ForceDriver` is `fake` **Important:** View subclasses should not access `Application.Driver`. Use the View APIs instead: - `View.Move(col, row)` for positioning - `View.AddRune()` and `View.AddStr()` for drawing -- `Application.Screen` for screen dimensions +- `View.App.Screen` for screen dimensions -## Custom Drivers - -To create a custom driver, implement `IComponentFactory`: - -```csharp -public class MyComponentFactory : ComponentFactory -{ - public override IConsoleInput CreateInput() - { - return new MyConsoleInput(); - } - - public override IConsoleOutput CreateOutput() - { - return new MyConsoleOutput(); - } - - public override IInputProcessor CreateInputProcessor( - ConcurrentQueue inputBuffer) - { - return new MyInputProcessor(inputBuffer); - } -} -``` - -Then use it: - -```csharp -ApplicationImpl.ChangeComponentFactory(new MyComponentFactory()); -Application.Init(); -``` - -## Legacy Drivers - -Terminal.Gui v1 drivers that implement `IConsoleDriver` but not `IConsoleDriverFacade` are still supported through a legacy compatibility layer. However, they do not benefit from the v2 architecture improvements (multi-threading, component separation, etc.). - -**Note**: The legacy `MainLoop` infrastructure (including the `MainLoop` class, `IMainLoopDriver` interface, and `FakeMainLoop`) has been removed in favor of the modern architecture. All drivers now use the `MainLoopCoordinator` and `ApplicationMainLoop` system exclusively. ## See Also diff --git a/docfx/docs/keyboard.md b/docfx/docs/keyboard.md index 14071f69c..6a08190fa 100644 --- a/docfx/docs/keyboard.md +++ b/docfx/docs/keyboard.md @@ -91,7 +91,7 @@ The Command can be invoked even if the `View` that defines them is not focused o ### **Key Events** -Keyboard events are retrieved from [Console Drivers](drivers.md) each iteration of the [Application](~/api/Terminal.Gui.App.Application.yml) [Main Loop](multitasking.md). The console driver raises the @Terminal.Gui.Drivers.ConsoleDriver.KeyDown and @Terminal.Gui.Drivers.ConsoleDriver.KeyUp events which invoke @Terminal.Gui.App.Application.RaiseKeyDownEvent* and @Terminal.Gui.App.Application.RaiseKeyUpEvent(Terminal.Gui.Input.Key) respectively. +Keyboard events are retrieved from [Drivers](drivers.md) each iteration of the [Application](~/api/Terminal.Gui.App.Application.yml) [Main Loop](multitasking.md). The driver raises the @Terminal.Gui.Drivers.IDriver.KeyDown and @Terminal.Gui.Drivers.IDriver.KeyUp events which invoke @Terminal.Gui.App.Application.RaiseKeyDownEvent* and @Terminal.Gui.App.Application.RaiseKeyUpEvent(Terminal.Gui.Input.Key) respectively. > [!NOTE] > Not all drivers/platforms support sensing distinct KeyUp events. These drivers will simulate KeyUp events by raising KeyUp after KeyDown. @@ -113,12 +113,12 @@ To define application key handling logic for an entire application in cases wher ## **Key Down/Up Events** -*Terminal.Gui* supports key up/down events with @Terminal.Gui.ViewBase.View.OnKeyDown* and @Terminal.Gui.ViewBase.View.OnKeyUp*, but not all [Console Drivers](drivers.md) do. To receive these key down and key up events, you must use a driver that supports them (e.g. `WindowsDriver`). +*Terminal.Gui* supports key up/down events with @Terminal.Gui.ViewBase.View.OnKeyDown* and @Terminal.Gui.ViewBase.View.OnKeyUp*, but not all [Drivers](drivers.md) do. To receive these key down and key up events, you must use a driver that supports them (e.g. `WindowsDriver`). # General input model -- Key Down and Up events are generated by `ConsoleDriver`. -- `Application` subscribes to `ConsoleDriver.Down/Up` events and forwards them to the most-focused `TopLevel` view using `View.NewKeyDownEvent` and `View.NewKeyUpEvent`. +- Key Down and Up events are generated by the driver. +- `IApplication` implementations subscribe to driver KeyDown/Up events and forwards them to the most-focused `TopLevel` view using `View.NewKeyDownEvent` and `View.NewKeyUpEvent`. - The base (`View`) implementation of `NewKeyDownEvent` follows a pattern of "Before", "During", and "After" processing: - **Before** - If `Enabled == false` that view should *never* see keyboard (or mouse input). @@ -134,25 +134,19 @@ To define application key handling logic for an entire application in cases wher - Subclasses of `View` can (rarely) override `OnKeyDown` (or subscribe to `KeyDown`) to see keys before they are processed - Subclasses of `View` can (often) override `OnKeyDownNotHandled` to do key processing for keys that were not previously handled. `TextField` and `TextView` are examples. -## ConsoleDriver - -* No concept of `Command` or `KeyBindings` -* Use the low-level `KeyCode` enum. -* Exposes non-cancelable `KeyDown/Up` events. The `OnKey/Down/Up` methods are public and can be used to simulate keyboard input (in addition to SendKeys). - ## Application * Implements support for `KeyBindingScope.Application`. * Keyboard functionality is now encapsulated in the @Terminal.Gui.App.IKeyboard interface, accessed via @Terminal.Gui.App.Application.Keyboard. * @Terminal.Gui.App.Application.Keyboard provides access to @Terminal.Gui.Input.KeyBindings, key binding configuration (QuitKey, ArrangeKey, navigation keys), and keyboard event handling. -* For backward compatibility, @Terminal.Gui.App.Application still exposes static properties/methods that delegate to @Terminal.Gui.App.Application.Keyboard (e.g., `Application.KeyBindings`, `Application.RaiseKeyDownEvent`, `Application.QuitKey`). +* For backward compatibility, @Terminal.Gui.App.Application still exposes static properties/methods that delegate to @Terminal.Gui.App.Application.Keyboard (e.g., `IApplication.KeyBindings`, `IApplication.RaiseKeyDownEvent`, `IApplication.QuitKey`). * Exposes cancelable `KeyDown/Up` events (via `Handled = true`). The `RaiseKeyDownEvent` and `RaiseKeyUpEvent` methods are public and can be used to simulate keyboard input. * The @Terminal.Gui.App.IKeyboard interface enables testability with isolated keyboard instances that don't depend on static Application state. ## View * Implements support for `KeyBindings` and `HotKeyBindings`. -* Exposes cancelable non-virtual methods for a new key event: `NewKeyDownEvent` and `NewKeyUpEvent`. These methods are called by `Application` can be called to simulate keyboard input. +* Exposes cancelable non-virtual methods for a new key event: `NewKeyDownEvent` and `NewKeyUpEvent`. These methods are called by `IApplication` can be called to simulate keyboard input. * Exposes cancelable virtual methods for a new key event: `OnKeyDown` and `OnKeyUp`. These methods are called by `NewKeyDownEvent` and `NewKeyUpEvent` and can be overridden to handle keyboard input. ## IKeyboard Architecture @@ -175,9 +169,9 @@ The @Terminal.Gui.App.IKeyboard interface provides a decoupled, testable archite ```csharp // Modern approach - using IKeyboard -Application.Keyboard.KeyBindings.Add(Key.F1, Command.HotKey); -Application.Keyboard.RaiseKeyDownEvent(Key.Enter); -Application.Keyboard.QuitKey = Key.Q.WithCtrl; +App.Keyboard.KeyBindings.Add(Key.F1, Command.HotKey); +App.Keyboard.RaiseKeyDownEvent(Key.Enter); +App.Keyboard.QuitKey = Key.Q.WithCtrl; // Legacy approach - still works (delegates to Application.Keyboard) Application.KeyBindings.Add(Key.F1, Command.HotKey); @@ -202,6 +196,24 @@ Assert.Equal(Key.Q.WithCtrl, keyboard1.QuitKey); Assert.Equal(Key.X.WithCtrl, keyboard2.QuitKey); ``` +**Accessing application context from views:** + +```csharp +public class MyView : View +{ + protected override bool OnKeyDown(Key key) + { + // Use View.App instead of static Application + if (key == Key.F1) + { + App?.Keyboard?.KeyBindings.Add(Key.F2, Command.Accept); + return true; + } + return base.OnKeyDown(key); + } +} +``` + ### Architecture Benefits - **Parallel Testing**: Multiple test methods can create and use separate @Terminal.Gui.App.IKeyboard instances simultaneously without state interference. @@ -218,4 +230,10 @@ The @Terminal.Gui.App.Keyboard class implements @Terminal.Gui.App.IKeyboard and - **Events**: KeyDown, KeyUp events for application-level keyboard monitoring - **Command Implementations**: Handlers for Application-scoped commands (Quit, Suspend, Navigation, Refresh, Arrange) -The @Terminal.Gui.App.ApplicationImpl class creates and manages the @Terminal.Gui.App.IKeyboard instance, setting its `Application` property to `this` to provide the necessary @Terminal.Gui.App.IApplication reference. \ No newline at end of file +The @Terminal.Gui.App.ApplicationImpl class creates and manages the @Terminal.Gui.App.IKeyboard instance, setting its `IApplication` property to `this` to provide the necessary @Terminal.Gui.App.IApplication reference. + +## Driver + +* No concept of `Command` or `KeyBindings` +* Use the low-level `KeyCode` enum. +* Exposes non-cancelable `KeyDown/Up` events. The `OnKey/Down/Up` methods are public and can be used to simulate keyboard input (in addition to SendKeys) \ No newline at end of file diff --git a/docfx/docs/migratingfromv1.md b/docfx/docs/migratingfromv1.md index fbf121c2f..8a459e0a0 100644 --- a/docfx/docs/migratingfromv1.md +++ b/docfx/docs/migratingfromv1.md @@ -85,12 +85,12 @@ When measuring the screen space taken up by a `string` you can use the extension In v1, @Terminal.Gui.View was derived from `Responder` which supported `IDisposable`. In v2, `Responder` has been removed and @Terminal.Gui.View is the base-class supporting `IDisposable`. -In v1, @Terminal.Gui./Terminal.Gui.Application.Init) automatically created a toplevel view and set [Application.Top](~/api/Terminal.Gui.Application.Top. In v2, @Terminal.Gui.App.Application.Init no longer automatically creates a toplevel or sets @Terminal.Gui.App.Application.Top; app developers must explicitly create the toplevel view and pass it to @Terminal.Gui.App.Application.Run (or use `Application.Run`). Developers are responsible for calling `Dispose` on any toplevel they create before exiting. +In v1, @Terminal.Gui./Terminal.Gui.Application.Init) automatically created a toplevel view and set [Application.Current](~/api/Terminal.Gui.Application.Current. In v2, @Terminal.Gui.App.Application.Init no longer automatically creates a toplevel or sets @Terminal.Gui.App.Application.Current; app developers must explicitly create the toplevel view and pass it to @Terminal.Gui.App.Application.Run (or use `Application.Run`). Developers are responsible for calling `Dispose` on any toplevel they create before exiting. ### How to Fix * Replace `Responder` with @Terminal.Gui.View -* Update any code that assumes `Application.Init` automatically created a toplevel view and set `Application.Top`. +* Update any code that assumes `Application.Init` automatically created a toplevel view and set `Application.Current`. * Update any code that assumes `Application.Init` automatically disposed of the toplevel view when the application exited. ## @Terminal.Gui.Pos and @Terminal.Gui.Dim types now adhere to standard C# idioms @@ -523,6 +523,6 @@ new ( * To simplify programming, any `View` added as a SubView another `View` will have it's lifecycle owned by the Superview; when a `View` is disposed, it will call `Dispose` on all the items in the `SubViews` property. Note this behavior is the same as it was in v1, just clarified. -* In v1, `Application.End` called `Dispose ()` on @Terminal.Gui.App.Application.Top (via `Runstate.Toplevel`). This was incorrect as it meant that after `Application.Run` returned, `Application.Top` had been disposed, and any code that wanted to interrogate the results of `Run` by accessing `Application.Top` only worked by accident. This is because GC had not actually happened; if it had the application would have crashed. In v2 `Application.End` does NOT call `Dispose`, and it is the caller to `Application.Run` who is responsible for disposing the `Toplevel` that was either passed to `Application.Run (View)` or created by `Application.Run ()`. +* In v1, `Application.End` called `Dispose ()` on @Terminal.Gui.App.Application.Current (via `Runstate.Toplevel`). This was incorrect as it meant that after `Application.Run` returned, `Application.Current` had been disposed, and any code that wanted to interrogate the results of `Run` by accessing `Application.Current` only worked by accident. This is because GC had not actually happened; if it had the application would have crashed. In v2 `Application.End` does NOT call `Dispose`, and it is the caller to `Application.Run` who is responsible for disposing the `Toplevel` that was either passed to `Application.Run (View)` or created by `Application.Run ()`. * Any code that creates a `Toplevel`, either by using `top = new()` or by calling either `top = Application.Run ()` or `top = ApplicationRun()` must call `top.Dispose` when complete. The exception to this is if `top` is passed to `myView.Add(top)` making it a subview of `myView`. This is because the semantics of `Add` are that the `myView` takes over responsibility for the subviews lifetimes. Of course, if someone calls `myView.Remove(top)` to remove said subview, they then re-take responsbility for `top`'s lifetime and they must call `top.Dispose`. diff --git a/docfx/docs/mouse.md b/docfx/docs/mouse.md index 552a6c585..56e8dff85 100644 --- a/docfx/docs/mouse.md +++ b/docfx/docs/mouse.md @@ -66,14 +66,14 @@ Here are some common mouse binding patterns used throughout Terminal.Gui: At the core of *Terminal.Gui*'s mouse API is the @Terminal.Gui.Input.MouseEventArgs class. The @Terminal.Gui.Input.MouseEventArgs class provides a platform-independent abstraction for common mouse events. Every mouse event can be fully described in a @Terminal.Gui.Input.MouseEventArgs instance, and most of the mouse-related APIs are simply helper functions for decoding a @Terminal.Gui.Input.MouseEventArgs. -When the user does something with the mouse, the `ConsoleDriver` maps the platform-specific mouse event into a `MouseEventArgs` and calls `Application.RaiseMouseEvent`. Then, `Application.RaiseMouseEvent` determines which `View` the event should go to. The `View.OnMouseEvent` method can be overridden or the `View.MouseEvent` event can be subscribed to, to handle the low-level mouse event. If the low-level event is not handled by a view, `Application` will then call the appropriate high-level helper APIs. For example, if the user double-clicks the mouse, `View.OnMouseClick` will be called/`View.MouseClick` will be raised with the event arguments indicating which mouse button was double-clicked. +When the user does something with the mouse, the driver maps the platform-specific mouse event into a `MouseEventArgs` and calls `IApplication.Mouse.RaiseMouseEvent`. Then, `IApplication.Mouse.RaiseMouseEvent` determines which `View` the event should go to. The `View.OnMouseEvent` method can be overridden or the `View.MouseEvent` event can be subscribed to, to handle the low-level mouse event. If the low-level event is not handled by a view, `IApplication` will then call the appropriate high-level helper APIs. For example, if the user double-clicks the mouse, `View.OnMouseClick` will be called/`View.MouseClick` will be raised with the event arguments indicating which mouse button was double-clicked. ### Mouse Event Processing Flow Mouse events are processed through the following workflow using the [Cancellable Work Pattern](cancellable-work-pattern.md): -1. **Driver Level**: The ConsoleDriver captures platform-specific mouse events and converts them to `MouseEventArgs` -2. **Application Level**: `Application.RaiseMouseEvent` determines the target view and routes the event +1. **Driver Level**: The driver captures platform-specific mouse events and converts them to `MouseEventArgs` +2. **Application Level**: `IApplication.Mouse.RaiseMouseEvent` determines the target view and routes the event 3. **View Level**: The target view processes the event through: - `OnMouseEvent` (virtual method that can be overridden) - `MouseEvent` event (for event subscribers) @@ -157,8 +157,8 @@ view.MouseStateChanged += (sender, e) => The @Terminal.Gui.App.Application.MouseEvent event can be used if an application wishes to receive all mouse events before they are processed by individual views: -```cs -Application.MouseEvent += (sender, e) => +```csharp +App.Mouse.MouseEvent += (sender, e) => { // Handle application-wide mouse events if (e.Flags.HasFlag(MouseFlags.Button3Clicked)) @@ -169,6 +169,24 @@ Application.MouseEvent += (sender, e) => }; ``` +For view-specific mouse handling that needs access to application context, use `View.App`: + +```csharp +public class MyView : View +{ + protected override bool OnMouseEvent(MouseEventArgs mouseEvent) + { + if (mouseEvent.Flags.HasFlag(MouseFlags.Button3Clicked)) + { + // Access application mouse functionality through View.App + App?.MouseEvent?.Invoke(this, mouseEvent); + return true; + } + return base.OnMouseEvent(mouseEvent); + } +} +``` + ## Mouse Enter/Leave Events The @Terminal.Gui.ViewBase.View.MouseEnter and @Terminal.Gui.ViewBase.View.MouseLeave events enable a View to take action when the mouse enters or exits the view boundary. Internally, this is used to enable @Terminal.Gui.ViewBase.View.Highlight functionality: @@ -218,3 +236,4 @@ The `MouseEventArgs` provides both coordinate systems: + diff --git a/docfx/docs/navigation.md b/docfx/docs/navigation.md index c83f72d50..7fde06795 100644 --- a/docfx/docs/navigation.md +++ b/docfx/docs/navigation.md @@ -374,7 +374,7 @@ In v1 `View` had `MostFocused` property that traversed up the view-hierarchy ret var focused = Application.Navigation.GetFocused(); // This replaces the v1 pattern: -// var focused = Application.Top.MostFocused; +// var focused = Application.Current.MostFocused; ``` ## How Does `View.Add/Remove` Work? diff --git a/docfx/docs/toc.yml b/docfx/docs/toc.yml index 66c9b5cbb..8acb4573f 100644 --- a/docfx/docs/toc.yml +++ b/docfx/docs/toc.yml @@ -26,6 +26,8 @@ href: events.md - name: Lexicon & Taxonomy href: lexicon.md +- name: Terminology Proposal + href: terminology-index.md - name: Keyboard href: keyboard.md - name: Layout Engine diff --git a/docfx/schemas/tui-config-schema.json b/docfx/schemas/tui-config-schema.json index 87fec828d..52b7d7f38 100644 --- a/docfx/schemas/tui-config-schema.json +++ b/docfx/schemas/tui-config-schema.json @@ -220,7 +220,7 @@ "additionalProperties": true, "definitions": { "Color": { - "description": "One be either one of the W3C standard color names (parsed case-insensitively), a ColorName16 (e.g. 'BrightBlue', parsed case-insensitively), an rgb(r,g,b) tuple, or a hex color string in the format #RRGGBB.", + "description": "One of the standard color names (parsed case-insensitively; (e.g. 'BrightBlue'), an rgb(r,g,b) tuple, or a hex color string in the format #RRGGBB.", "$schema": "http://json-schema.org/draft-07/schema#", "type": "string", "oneOf": [ From a6258ed398cb080df26df1a539eb1fb2335d35c0 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 19 Nov 2025 20:39:34 -0500 Subject: [PATCH 02/16] Updates `IListDataSource.Render` to rename the `start` parameter to `viewportXOffset` (#4392) * Add comprehensive unit tests for WindowsKeyConverter - Implement 118 parallelizable unit tests for WindowsKeyConverter - Cover ToKey and ToKeyInfo methods with full bidirectional testing - Test basic characters, modifiers, special keys, function keys - Test VK_PACKET Unicode/IME input - Test OEM keys, NumPad keys, and lock states - Include round-trip conversion tests - All tests passing successfully Fixes #4389 * Rename `start` parameter to `viewportXOffset` for clarity The `start` parameter in several methods and interfaces has been renamed to `viewportXOffset` to better reflect its purpose as the horizontal offset of the viewport during string rendering. - Updated method signatures in `ListViewWithSelection` to use `viewportXOffset` instead of `start`, including default values. - Modified the `RenderUstr` method in `ListViewWithSelection` to use `viewportXOffset` for calculating the starting index. - Renamed the `start` parameter to `viewportXOffset` in the `IListDataSource` interface and updated its documentation. - Replaced all occurrences of `start` with `viewportXOffset` in the `ListWrapper` class, including method calls and logic. - Updated the `RenderUstr` method in `ListWrapper` to use `viewportXOffset` for substring calculations. - Adjusted the test method in `ListViewTests.cs` to reflect the parameter name change. These changes improve code readability and make the parameter's role in rendering logic more explicit. * Remove WindowsKeyConverterTests class that was added by mistake * Modernized ListView and IListDataSource - Tons of new unit tests Refactored `ListView` and `IListDataSource` to improve readability, maintainability, and functionality. Introduced `ListWrapper` as a default implementation of `IListDataSource` for easier integration with standard collections. Enhanced `ListView` with better handling of marking, selection, and scrolling. Replaced `viewportXOffset` with `viewportX` for horizontal scrolling. Added `EnsureSelectedItemVisible` to maintain visibility of the selected item. Updated `IListDataSource` with detailed XML documentation and added `SuspendCollectionChangedEvent` for bulk updates. Improved null safety with nullable reference types. Added comprehensive unit tests for `ListWrapper` and `IListDataSource` to ensure robustness. Modernized the codebase with C# features like expression-bodied members and pattern matching. Fixed bugs related to `SelectedItem` validation and rendering artifacts. * Improve index validation in ComboBox and ListView Enhance robustness by adding stricter checks for valid indices in ComboBox and ListView. Updated conditions in the `_listview.SelectedItemChanged` event handler to ensure `e.Item` is non-negative before accessing `_searchSet`. Modified the `SetValue` method to use `e.Item` instead of `_listview.SelectedItem`. In ListView, updated the `OnSelectedChanged` method to validate that `SelectedItem` is non-negative (`>= 0`) before accessing the `Source` list. These changes prevent potential out-of-range errors and improve code safety. * Refactor and enhance test coverage across modules Refactored and added new tests to improve coverage, readability, and consistency across multiple test files. Key changes include: - **ShortcutTests.cs**: Added tests for `BindKeyToApplication` and removed redundant tests. - **SourcesManagerTests.cs**: Renamed `Update_*` tests to `Load_*` for clarity. - **ArrangementTests.cs**: Reintroduced `MouseGrabHandler` tests, added `ViewArrangement` flag tests, and improved structure. - **NeedsDrawTests.cs**: Replaced `Application.Screen.Size` with fixed dimensions for better isolation. - **DimAutoTests.cs**: Updated layout tests to use fixed dimensions. - **FrameTests.cs**: Standardized object initialization and validated frame behavior. - **SubViewTests.cs**: Improved formatting and modernized event handling. - **NumericUpDownTests.cs**: Decoupled layout tests from screen size. General improvements: - Enhanced formatting and removed redundant tests. - Added comments for clarity. - Introduced `ITestOutputHelper` for better debugging in `ArrangementTests`. * Refactor to use nullable types for better null safety Enabled nullable reference types across the codebase to improve null safety and prevent potential null reference issues. Refactored `SelectedItem` and related properties from `int` to `int?` to represent no selection with `null` instead of `-1`. Updated logic, event arguments, and method signatures to handle nullable values consistently. Simplified object initialization using modern C# syntax and improved code readability with interpolated strings. Added null checks and early returns to prevent runtime errors. Enhanced error handling by throwing `ArgumentOutOfRangeException` for invalid values. Updated tests to reflect the changes, replacing assertions for `-1` with `null` and ensuring proper handling of nullable values. Cleaned up redundant code and improved formatting for better maintainability. * on` functionality has been deprecated, refactored, or removed from the `Shortcut` class. * Refactor: Transition to instance-based architecture Updated `Run-LocalCoverage.ps1` to increase `--blame-hang-timeout` from 10s to 60s. Improved null safety in `GuiTestContext` by adding null-conditional operators. Commented out problematic code in `SetupFakeApplicationAttribute.cs` to prevent test hangs. Excluded `ViewBase` files from `UnitTests.Parallelizable.csproj` and removed redundant folder declarations. Simplified event handling in `IListDataSourceTests.cs` and updated `ListViewTests.cs` to use nullable reference types. Enhanced documentation to emphasize the transition to an instance-based application architecture. Updated examples in `application.md`, `multitasking.md`, and `navigation.md` to reflect the use of `Application.Create()` and `View.App`. Clarified the obsolescence of the static `Application` class. Revised table of contents in `toc.yml` to include new sections like "Application Deep Dive" and "Scheme Deep Dive." Added `dotnet-tools.json` for tool configuration. These changes improve maintainability, testability, and alignment with modern C# practices. * Refactor ListViewTests to use Terminal.Gui framework The `ListViewTests` class has been refactored to replace the `AutoInitShutdown` attribute with explicit application lifecycle management using `IApplication` and `app.Init()` from the `Terminal.Gui` framework. Key changes include: - Rewriting tests to use `Terminal.Gui`'s application lifecycle. - Adding a private `_output` field for logging test output via `ITestOutputHelper`. - Updating `DriverAssert.AssertDriverContentsWithFrameAre` to include `app.Driver` for UI verification. - Rewriting tests like `Clicking_On_Border_Is_Ignored`, `EnsureSelectedItemVisible_SelectedItem`, and others to align with the new framework. - Adding explicit calls to `app.Shutdown()` for proper cleanup. - Enabling nullable reference types with `#nullable enable`. - Updating `using` directives and `namespace` to reflect the new structure. These changes improve test maintainability, compatibility, and diagnostics. * Update Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Examples/UICatalog/UICatalogTop.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Terminal.Gui/Views/ListWrapper.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Terminal.Gui/Views/ListWrapper.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Updated the `SetMark` method to return `Source.IsMarked(SelectedItem.Value)` for consistency and removed an outdated comment questioning its correctness. Enhanced the exception message in the `SelectedItem` property setter to provide clearer guidance when the value is out of range. * Add comprehensive ListView behavior test coverage Added multiple test methods to validate `ListView` behavior: - `Vertical_ScrollBar_Hides_And_Shows_As_Needed`: Ensures the vertical scrollbar auto-hides/shows based on content height. - `Mouse_Wheel_Scrolls`: Verifies vertical scrolling with the mouse wheel updates `TopItem`. - `SelectedItem_With_Source_Null_Does_Nothing`: Confirms no exceptions occur when setting `SelectedItem` with a `null` source. - `Horizontal_Scroll`: Tests horizontal scrolling, including programmatic and mouse wheel interactions, ensuring `LeftItem` updates correctly. - `SetSourceAsync_SetsSource`: Validates the asynchronous `SetSourceAsync` method updates the source and item count. - `AllowsMultipleSelection_Set_To_False_Unmarks_All_But_Selected`: Ensures disabling multiple selection unmarks all but the selected item. - `Source_CollectionChanged_Remove`: Confirms `SelectedItem` and source count update correctly when items are removed from the source collection. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .config/dotnet-tools.json | 5 + .../UICatalog/Scenarios/AllViewsTester.cs | 2 +- .../UICatalog/Scenarios/ComboBoxIteration.cs | 4 +- .../UICatalog/Scenarios/DynamicMenuBar.cs | 24 +- .../UICatalog/Scenarios/DynamicStatusBar.cs | 29 +- .../Scenarios/ListViewWithSelection.cs | 8 +- .../UICatalog/Scenarios/ListsAndCombos.cs | 2 +- Examples/UICatalog/Scenarios/SpinnerStyles.cs | 2 +- Examples/UICatalog/UICatalogTop.cs | 14 +- Scripts/Run-LocalCoverage.ps1 | 2 +- .../CollectionNavigatorBase.cs | 74 +- .../ICollectionNavigator.cs | 8 +- Terminal.Gui/Views/ComboBox.cs | 27 +- Terminal.Gui/Views/IListDataSource.cs | 107 +- Terminal.Gui/Views/ListView.cs | 1451 +++++++-------- Terminal.Gui/Views/ListViewEventArgs.cs | 9 +- Terminal.Gui/Views/ListWrapper.cs | 256 +++ Terminal.Gui/Views/TableView/TableView.cs | 6 +- Terminal.Gui/Views/Toplevel.cs | 2 +- Terminal.sln.DotSettings | 1 + .../UICatalog/ScenarioTests.cs | 4 +- .../GuiTestContext.ContextMenu.cs | 2 +- .../GuiTestContext.ViewBase.cs | 4 +- .../SetupFakeApplicationAttribute.cs | 5 +- Tests/UnitTests/Views/ListViewTests.cs | 1225 ------------- .../Configuration/SourcesManagerTests.cs | 22 +- .../Text/CollectionNavigatorTests.cs | 30 +- .../UnitTests.Parallelizable.csproj | 8 +- .../View/Draw/NeedsDrawTests.cs | 10 +- .../View/Layout/Dim.AutoTests.PosTypes.cs | 2 +- .../View/Layout/Dim.AutoTests.cs | 14 +- .../View/Layout/FrameTests.cs | 10 +- .../View/SubviewTests.cs | 12 +- .../Views/IListDataSourceTests.cs | 513 ++++++ .../Views/ListViewTests.cs | 1556 ++++++++++++++++- .../Views/NumericUpDownTests.cs | 10 +- docfx/docs/application.md | 56 +- docfx/docs/config.md | 3 +- docfx/docs/index.md | 8 + docfx/docs/migratingfromv1.md | 68 + docfx/docs/multitasking.md | 56 +- docfx/docs/navigation.md | 15 +- docfx/docs/newinv2.md | 41 + docfx/docs/toc.yml | 20 +- 44 files changed, 3372 insertions(+), 2355 deletions(-) create mode 100644 .config/dotnet-tools.json create mode 100644 Terminal.Gui/Views/ListWrapper.cs delete mode 100644 Tests/UnitTests/Views/ListViewTests.cs create mode 100644 Tests/UnitTestsParallelizable/Views/IListDataSourceTests.cs diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 000000000..b0e38abda --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "isRoot": true, + "tools": {} +} \ No newline at end of file diff --git a/Examples/UICatalog/Scenarios/AllViewsTester.cs b/Examples/UICatalog/Scenarios/AllViewsTester.cs index 2f6e9e016..5ac97bea9 100644 --- a/Examples/UICatalog/Scenarios/AllViewsTester.cs +++ b/Examples/UICatalog/Scenarios/AllViewsTester.cs @@ -65,7 +65,7 @@ public class AllViewsTester : Scenario // Dispose existing current View, if any DisposeCurrentView (); - CreateCurrentView (_viewClasses.Values.ToArray () [_classListView.SelectedItem]); + CreateCurrentView (_viewClasses.Values.ToArray () [_classListView.SelectedItem.Value]); // Force ViewToEdit to be the view and not a subview if (_adornmentsEditor is { }) diff --git a/Examples/UICatalog/Scenarios/ComboBoxIteration.cs b/Examples/UICatalog/Scenarios/ComboBoxIteration.cs index 9440f37f3..003926396 100644 --- a/Examples/UICatalog/Scenarios/ComboBoxIteration.cs +++ b/Examples/UICatalog/Scenarios/ComboBoxIteration.cs @@ -42,8 +42,8 @@ public class ComboBoxIteration : Scenario listview.SelectedItemChanged += (s, e) => { - lbListView.Text = items [e.Item]; - comboBox.SelectedItem = e.Item; + lbListView.Text = items [e.Item!.Value]; + comboBox.SelectedItem = e.Item.Value; }; comboBox.SelectedItemChanged += (sender, text) => diff --git a/Examples/UICatalog/Scenarios/DynamicMenuBar.cs b/Examples/UICatalog/Scenarios/DynamicMenuBar.cs index 2687b4f6c..28a13a916 100644 --- a/Examples/UICatalog/Scenarios/DynamicMenuBar.cs +++ b/Examples/UICatalog/Scenarios/DynamicMenuBar.cs @@ -712,7 +712,7 @@ public class DynamicMenuBar : Scenario btnUp.Accepting += (s, e) => { - int i = _lstMenus.SelectedItem; + int i = _lstMenus.SelectedItem.Value; MenuItem menuItem = DataContext.Menus.Count > 0 ? DataContext.Menus [i].MenuItem : null; if (menuItem != null) @@ -734,7 +734,7 @@ public class DynamicMenuBar : Scenario btnDown.Accepting += (s, e) => { - int i = _lstMenus.SelectedItem; + int i = _lstMenus.SelectedItem.Value; MenuItem menuItem = DataContext.Menus.Count > 0 ? DataContext.Menus [i].MenuItem : null; if (menuItem != null) @@ -836,7 +836,7 @@ public class DynamicMenuBar : Scenario : MenuItemCheckStyle.Radio, ShortcutKey = frmMenuDetails.TextShortcutKey.Text }; - UpdateMenuItem (_currentEditMenuBarItem, menuItem, _lstMenus.SelectedItem); + UpdateMenuItem (_currentEditMenuBarItem, menuItem, _lstMenus.SelectedItem.Value); } }; @@ -885,8 +885,8 @@ public class DynamicMenuBar : Scenario btnRemove.Accepting += (s, e) => { - MenuItem menuItem = (DataContext.Menus.Count > 0 && _lstMenus.SelectedItem > -1 - ? DataContext.Menus [_lstMenus.SelectedItem].MenuItem + MenuItem menuItem = (DataContext.Menus.Count > 0 && _lstMenus.SelectedItem is {} selectedItem + ? DataContext.Menus [selectedItem].MenuItem : _currentEditMenuBarItem); if (menuItem != null) @@ -905,9 +905,9 @@ public class DynamicMenuBar : Scenario SelectCurrentMenuBarItem (); } - if (_lstMenus.SelectedItem > -1) + if (_lstMenus.SelectedItem is {} selected) { - DataContext.Menus?.RemoveAt (_lstMenus.SelectedItem); + DataContext.Menus?.RemoveAt (selected); } if (_lstMenus.Source.Count > 0 && _lstMenus.SelectedItem > _lstMenus.Source.Count - 1) @@ -927,7 +927,7 @@ public class DynamicMenuBar : Scenario _lstMenus.OpenSelectedItem += (s, e) => { - _currentMenuBarItem = DataContext.Menus [e.Item].MenuItem; + _currentMenuBarItem = DataContext.Menus [e.Item.Value].MenuItem; if (!(_currentMenuBarItem is MenuBarItem)) { @@ -945,8 +945,8 @@ public class DynamicMenuBar : Scenario _lstMenus.HasFocusChanging += (s, e) => { - MenuItem menuBarItem = _lstMenus.SelectedItem > -1 && DataContext.Menus.Count > 0 - ? DataContext.Menus [_lstMenus.SelectedItem].MenuItem + MenuItem menuBarItem = _lstMenus.SelectedItem is {} selectedItem && DataContext.Menus.Count > 0 + ? DataContext.Menus [selectedItem].MenuItem : null; SetFrameDetails (menuBarItem); }; @@ -1077,8 +1077,8 @@ public class DynamicMenuBar : Scenario if (menuBarItem == null) { - menuItem = _lstMenus.SelectedItem > -1 && DataContext.Menus.Count > 0 - ? DataContext.Menus [_lstMenus.SelectedItem].MenuItem + menuItem = _lstMenus.SelectedItem is {} selectedItem && DataContext.Menus.Count > 0 + ? DataContext.Menus [selectedItem].MenuItem : _currentEditMenuBarItem; } else diff --git a/Examples/UICatalog/Scenarios/DynamicStatusBar.cs b/Examples/UICatalog/Scenarios/DynamicStatusBar.cs index de110d0c6..73dd3b802 100644 --- a/Examples/UICatalog/Scenarios/DynamicStatusBar.cs +++ b/Examples/UICatalog/Scenarios/DynamicStatusBar.cs @@ -312,7 +312,12 @@ public class DynamicStatusBar : Scenario btnUp.Accepting += (s, e) => { - int i = _lstItems.SelectedItem; + if (_lstItems.SelectedItem is null) + { + return; + } + int i = _lstItems.SelectedItem.Value; + Shortcut statusItem = DataContext.Items.Count > 0 ? DataContext.Items [i].Shortcut : null; if (statusItem != null) @@ -335,7 +340,12 @@ public class DynamicStatusBar : Scenario btnDown.Accepting += (s, e) => { - int i = _lstItems.SelectedItem; + if (_lstItems.SelectedItem is null) + { + return; + } + int i = _lstItems.SelectedItem.Value; + Shortcut statusItem = DataContext.Items.Count > 0 ? DataContext.Items [i].Shortcut : null; if (statusItem != null) @@ -376,14 +386,17 @@ public class DynamicStatusBar : Scenario } else if (_currentEditStatusItem != null) { - var statusItem = new DynamicStatusItem { Title = frmStatusBarDetails.TextTitle.Text, Action = frmStatusBarDetails.TextAction.Text, Shortcut = frmStatusBarDetails.TextShortcut.Text }; - UpdateStatusItem (_currentEditStatusItem, statusItem, _lstItems.SelectedItem); + + if (_lstItems.SelectedItem is { } selectedItem) + { + UpdateStatusItem (_currentEditStatusItem, statusItem, selectedItem); + } } }; @@ -420,14 +433,14 @@ public class DynamicStatusBar : Scenario btnRemove.Accepting += (s, e) => { Shortcut statusItem = DataContext.Items.Count > 0 - ? DataContext.Items [_lstItems.SelectedItem].Shortcut + ? DataContext.Items [_lstItems.SelectedItem.Value].Shortcut : null; if (statusItem != null) { _statusBar.RemoveShortcut (_currentSelectedStatusBar); statusItem.Dispose (); - DataContext.Items.RemoveAt (_lstItems.SelectedItem); + DataContext.Items.RemoveAt (_lstItems.SelectedItem.Value); if (_lstItems.Source.Count > 0 && _lstItems.SelectedItem > _lstItems.Source.Count - 1) { @@ -442,7 +455,7 @@ public class DynamicStatusBar : Scenario _lstItems.HasFocusChanging += (s, e) => { Shortcut statusItem = DataContext.Items.Count > 0 - ? DataContext.Items [_lstItems.SelectedItem].Shortcut + ? DataContext.Items [_lstItems.SelectedItem.Value].Shortcut : null; SetFrameDetails (statusItem); }; @@ -489,7 +502,7 @@ public class DynamicStatusBar : Scenario if (statusItem == null) { newStatusItem = DataContext.Items.Count > 0 - ? DataContext.Items [_lstItems.SelectedItem].Shortcut + ? DataContext.Items [_lstItems.SelectedItem.Value].Shortcut : null; } else diff --git a/Examples/UICatalog/Scenarios/ListViewWithSelection.cs b/Examples/UICatalog/Scenarios/ListViewWithSelection.cs index 874d061a5..a20eb67af 100644 --- a/Examples/UICatalog/Scenarios/ListViewWithSelection.cs +++ b/Examples/UICatalog/Scenarios/ListViewWithSelection.cs @@ -237,7 +237,7 @@ public class ListViewWithSelection : Scenario int col, int line, int width, - int start = 0 + int viewportX = 0 ) { container.Move (col, line); @@ -247,7 +247,7 @@ public class ListViewWithSelection : Scenario string.Format ("{{0,{0}}}", -_nameColumnWidth), Scenarios [item].GetName () ); - RenderUstr (container, $"{s} ({Scenarios [item].GetDescription ()})", col, line, width, start); + RenderUstr (container, $"{s} ({Scenarios [item].GetDescription ()})", col, line, width, viewportX); } public void SetMark (int item, bool value) @@ -288,10 +288,10 @@ public class ListViewWithSelection : Scenario } // A slightly adapted method from: https://github.com/gui-cs/Terminal.Gui/blob/fc1faba7452ccbdf49028ac49f0c9f0f42bbae91/Terminal.Gui/Views/ListView.cs#L433-L461 - private void RenderUstr (View view, string ustr, int col, int line, int width, int start = 0) + private void RenderUstr (View view, string ustr, int col, int line, int width, int viewportX = 0) { var used = 0; - int index = start; + int index = viewportX; while (index < ustr.Length) { diff --git a/Examples/UICatalog/Scenarios/ListsAndCombos.cs b/Examples/UICatalog/Scenarios/ListsAndCombos.cs index dba1adc73..775c78d24 100644 --- a/Examples/UICatalog/Scenarios/ListsAndCombos.cs +++ b/Examples/UICatalog/Scenarios/ListsAndCombos.cs @@ -50,7 +50,7 @@ public class ListsAndCombos : Scenario Width = Dim.Percent (40), Source = new ListWrapper (items) }; - listview.SelectedItemChanged += (s, e) => lbListView.Text = items [listview.SelectedItem]; + listview.SelectedItemChanged += (s, e) => lbListView.Text = items [listview.SelectedItem.Value]; win.Add (lbListView, listview); //var scrollBar = new ScrollBarView (listview, true); diff --git a/Examples/UICatalog/Scenarios/SpinnerStyles.cs b/Examples/UICatalog/Scenarios/SpinnerStyles.cs index e9e923ae7..87e2b1b3b 100644 --- a/Examples/UICatalog/Scenarios/SpinnerStyles.cs +++ b/Examples/UICatalog/Scenarios/SpinnerStyles.cs @@ -153,7 +153,7 @@ public class SpinnerViewStyles : Scenario else { spinner.Visible = true; - spinner.Style = (SpinnerStyle)Activator.CreateInstance (styleDict [e.Item].Value); + spinner.Style = (SpinnerStyle)Activator.CreateInstance (styleDict [e.Item.Value].Value); delayField.Text = spinner.SpinDelay.ToString (); ckbBounce.CheckedState = spinner.SpinBounce ? CheckState.Checked : CheckState.UnChecked; ckbNoSpecial.CheckedState = !spinner.HasSpecialCharacters ? CheckState.Checked : CheckState.UnChecked; diff --git a/Examples/UICatalog/UICatalogTop.cs b/Examples/UICatalog/UICatalogTop.cs index 88a5e4144..63c3eb528 100644 --- a/Examples/UICatalog/UICatalogTop.cs +++ b/Examples/UICatalog/UICatalogTop.cs @@ -43,7 +43,11 @@ public class UICatalogTop : Toplevel Unloaded += UnloadedHandler; // Restore previous selections - _categoryList.SelectedItem = _cachedCategoryIndex; + if (_categoryList.Source?.Count > 0) { + _categoryList.SelectedItem = _cachedCategoryIndex ?? 0; + } else { + _categoryList.SelectedItem = null; + } _scenarioList.SelectedRow = _cachedScenarioIndex; SchemeName = CachedTopLevelScheme = SchemeManager.SchemesToSchemeName (Schemes.Base); @@ -510,7 +514,7 @@ public class UICatalogTop : Toplevel #region Category List private readonly ListView? _categoryList; - private static int _cachedCategoryIndex; + private static int? _cachedCategoryIndex; public static ObservableCollection? CachedCategories { get; set; } private ListView CreateCategoryList () @@ -540,7 +544,11 @@ public class UICatalogTop : Toplevel private void CategoryView_SelectedChanged (object? sender, ListViewItemEventArgs? e) { - string item = CachedCategories! [e!.Item]; + if (e is null or { Item: null }) + { + return; + } + string item = CachedCategories! [e.Item.Value]; ObservableCollection newScenarioList; if (e.Item == 0) diff --git a/Scripts/Run-LocalCoverage.ps1 b/Scripts/Run-LocalCoverage.ps1 index 312b229a9..32b88053d 100644 --- a/Scripts/Run-LocalCoverage.ps1 +++ b/Scripts/Run-LocalCoverage.ps1 @@ -27,7 +27,7 @@ dotnet test Tests/UnitTests ` --verbosity minimal ` --collect:"XPlat Code Coverage" ` --settings Tests/UnitTests/runsettings.coverage.xml ` - --blame-hang-timeout 10s + --blame-hang-timeout 60s # ------------------------------------------------------------ # 4. Run UNIT TESTS (parallel) diff --git a/Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs index 39234b10b..7fd71f491 100644 --- a/Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs +++ b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs @@ -1,6 +1,5 @@ - namespace Terminal.Gui.Views; /// @@ -27,8 +26,13 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator public int TypingDelay { get; set; } = 500; /// - public int GetNextMatchingItem (int currentIndex, char keyStruck) + public int? GetNextMatchingItem (int? currentIndex, char keyStruck) { + if (currentIndex.HasValue && currentIndex < 0) + { + throw new ArgumentOutOfRangeException (nameof (currentIndex), @"Must be non-negative"); + } + if (!char.IsControl (keyStruck)) { // maybe user pressed 'd' and now presses 'd' again. @@ -36,7 +40,7 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator // but if we find none then we must fallback on cycling // d instead and discard the candidate state var candidateState = ""; - var elapsedTime = DateTime.Now - _lastKeystroke; + TimeSpan elapsedTime = DateTime.Now - _lastKeystroke; Logging.Debug ($"CollectionNavigator began processing '{keyStruck}', it has been {elapsedTime} since last keystroke"); @@ -51,26 +55,28 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator { // its a fresh keystroke after some time // or its first ever key press - SearchString = new string (keyStruck, 1); - Logging.Debug ($"It has been too long since last key press so beginning new search"); + SearchString = new (keyStruck, 1); + Logging.Debug ("It has been too long since last key press so beginning new search"); } - int idxCandidate = GetNextMatchingItem ( - currentIndex, - candidateState, + int? idxCandidate = GetNextMatchingItem ( + currentIndex, + candidateState, - // prefer not to move if there are multiple characters e.g. "ca" + 'r' should stay on "car" and not jump to "cart" - candidateState.Length > 1 - ); + // prefer not to move if there are multiple characters e.g. "ca" + 'r' should stay on "car" and not jump to "cart" + candidateState.Length > 1 + ); Logging.Debug ($"CollectionNavigator searching (preferring minimum movement) matched:{idxCandidate}"); - if (idxCandidate != -1) + + if (idxCandidate is { }) { // found "dd" so candidate search string is accepted _lastKeystroke = DateTime.Now; SearchString = candidateState; Logging.Debug ($"Found collection item that matched search:{idxCandidate}"); + return idxCandidate; } @@ -83,16 +89,17 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator // if a match wasn't found, the user typed a 'wrong' key in their search ("can" + 'z' // instead of "can" + 'd'). - if (SearchString.Length > 1 && idxCandidate == -1) + if (SearchString.Length > 1 && idxCandidate is null) { Logging.Debug ("CollectionNavigator ignored key and returned existing index"); + // ignore it since we're still within the typing delay // don't add it to SearchString either return currentIndex; } // if no changes to current state manifested - if (idxCandidate == currentIndex || idxCandidate == -1) + if (idxCandidate == currentIndex || idxCandidate is null) { Logging.Debug ("CollectionNavigator found no changes to current index, so clearing search"); @@ -100,37 +107,29 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator ClearSearchString (); // match on the fresh letter alone - SearchString = new string (keyStruck, 1); + SearchString = new (keyStruck, 1); idxCandidate = GetNextMatchingItem (currentIndex, SearchString); Logging.Debug ($"CollectionNavigator new SearchString {SearchString} matched index:{idxCandidate}"); - return idxCandidate == -1 ? currentIndex : idxCandidate; + return idxCandidate ?? currentIndex; } Logging.Debug ($"CollectionNavigator final answer was:{idxCandidate}"); + // Found another "d" or just leave index as it was return idxCandidate; } - Logging.Debug ("CollectionNavigator found key press was not actionable so clearing search and returning -1"); + Logging.Debug ("CollectionNavigator found key press was not actionable so clearing search and returning null"); // clear state because keypress was a control char ClearSearchString (); // control char indicates no selection - return -1; + return null; } - - - /// - /// Raised when the is changed. Useful for debugging. Raises the - /// event. - /// - /// - protected virtual void OnSearchStringChanged (KeystrokeNavigatorEventArgs e) { SearchStringChanged?.Invoke (this, e); } - /// This event is raised when is changed. Useful for debugging. public event EventHandler? SearchStringChanged; @@ -141,6 +140,13 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator /// Return the number of elements in the collection protected abstract int GetCollectionLength (); + /// + /// Raised when the is changed. Useful for debugging. Raises the + /// event. + /// + /// + protected virtual void OnSearchStringChanged (KeystrokeNavigatorEventArgs e) { SearchStringChanged?.Invoke (this, e); } + /// Gets the index of the next item in the collection that matches . /// The index in the collection to start the search from. /// The search string to use. @@ -150,17 +156,17 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator /// (the default), the next matching item will be returned, even if it is above in the /// collection. /// - /// The index of the next matching item or if no match was found. - internal int GetNextMatchingItem (int currentIndex, string search, bool minimizeMovement = false) + /// The index of the next matching item or if no match was found. + internal int? GetNextMatchingItem (int? currentIndex, string search, bool minimizeMovement = false) { if (string.IsNullOrEmpty (search)) { - return -1; + return null; } int collectionLength = GetCollectionLength (); - if (currentIndex != -1 && currentIndex < collectionLength && Matcher.IsMatch (search, ElementAt (currentIndex))) + if (currentIndex.HasValue && currentIndex < collectionLength && Matcher.IsMatch (search, ElementAt (currentIndex.Value))) { // we are already at a match if (minimizeMovement) @@ -172,9 +178,9 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator for (var i = 1; i < collectionLength; i++) { //circular - int idxCandidate = (i + currentIndex) % collectionLength; + int? idxCandidate = (i + currentIndex) % collectionLength; - if (Matcher.IsMatch (search, ElementAt (idxCandidate))) + if (Matcher.IsMatch (search, ElementAt (idxCandidate!.Value))) { return idxCandidate; } @@ -194,7 +200,7 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator } // Nothing matches - return -1; + return null; } private void ClearSearchString () diff --git a/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigator.cs b/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigator.cs index 5ea621646..69256db43 100644 --- a/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigator.cs +++ b/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigator.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui.Views; /// /// Navigates a collection of items using keystrokes. The keystrokes are used to build a search string. The /// is used to find the next item in the collection that matches the search string when -/// is called. +/// is called. /// /// If the user types keystrokes that can't be found in the collection, the search string is cleared and the next /// item is found that starts with the last keystroke. @@ -17,7 +17,7 @@ public interface ICollectionNavigator { /// /// Gets or sets the number of milliseconds to delay before clearing the search string. The delay is reset on each - /// call to . The default is 500ms. + /// call to . The default is 500ms. /// public int TypingDelay { get; set; } @@ -43,8 +43,8 @@ public interface ICollectionNavigator /// The index in the collection to start the search from. /// The character of the key the user pressed. /// - /// The index of the item that matches what the user has typed. Returns if no item in the + /// The index of the item that matches what the user has typed. Returns if no item in the /// collection matched. /// - int GetNextMatchingItem (int currentIndex, char keyStruck); + int? GetNextMatchingItem (int? currentIndex, char keyStruck); } diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index 86c4f4f79..83e72ac79 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -47,9 +47,9 @@ public class ComboBox : View, IDesignable }; _listview.SelectedItemChanged += (sender, e) => { - if (!HideDropdownListOnClick && _searchSet.Count > 0) + if (e.Item >= 0 && !HideDropdownListOnClick && _searchSet.Count > 0) { - SetValue (_searchSet [_listview.SelectedItem]); + SetValue (_searchSet [e.Item.Value]); } }; Add (_search, _listview); @@ -114,7 +114,7 @@ public class ComboBox : View, IDesignable /// protected override bool OnSettingScheme (ValueChangingEventArgs args) { - _listview.SetScheme(args.NewValue); + _listview.SetScheme (args.NewValue); return base.OnSettingScheme (args); } @@ -461,7 +461,10 @@ public class ComboBox : View, IDesignable private void FocusSelectedItem () { - _listview.SelectedItem = SelectedItem > -1 ? SelectedItem : 0; + if (_listview.Source?.Count > 0) + { + _listview.SelectedItem = SelectedItem > -1 ? SelectedItem : 0; + } _listview.TabStop = TabBehavior.TabStop; _listview.SetFocus (); OnExpanded (); @@ -517,9 +520,9 @@ public class ComboBox : View, IDesignable _listview.TabStop = TabBehavior.TabStop; _listview.SetFocus (); - if (_listview.SelectedItem > -1) + if (_listview.SelectedItem is { }) { - SetValue (_searchSet [_listview.SelectedItem]); + SetValue (_searchSet [_listview.SelectedItem.Value]); } else { @@ -728,7 +731,7 @@ public class ComboBox : View, IDesignable IsShow = false; _listview.TabStop = TabBehavior.NoStop; - if (_listview.Source.Count == 0 || (_searchSet?.Count ?? 0) == 0) + if (_listview.Source!.Count == 0 || (_searchSet?.Count ?? 0) == 0) { _text = ""; HideList (); @@ -737,7 +740,7 @@ public class ComboBox : View, IDesignable return false; } - SetValue (_listview.SelectedItem > -1 ? _searchSet [_listview.SelectedItem] : _text); + SetValue (_listview.SelectedItem is { } ? _searchSet [_listview.SelectedItem.Value] : _text); _search.CursorPosition = _search.Text.GetColumns (); ShowHideList (Text); OnOpenSelectedItem (); @@ -977,7 +980,11 @@ public class ComboBox : View, IDesignable { bool res = base.OnSelectedChanged (); - _highlighted = SelectedItem; + if (SelectedItem is null) + { + return res; + } + _highlighted = SelectedItem.Value; return res; } @@ -997,7 +1004,7 @@ public class ComboBox : View, IDesignable _container = container ?? throw new ArgumentNullException ( nameof (container), - "ComboBox container cannot be null." + @"ComboBox container cannot be null." ); HideDropdownListOnClick = hideDropdownListOnClick; AddCommand (Command.Up, () => _container.MoveUpList ()); diff --git a/Terminal.Gui/Views/IListDataSource.cs b/Terminal.Gui/Views/IListDataSource.cs index 76ab7e956..d5d1e5bde 100644 --- a/Terminal.Gui/Views/IListDataSource.cs +++ b/Terminal.Gui/Views/IListDataSource.cs @@ -4,43 +4,68 @@ using System.Collections.Specialized; namespace Terminal.Gui.Views; -/// Implement to provide custom rendering for a . +/// +/// Provides data and rendering for . Implement this interface to provide custom rendering +/// or to wrap custom data sources. +/// +/// +/// +/// The default implementation is which renders items using +/// . +/// +/// +/// Implementors must manage their own marking state and raise when the +/// underlying data changes. +/// +/// public interface IListDataSource : IDisposable { /// - /// Event to raise when an item is added, removed, or moved, or the entire list is refreshed. + /// Raised when items are added, removed, moved, or the entire collection is refreshed. /// + /// + /// subscribes to this event to update its display and content size when the data + /// changes. Implementations should raise this event whenever the underlying collection changes, unless + /// is . + /// event NotifyCollectionChangedEventHandler CollectionChanged; - /// Returns the number of elements to display + /// Gets the number of items in the data source. int Count { get; } - /// Returns the maximum length of elements to display - int Length { get; } - - /// - /// Allow suspending the event from being invoked, - /// if , otherwise is . - /// - bool SuspendCollectionChangedEvent { get; set; } - - /// Should return whether the specified item is currently marked. - /// , if marked, otherwise. - /// Item index. + /// Determines whether the specified item is marked. + /// The zero-based index of the item. + /// if the item is marked; otherwise . + /// + /// calls this method to determine whether to render the item with a mark indicator when + /// is . + /// bool IsMarked (int item); - /// This method is invoked to render a specified item, the method should cover the entire provided width. - /// The render. - /// The list view to render. - /// Describes whether the item being rendered is currently selected by the user. - /// The index of the item to render, zero for the first item and so on. - /// The column where the rendering will start - /// The line where the rendering will be done. - /// The width that must be filled out. - /// The index of the string to be displayed. + /// Gets the width in columns of the widest item in the data source. /// - /// The default color will be set before this method is invoked, and will be based on whether the item is selected - /// or not. + /// uses this value to set its horizontal content size for scrolling. + /// + int Length { get; } + + /// Renders the specified item to the . + /// The to render to. + /// + /// if the item is currently selected; otherwise . + /// + /// The zero-based index of the item to render. + /// The column in where rendering starts. + /// The line in where rendering occurs. + /// The width available for rendering. + /// The horizontal scroll offset. + /// + /// + /// calls this method for each visible item during rendering. The color scheme will be + /// set based on selection state before this method is called. + /// + /// + /// Implementations must fill the entire to avoid rendering artifacts. + /// /// void Render ( ListView listView, @@ -49,15 +74,33 @@ public interface IListDataSource : IDisposable int col, int line, int width, - int start = 0 + int viewportX = 0 ); - /// Flags the item as marked. - /// Item index. - /// If set to value. + /// Sets the marked state of the specified item. + /// The zero-based index of the item. + /// to mark the item; to unmark it. + /// + /// calls this method when the user toggles marking (e.g., via the SPACE key) if + /// is . + /// void SetMark (int item, bool value); - /// Return the source as IList. - /// + /// + /// Gets or sets whether the event should be suppressed. + /// + /// + /// Set to to prevent from being raised during bulk + /// operations. Set back to to resume event notifications. + /// + bool SuspendCollectionChangedEvent { get; set; } + + /// Returns the underlying data source as an . + /// The data source as an . + /// + /// uses this method to access individual items for events like + /// and to enable keyboard search via + /// . + /// IList ToList (); } diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 729e8066b..e958844bf 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable enable using System.Collections; using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -17,7 +17,8 @@ namespace Terminal.Gui.Views; /// /// /// By default uses to render the items of any -/// object (e.g. arrays, , and other collections). Alternatively, an +/// object (e.g. arrays, , and other collections). +/// Alternatively, an /// object that implements can be provided giving full control of what is rendered. /// /// @@ -43,11 +44,6 @@ namespace Terminal.Gui.Views; /// public class ListView : View, IDesignable { - private bool _allowsMarking; - private bool _allowsMultipleSelection = false; - private int _lastSelectedItem = -1; - private int _selected = -1; - private IListDataSource _source; // TODO: ListView has been upgraded to use Viewport and ContentSize instead of the // TODO: bespoke _top and _left. It was a quick & dirty port. There is now duplicate logic // TODO: that could be removed. @@ -63,22 +59,8 @@ public class ListView : View, IDesignable // Things this view knows how to do // - AddCommand (Command.Up, (ctx) => - { - if (RaiseSelecting (ctx) == true) - { - return true; - } - return MoveUp (); - }); - AddCommand (Command.Down, (ctx) => - { - if (RaiseSelecting (ctx) == true) - { - return true; - } - return MoveDown (); - }); + AddCommand (Command.Up, ctx => RaiseSelecting (ctx) == true || MoveUp ()); + AddCommand (Command.Down, ctx => RaiseSelecting (ctx) == true || MoveDown ()); // TODO: add RaiseSelecting to all of these AddCommand (Command.ScrollUp, () => ScrollVertical (-1)); @@ -91,66 +73,67 @@ public class ListView : View, IDesignable AddCommand (Command.ScrollRight, () => ScrollHorizontal (1)); // Accept (Enter key) - Raise Accept event - DO NOT advance state - AddCommand (Command.Accept, (ctx) => - { - if (RaiseAccepting (ctx) == true) - { - return true; - } + AddCommand ( + Command.Accept, + ctx => + { + if (RaiseAccepting (ctx) == true) + { + return true; + } - if (OnOpenSelectedItem ()) - { - return true; - } - - return false; - }); + return OnOpenSelectedItem (); + }); // Select (Space key and single-click) - If markable, change mark and raise Select event - AddCommand (Command.Select, (ctx) => - { - if (_allowsMarking) - { - if (RaiseSelecting (ctx) == true) - { - return true; - } + AddCommand ( + Command.Select, + ctx => + { + if (!_allowsMarking) + { + return false; + } - if (MarkUnmarkSelectedItem ()) - { - return true; - } - } - - return false; - }); + if (RaiseSelecting (ctx) == true) + { + return true; + } + return MarkUnmarkSelectedItem (); + }); // Hotkey - If none set, select and raise Select event. SetFocus. - DO NOT raise Accept - AddCommand (Command.HotKey, (ctx) => - { - if (SelectedItem == -1) - { - SelectedItem = 0; - if (RaiseSelecting (ctx) == true) - { - return true; + AddCommand ( + Command.HotKey, + ctx => + { + if (SelectedItem is { }) + { + return !SetFocus (); + } - } - } + SelectedItem = 0; - return !SetFocus (); - }); + if (RaiseSelecting (ctx) == true) + { + return true; + } - AddCommand (Command.SelectAll, (ctx) => - { - if (ctx is not CommandContext keyCommandContext) - { - return false; - } + return !SetFocus (); + }); - return keyCommandContext.Binding.Data is { } && MarkAll ((bool)keyCommandContext.Binding.Data); - }); + AddCommand ( + Command.SelectAll, + ctx => + { + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + + return keyCommandContext.Binding.Data is { } && MarkAll ((bool)keyCommandContext.Binding.Data); + }); // Default keybindings for all ListViews KeyBindings.Add (Key.CursorUp, Command.Up); @@ -169,23 +152,25 @@ public class ListView : View, IDesignable KeyBindings.Add (Key.End, Command.End); // Key.Space is already bound to Command.Select; this gives us select then move down - KeyBindings.Add (Key.Space.WithShift, [Command.Select, Command.Down]); + KeyBindings.Add (Key.Space.WithShift, Command.Select, Command.Down); // Use the form of Add that lets us pass context to the handler KeyBindings.Add (Key.A.WithCtrl, new KeyBinding ([Command.SelectAll], true)); KeyBindings.Add (Key.U.WithCtrl, new KeyBinding ([Command.SelectAll], false)); } - /// - protected override void OnViewportChanged (DrawEventArgs e) - { - SetContentSize (new Size (MaxLength, _source?.Count ?? Viewport.Height)); - } + private bool _allowsMarking; + private bool _allowsMultipleSelection; - /// - protected override void OnFrameChanged (in Rectangle frame) + private IListDataSource? _source; + + /// + public bool EnableForDesign () { - EnsureSelectedItemVisible (); + ListWrapper source = new (["List Item 1", "List Item two", "List Item Quattro", "Last List Item"]); + Source = source; + + return true; } /// Gets or sets whether this allows items to be marked. @@ -217,10 +202,10 @@ public class ListView : View, IDesignable if (Source is { } && !_allowsMultipleSelection) { - // Clear all selections except selected + // Clear all selections except selected for (var i = 0; i < Source.Count; i++) { - if (Source.IsMarked (i) && i != _selected) + if (Source.IsMarked (i) && SelectedItem.HasValue && i != SelectedItem.Value) { Source.SetMark (i, false); } @@ -231,11 +216,34 @@ public class ListView : View, IDesignable } } + /// + /// Event to raise when an item is added, removed, or moved, or the entire list is refreshed. + /// + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + /// Ensures the selected item is always visible on the screen. + public void EnsureSelectedItemVisible () + { + if (SelectedItem is null) + { + return; + } + + if (SelectedItem < Viewport.Y) + { + Viewport = Viewport with { Y = SelectedItem.Value }; + } + else if (Viewport.Height > 0 && SelectedItem >= Viewport.Y + Viewport.Height) + { + Viewport = Viewport with { Y = SelectedItem.Value - Viewport.Height + 1 }; + } + } + /// /// Gets the that searches the collection as the /// user types. /// - public IListCollectionNavigator KeystrokeNavigator { get; } = new CollectionNavigator(); + public IListCollectionNavigator KeystrokeNavigator { get; } = new CollectionNavigator (); /// Gets or sets the leftmost column that is currently visible (when scrolling horizontally). /// The left position. @@ -244,7 +252,7 @@ public class ListView : View, IDesignable get => Viewport.X; set { - if (_source is null) + if (Source is null) { return; } @@ -259,99 +267,6 @@ public class ListView : View, IDesignable } } - /// Gets the widest item in the list. - public int MaxLength => _source?.Length ?? 0; - - /// Gets or sets the index of the currently selected item. - /// The selected item. - public int SelectedItem - { - get => _selected; - set - { - if (_source is null || _source.Count == 0) - { - return; - } - - if (value < -1 || value >= _source.Count) - { - throw new ArgumentException ("value"); - } - - _selected = value; - OnSelectedChanged (); - } - } - - /// Gets or sets the backing this , enabling custom rendering. - /// The source. - /// Use to set a new source. - public IListDataSource Source - { - get => _source; - set - { - if (_source == value) - { - return; - } - - _source?.Dispose (); - _source = value; - - if (_source is { }) - { - _source.CollectionChanged += Source_CollectionChanged; - } - - SetContentSize (new Size (_source?.Length ?? Viewport.Width, _source?.Count ?? Viewport.Width)); - if (IsInitialized) - { - // Viewport = Viewport with { Y = 0 }; - } - - KeystrokeNavigator.Collection = _source?.ToList (); - _selected = -1; - _lastSelectedItem = -1; - SetNeedsDraw (); - } - } - - - private void Source_CollectionChanged (object sender, NotifyCollectionChangedEventArgs e) - { - SetContentSize (new Size (_source?.Length ?? Viewport.Width, _source?.Count ?? Viewport.Width)); - - if (Source is { Count: > 0 } && _selected > Source.Count - 1) - { - SelectedItem = Source.Count - 1; - } - - SetNeedsDraw (); - - OnCollectionChanged (e); - } - - /// Gets or sets the index of the item that will appear at the top of the . - /// - /// This a helper property for accessing listView.Viewport.Y. - /// - /// The top item. - public int TopItem - { - get => Viewport.Y; - set - { - if (_source is null) - { - return; - } - - Viewport = Viewport with { Y = value }; - } - } - /// /// If and are both , /// marks all items. @@ -367,16 +282,402 @@ public class ListView : View, IDesignable if (AllowsMultipleSelection) { - for (var i = 0; i < Source.Count; i++) + for (var i = 0; i < Source?.Count; i++) { Source.SetMark (i, mark); } + return true; } return false; } + /// Marks the if it is not already marked. + /// if the was marked. + public bool MarkUnmarkSelectedItem () + { + if (Source is null || SelectedItem is null || !UnmarkAllButSelected ()) + { + return false; + } + + Source.SetMark (SelectedItem.Value, !Source.IsMarked (SelectedItem.Value)); + SetNeedsDraw (); + + return Source.IsMarked (SelectedItem.Value); + } + + /// Gets the widest item in the list. + public int MaxLength => Source?.Length ?? 0; + + /// Changes the to the next item in the list, scrolling the list if needed. + /// + public virtual bool MoveDown () + { + if (Source is null || Source.Count == 0) + { + return false; //Nothing for us to move to + } + + if (SelectedItem is null || SelectedItem >= Source.Count) + { + // If SelectedItem is null or for some reason we are currently outside the + // valid values range, we should select the first or bottommost valid value. + // This can occur if the backing data source changes. + SelectedItem = SelectedItem is null ? 0 : Source.Count - 1; + } + else if (SelectedItem + 1 < Source.Count) + { + //can move by down by one. + SelectedItem++; + + if (SelectedItem >= Viewport.Y + Viewport.Height) + { + Viewport = Viewport with { Y = Viewport.Y + 1 }; + } + else if (SelectedItem < Viewport.Y) + { + Viewport = Viewport with { Y = SelectedItem.Value }; + } + } + else if (SelectedItem >= Viewport.Y + Viewport.Height) + { + Viewport = Viewport with { Y = Source.Count - Viewport.Height }; + } + + return true; + } + + /// Changes the to last item in the list, scrolling the list if needed. + /// + public virtual bool MoveEnd () + { + if (Source is { Count: > 0 } && SelectedItem != Source.Count - 1) + { + SelectedItem = Source.Count - 1; + + if (Viewport.Y + SelectedItem > Viewport.Height - 1) + { + Viewport = Viewport with + { + Y = SelectedItem < Viewport.Height - 1 + ? Math.Max (Viewport.Height - SelectedItem.Value + 1, 0) + : Math.Max (SelectedItem.Value - Viewport.Height + 1, 0) + }; + } + } + + return true; + } + + /// Changes the to the first item in the list, scrolling the list if needed. + /// + public virtual bool MoveHome () + { + if (SelectedItem != 0) + { + SelectedItem = 0; + Viewport = Viewport with { Y = SelectedItem.Value }; + } + + return true; + } + + /// + /// Changes the to the item just below the bottom of the visible list, scrolling if + /// needed. + /// + /// + public virtual bool MovePageDown () + { + if (Source is null || Source.Count == 0) + { + return false; + } + + int n = (SelectedItem ?? 0) + Viewport.Height; + + if (n >= Source.Count) + { + n = Source.Count - 1; + } + + if (n != SelectedItem) + { + SelectedItem = n; + + if (Source.Count >= Viewport.Height) + { + Viewport = Viewport with { Y = SelectedItem.Value }; + } + else + { + Viewport = Viewport with { Y = 0 }; + } + } + + return true; + } + + /// Changes the to the item at the top of the visible list. + /// + public virtual bool MovePageUp () + { + if (Source is null || Source.Count == 0) + { + return false; + } + + int n = (SelectedItem ?? 0) - Viewport.Height; + + if (n < 0) + { + n = 0; + } + + if (n != SelectedItem && n < Source?.Count) + { + SelectedItem = n; + Viewport = Viewport with { Y = SelectedItem.Value }; + } + + return true; + } + + /// Changes the to the previous item in the list, scrolling the list if needed. + /// + public virtual bool MoveUp () + { + if (Source is null || Source.Count == 0) + { + return false; //Nothing for us to move to + } + + if (SelectedItem is null || SelectedItem >= Source.Count) + { + // If SelectedItem is null or for some reason we are currently outside the + // valid values range, we should select the bottommost valid value. + // This can occur if the backing data source changes. + SelectedItem = Source.Count - 1; + } + else if (SelectedItem > 0) + { + SelectedItem--; + + if (SelectedItem > Source.Count) + { + SelectedItem = Source.Count - 1; + } + + if (SelectedItem < Viewport.Y) + { + Viewport = Viewport with { Y = SelectedItem.Value }; + } + else if (SelectedItem > Viewport.Y + Viewport.Height) + { + Viewport = Viewport with { Y = SelectedItem.Value - Viewport.Height + 1 }; + } + } + else if (SelectedItem < Viewport.Y) + { + Viewport = Viewport with { Y = SelectedItem.Value }; + } + + return true; + } + + /// Invokes the event if it is defined. + /// if the event was fired. + public bool OnOpenSelectedItem () + { + if (Source is null || SelectedItem is null || Source.Count <= SelectedItem || SelectedItem < 0 || OpenSelectedItem is null) + { + return false; + } + + object? value = Source.ToList () [SelectedItem.Value]; + + OpenSelectedItem?.Invoke (this, new (SelectedItem.Value, value!)); + + // BUGBUG: this should not blindly return true. + return true; + } + + /// Virtual method that will invoke the . + /// + public virtual void OnRowRender (ListViewRowEventArgs rowEventArgs) { RowRender?.Invoke (this, rowEventArgs); } + + + /// This event is raised when the user Double-Clicks on an item or presses ENTER to open the selected item. + public event EventHandler? OpenSelectedItem; + + /// + /// Allow resume the event from being invoked, + /// + public void ResumeSuspendCollectionChangedEvent () + { + if (Source is { }) + { + Source.SuspendCollectionChangedEvent = false; + } + } + + /// This event is invoked when this is being drawn before rendering. + public event EventHandler? RowRender; + + private int? _selectedItem = null; + private int? _lastSelectedItem = null; + + /// Gets or sets the index of the currently selected item. + /// The selected item or null if no item is selected. + public int? SelectedItem + { + get => _selectedItem; + set + { + if (Source is null) + { + return; + } + + if (value.HasValue && (value < 0 || value >= Source.Count)) + { + throw new ArgumentException (@"SelectedItem must be greater than 0 or less than the number of items."); + } + + _selectedItem = value; + OnSelectedChanged (); + SetNeedsDraw (); + } + } + + // TODO: Use standard event model + /// Invokes the event if it is defined. + /// + public virtual bool OnSelectedChanged () + { + if (SelectedItem != _lastSelectedItem) + { + object? value = SelectedItem.HasValue && Source?.Count > 0 ? Source.ToList () [SelectedItem.Value] : null; + SelectedItemChanged?.Invoke (this, new (SelectedItem, value)); + _lastSelectedItem = SelectedItem; + EnsureSelectedItemVisible (); + + return true; + } + + return false; + } + + /// This event is raised when the selected item in the has changed. + public event EventHandler? SelectedItemChanged; + + /// Sets the source of the to an . + /// An object implementing the IList interface. + /// + /// Use the property to set a new source and use custom + /// rendering. + /// + public void SetSource (ObservableCollection? source) + { + if (source is null && Source is not ListWrapper) + { + Source = null; + } + else + { + Source = new ListWrapper (source); + } + } + + /// Sets the source to an value asynchronously. + /// An item implementing the IList interface. + /// + /// Use the property to set a new source and use custom + /// rendering. + /// + public Task SetSourceAsync (ObservableCollection? source) + { + return Task.Factory.StartNew ( + () => + { + if (source is null && Source is not ListWrapper) + { + Source = null; + } + else + { + Source = new ListWrapper (source); + } + + return source; + }, + CancellationToken.None, + TaskCreationOptions.DenyChildAttach, + TaskScheduler.Default + ); + } + + /// Gets or sets the backing this , enabling custom rendering. + /// The source. + /// Use to set a new source. + public IListDataSource? Source + { + get => _source; + set + { + if (_source == value) + { + return; + } + + _source?.Dispose (); + _source = value; + + if (_source is { }) + { + _source.CollectionChanged += Source_CollectionChanged; + SetContentSize (new Size (_source?.Length ?? Viewport.Width, _source?.Count ?? Viewport.Width)); + KeystrokeNavigator.Collection = _source?.ToList (); + } + + SelectedItem = null; + _lastSelectedItem = null; + SetNeedsDraw (); + } + } + + /// + /// Allow suspending the event from being invoked, + /// + public void SuspendCollectionChangedEvent () + { + if (Source is { }) + { + Source.SuspendCollectionChangedEvent = true; + } + } + + /// Gets or sets the index of the item that will appear at the top of the . + /// + /// This a helper property for accessing listView.Viewport.Y. + /// + /// The top item. + public int TopItem + { + get => Viewport.Y; + set + { + if (Source is null) + { + return; + } + + Viewport = Viewport with { Y = value }; + } + } + /// /// If and are both , /// unmarks all marked items other than . @@ -391,9 +692,9 @@ public class ListView : View, IDesignable if (!AllowsMultipleSelection) { - for (var i = 0; i < Source.Count; i++) + for (var i = 0; i < Source?.Count; i++) { - if (Source.IsMarked (i) && i != _selected) + if (Source.IsMarked (i) && i != SelectedItem) { Source.SetMark (i, false); @@ -405,36 +706,121 @@ public class ListView : View, IDesignable return true; } - /// Ensures the selected item is always visible on the screen. - public void EnsureSelectedItemVisible () + /// + protected override void Dispose (bool disposing) { - if (_selected == -1) + Source?.Dispose (); + + base.Dispose (disposing); + } + + /// + /// Call the event to raises the . + /// + /// + protected virtual void OnCollectionChanged (NotifyCollectionChangedEventArgs e) { CollectionChanged?.Invoke (this, e); } + + /// + protected override bool OnDrawingContent () + { + if (Source is null) { - return; + return base.OnDrawingContent (); } - if (_selected < Viewport.Y) + + var current = Attribute.Default; + Move (0, 0); + Rectangle f = Viewport; + int item = Viewport.Y; + bool focused = HasFocus; + int col = _allowsMarking ? 2 : 0; + int start = Viewport.X; + + for (var row = 0; row < f.Height; row++, item++) { - Viewport = Viewport with { Y = _selected }; + bool isSelected = item == SelectedItem; + + Attribute newAttribute = focused ? isSelected ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal) : + isSelected ? GetAttributeForRole (VisualRole.Active) : GetAttributeForRole (VisualRole.Normal); + + if (newAttribute != current) + { + SetAttribute (newAttribute); + current = newAttribute; + } + + Move (0, row); + + if (Source is null || item >= Source.Count) + { + for (var c = 0; c < f.Width; c++) + { + AddRune ((Rune)' '); + } + } + else + { + var rowEventArgs = new ListViewRowEventArgs (item); + OnRowRender (rowEventArgs); + + if (rowEventArgs.RowAttribute is { } && current != rowEventArgs.RowAttribute) + { + current = (Attribute)rowEventArgs.RowAttribute; + SetAttribute (current); + } + + if (_allowsMarking) + { + AddRune ( + Source.IsMarked (item) ? AllowsMultipleSelection ? Glyphs.CheckStateChecked : Glyphs.Selected : + AllowsMultipleSelection ? Glyphs.CheckStateUnChecked : Glyphs.UnSelected + ); + AddRune ((Rune)' '); + } + + Source.Render (this, isSelected, item, col, row, f.Width - col, start); + } } - else if (Viewport.Height > 0 && _selected >= Viewport.Y + Viewport.Height) + + return true; + } + + /// + protected override void OnFrameChanged (in Rectangle frame) { EnsureSelectedItemVisible (); } + + /// + protected override void OnHasFocusChanged (bool newHasFocus, View? currentFocused, View? newFocused) + { + if (newHasFocus && _lastSelectedItem != SelectedItem) { - Viewport = Viewport with { Y = _selected - Viewport.Height + 1 }; + EnsureSelectedItemVisible (); } } - /// Marks the if it is not already marked. - /// if the was marked. - public bool MarkUnmarkSelectedItem () + /// + protected override bool OnKeyDown (Key key) { - if (UnmarkAllButSelected ()) + // If the key was bound to key command, let normal KeyDown processing happen. This enables overriding the default handling. + // See: https://github.com/gui-cs/Terminal.Gui/issues/3950#issuecomment-2807350939 + if (KeyBindings.TryGet (key, out _)) { - Source.SetMark (SelectedItem, !Source.IsMarked (SelectedItem)); - SetNeedsDraw (); - - return Source.IsMarked (SelectedItem); + return false; } - // BUGBUG: Shouldn't this return Source.IsMarked (SelectedItem) + // Enable user to find & select an item by typing text + if (KeystrokeNavigator.Matcher.IsCompatibleKey (key)) + { + int? newItem = KeystrokeNavigator?.GetNextMatchingItem (SelectedItem ?? null, (char)key); + + if (newItem is { } && newItem != -1) + { + SelectedItem = (int)newItem; + EnsureSelectedItemVisible (); + SetNeedsDraw (); + + return true; + } + } return false; } @@ -457,7 +843,7 @@ public class ListView : View, IDesignable SetFocus (); } - if (_source is null) + if (Source is null) { return false; } @@ -496,21 +882,20 @@ public class ListView : View, IDesignable return true; } - if (me.Position.Y + Viewport.Y >= _source.Count + if (me.Position.Y + Viewport.Y >= Source.Count || me.Position.Y + Viewport.Y < 0 || me.Position.Y + Viewport.Y > Viewport.Y + Viewport.Height) { return true; } - _selected = Viewport.Y + me.Position.Y; + SelectedItem = Viewport.Y + me.Position.Y; if (MarkUnmarkSelectedItem ()) { // return true; } - OnSelectedChanged (); SetNeedsDraw (); if (me.Flags == MouseFlags.Button1DoubleClicked) @@ -521,666 +906,20 @@ public class ListView : View, IDesignable return true; } - /// Changes the to the next item in the list, scrolling the list if needed. - /// - public virtual bool MoveDown () - { - if (_source is null || _source.Count == 0) - { - // Do we set lastSelectedItem to -1 here? - return false; //Nothing for us to move to - } - - if (_selected >= _source.Count) - { - // If for some reason we are currently outside of the - // valid values range, we should select the bottommost valid value. - // This can occur if the backing data source changes. - _selected = _source.Count - 1; - OnSelectedChanged (); - SetNeedsDraw (); - } - else if (_selected + 1 < _source.Count) - { - //can move by down by one. - _selected++; - - if (_selected >= Viewport.Y + Viewport.Height) - { - Viewport = Viewport with { Y = Viewport.Y + 1 }; - } - else if (_selected < Viewport.Y) - { - Viewport = Viewport with { Y = _selected }; - } - - OnSelectedChanged (); - SetNeedsDraw (); - } - else if (_selected == 0) - { - OnSelectedChanged (); - SetNeedsDraw (); - } - else if (_selected >= Viewport.Y + Viewport.Height) - { - Viewport = Viewport with { Y = _source.Count - Viewport.Height }; - SetNeedsDraw (); - } - - return true; - } - - /// Changes the to last item in the list, scrolling the list if needed. - /// - public virtual bool MoveEnd () - { - if (_source is { Count: > 0 } && _selected != _source.Count - 1) - { - _selected = _source.Count - 1; - - if (Viewport.Y + _selected > Viewport.Height - 1) - { - Viewport = Viewport with - { - Y = _selected < Viewport.Height - 1 - ? Math.Max (Viewport.Height - _selected + 1, 0) - : Math.Max (_selected - Viewport.Height + 1, 0) - }; - } - - OnSelectedChanged (); - SetNeedsDraw (); - } - - return true; - } - - /// Changes the to the first item in the list, scrolling the list if needed. - /// - public virtual bool MoveHome () - { - if (_selected != 0) - { - _selected = 0; - Viewport = Viewport with { Y = _selected }; - OnSelectedChanged (); - SetNeedsDraw (); - } - - return true; - } - - /// - /// Changes the to the item just below the bottom of the visible list, scrolling if - /// needed. - /// - /// - public virtual bool MovePageDown () - { - if (_source is null) - { - return true; - } - - int n = _selected + Viewport.Height; - - if (n >= _source.Count) - { - n = _source.Count - 1; - } - - if (n != _selected) - { - _selected = n; - - if (_source.Count >= Viewport.Height) - { - Viewport = Viewport with { Y = _selected }; - } - else - { - Viewport = Viewport with { Y = 0 }; - } - - OnSelectedChanged (); - SetNeedsDraw (); - } - - return true; - } - - /// Changes the to the item at the top of the visible list. - /// - public virtual bool MovePageUp () - { - int n = _selected - Viewport.Height; - - if (n < 0) - { - n = 0; - } - - if (n != _selected) - { - _selected = n; - Viewport = Viewport with { Y = _selected }; - OnSelectedChanged (); - SetNeedsDraw (); - } - - return true; - } - - /// Changes the to the previous item in the list, scrolling the list if needed. - /// - public virtual bool MoveUp () - { - if (_source is null || _source.Count == 0) - { - // Do we set lastSelectedItem to -1 here? - return false; //Nothing for us to move to - } - - if (_selected >= _source.Count) - { - // If for some reason we are currently outside of the - // valid values range, we should select the bottommost valid value. - // This can occur if the backing data source changes. - _selected = _source.Count - 1; - OnSelectedChanged (); - SetNeedsDraw (); - } - else if (_selected > 0) - { - _selected--; - - if (_selected > Source.Count) - { - _selected = Source.Count - 1; - } - - if (_selected < Viewport.Y) - { - Viewport = Viewport with { Y = _selected }; - } - else if (_selected > Viewport.Y + Viewport.Height) - { - Viewport = Viewport with { Y = _selected - Viewport.Height + 1 }; - } - - OnSelectedChanged (); - SetNeedsDraw (); - } - else if (_selected < Viewport.Y) - { - Viewport = Viewport with { Y = _selected }; - SetNeedsDraw (); - } - - return true; - } - /// - protected override bool OnDrawingContent () + protected override void OnViewportChanged (DrawEventArgs e) { SetContentSize (new Size (MaxLength, Source?.Count ?? Viewport.Height)); } + + private void Source_CollectionChanged (object? sender, NotifyCollectionChangedEventArgs e) { - Attribute current = Attribute.Default; - Move (0, 0); - Rectangle f = Viewport; - int item = Viewport.Y; - bool focused = HasFocus; - int col = _allowsMarking ? 2 : 0; - int start = Viewport.X; + SetContentSize (new Size (Source?.Length ?? Viewport.Width, Source?.Count ?? Viewport.Width)); - for (var row = 0; row < f.Height; row++, item++) + if (Source is { Count: > 0 } && SelectedItem.HasValue && SelectedItem > Source.Count - 1) { - bool isSelected = item == _selected; - - Attribute newAttribute = focused ? isSelected ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal) : - isSelected ? GetAttributeForRole (VisualRole.Active) : GetAttributeForRole (VisualRole.Normal); - - if (newAttribute != current) - { - SetAttribute (newAttribute); - current = newAttribute; - } - - Move (0, row); - - if (_source is null || item >= _source.Count) - { - for (var c = 0; c < f.Width; c++) - { - AddRune ((Rune)' '); - } - } - else - { - var rowEventArgs = new ListViewRowEventArgs (item); - OnRowRender (rowEventArgs); - - if (rowEventArgs.RowAttribute is { } && current != rowEventArgs.RowAttribute) - { - current = (Attribute)rowEventArgs.RowAttribute; - SetAttribute (current); - } - - if (_allowsMarking) - { - AddRune ( - _source.IsMarked (item) ? AllowsMultipleSelection ? Glyphs.CheckStateChecked : Glyphs.Selected : - AllowsMultipleSelection ? Glyphs.CheckStateUnChecked : Glyphs.UnSelected - ); - AddRune ((Rune)' '); - } - - Source.Render (this, isSelected, item, col, row, f.Width - col, start); - } - } - return true; - } - - /// - protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View currentFocused, [CanBeNull] View newFocused) - { - if (newHasFocus && _lastSelectedItem != _selected) - { - EnsureSelectedItemVisible (); - } - } - - /// Invokes the event if it is defined. - /// if the event was fired. - public bool OnOpenSelectedItem () - { - if (_source is null || _source.Count <= _selected || _selected < 0 || OpenSelectedItem is null) - { - return false; + SelectedItem = Source.Count - 1; } - object value = _source.ToList () [_selected]; + SetNeedsDraw (); - OpenSelectedItem?.Invoke (this, new ListViewItemEventArgs (_selected, value)); - - // BUGBUG: this should not blindly return true. - return true; - } - - /// - protected override bool OnKeyDown (Key key) - { - // If the key was bound to key command, let normal KeyDown processing happen. This enables overriding the default handling. - // See: https://github.com/gui-cs/Terminal.Gui/issues/3950#issuecomment-2807350939 - if (KeyBindings.TryGet (key, out _)) - { - return false; - } - - // Enable user to find & select an item by typing text - if (KeystrokeNavigator.Matcher.IsCompatibleKey (key)) - { - int? newItem = KeystrokeNavigator?.GetNextMatchingItem (SelectedItem, (char)key); - - if (newItem is { } && newItem != -1) - { - SelectedItem = (int)newItem; - EnsureSelectedItemVisible (); - SetNeedsDraw (); - - return true; - } - } - - return false; - } - - /// Virtual method that will invoke the . - /// - public virtual void OnRowRender (ListViewRowEventArgs rowEventArgs) { RowRender?.Invoke (this, rowEventArgs); } - - // TODO: Use standard event model - /// Invokes the event if it is defined. - /// - public virtual bool OnSelectedChanged () - { - if (_selected != _lastSelectedItem) - { - object value = _source?.Count > 0 ? _source.ToList () [_selected] : null; - SelectedItemChanged?.Invoke (this, new ListViewItemEventArgs (_selected, value)); - _lastSelectedItem = _selected; - EnsureSelectedItemVisible (); - - return true; - } - - return false; - } - - /// This event is raised when the user Double-Clicks on an item or presses ENTER to open the selected item. - public event EventHandler OpenSelectedItem; - - /// This event is invoked when this is being drawn before rendering. - public event EventHandler RowRender; - - /// This event is raised when the selected item in the has changed. - public event EventHandler SelectedItemChanged; - - /// - /// Event to raise when an item is added, removed, or moved, or the entire list is refreshed. - /// - public event NotifyCollectionChangedEventHandler CollectionChanged; - - /// Sets the source of the to an . - /// An object implementing the IList interface. - /// - /// Use the property to set a new source and use custom - /// rendering. - /// - public void SetSource (ObservableCollection source) - { - if (source is null && Source is not ListWrapper) - { - Source = null; - } - else - { - Source = new ListWrapper (source); - } - } - - /// Sets the source to an value asynchronously. - /// An item implementing the IList interface. - /// - /// Use the property to set a new source and use custom - /// rendering. - /// - public Task SetSourceAsync (ObservableCollection source) - { - return Task.Factory.StartNew ( - () => - { - if (source is null && (Source is null || !(Source is ListWrapper))) - { - Source = null; - } - else - { - Source = new ListWrapper (source); - } - - return source; - }, - CancellationToken.None, - TaskCreationOptions.DenyChildAttach, - TaskScheduler.Default - ); - } - - private void ListView_LayoutStarted (object sender, LayoutEventArgs e) { EnsureSelectedItemVisible (); } - /// - /// Call the event to raises the . - /// - /// - protected virtual void OnCollectionChanged (NotifyCollectionChangedEventArgs e) { CollectionChanged?.Invoke (this, e); } - - /// - protected override void Dispose (bool disposing) - { - _source?.Dispose (); - - base.Dispose (disposing); - } - - /// - /// Allow suspending the event from being invoked, - /// - public void SuspendCollectionChangedEvent () - { - if (Source is { }) - { - Source.SuspendCollectionChangedEvent = true; - } - } - - /// - /// Allow resume the event from being invoked, - /// - public void ResumeSuspendCollectionChangedEvent () - { - if (Source is { }) - { - Source.SuspendCollectionChangedEvent = false; - } - } - - /// - public bool EnableForDesign () - { - var source = new ListWrapper (["List Item 1", "List Item two", "List Item Quattro", "Last List Item"]); - Source = source; - - return true; - } -} - -/// -/// Provides a default implementation of that renders items -/// using . -/// -public class ListWrapper : IListDataSource, IDisposable -{ - private int _count; - private BitArray _marks; - private readonly ObservableCollection _source; - - /// - public ListWrapper (ObservableCollection source) - { - if (source is { }) - { - _count = source.Count; - _marks = new BitArray (_count); - _source = source; - _source.CollectionChanged += Source_CollectionChanged; - Length = GetMaxLengthItem (); - } - } - - private void Source_CollectionChanged (object sender, NotifyCollectionChangedEventArgs e) - { - if (!SuspendCollectionChangedEvent) - { - CheckAndResizeMarksIfRequired (); - CollectionChanged?.Invoke (sender, e); - } - } - - /// - public event NotifyCollectionChangedEventHandler CollectionChanged; - - /// - public int Count => _source?.Count ?? 0; - - /// - public int Length { get; private set; } - - private bool _suspendCollectionChangedEvent; - - /// - public bool SuspendCollectionChangedEvent - { - get => _suspendCollectionChangedEvent; - set - { - _suspendCollectionChangedEvent = value; - - if (!_suspendCollectionChangedEvent) - { - CheckAndResizeMarksIfRequired (); - } - } - } - - private void CheckAndResizeMarksIfRequired () - { - if (_source != null && _count != _source.Count) - { - _count = _source.Count; - BitArray newMarks = new BitArray (_count); - for (var i = 0; i < Math.Min (_marks.Length, newMarks.Length); i++) - { - newMarks [i] = _marks [i]; - } - _marks = newMarks; - - Length = GetMaxLengthItem (); - } - } - - /// - public void Render ( - ListView container, - bool marked, - int item, - int col, - int line, - int width, - int start = 0 - ) - { - container.Move (Math.Max (col - start, 0), line); - - if (_source is { }) - { - object t = _source [item]; - - if (t is null) - { - RenderUstr (container, "", col, line, width); - } - else - { - if (t is string s) - { - RenderUstr (container, s, col, line, width, start); - } - else - { - RenderUstr (container, t.ToString (), col, line, width, start); - } - } - } - } - - /// - public bool IsMarked (int item) - { - if (item >= 0 && item < _count) - { - return _marks [item]; - } - - return false; - } - - /// - public void SetMark (int item, bool value) - { - if (item >= 0 && item < _count) - { - _marks [item] = value; - } - } - - /// - public IList ToList () { return _source; } - - /// - public int StartsWith (string search) - { - if (_source is null || _source?.Count == 0) - { - return -1; - } - - for (var i = 0; i < _source.Count; i++) - { - object t = _source [i]; - - if (t is string u) - { - if (u.ToUpper ().StartsWith (search.ToUpperInvariant ())) - { - return i; - } - } - else if (t is string s) - { - if (s.StartsWith (search, StringComparison.InvariantCultureIgnoreCase)) - { - return i; - } - } - } - - return -1; - } - - private int GetMaxLengthItem () - { - if (_source is null || _source?.Count == 0) - { - return 0; - } - - var maxLength = 0; - - for (var i = 0; i < _source!.Count; i++) - { - object t = _source [i]; - int l; - - if (t is string u) - { - l = u.GetColumns (); - } - else if (t is string s) - { - l = s.Length; - } - else - { - l = t.ToString ().Length; - } - - if (l > maxLength) - { - maxLength = l; - } - } - - return maxLength; - } - - private void RenderUstr (View driver, string ustr, int col, int line, int width, int start = 0) - { - string str = start > ustr.GetColumns () ? string.Empty : ustr.Substring (Math.Min (start, ustr.ToRunes ().Length - 1)); - string u = TextFormatter.ClipAndJustify (str, width, Alignment.Start); - driver.AddStr (u); - width -= u.GetColumns (); - - while (width-- > 0) - { - driver.AddRune ((Rune)' '); - } - } - - /// - public void Dispose () - { - if (_source is { }) - { - _source.CollectionChanged -= Source_CollectionChanged; - } + OnCollectionChanged (e); } } diff --git a/Terminal.Gui/Views/ListViewEventArgs.cs b/Terminal.Gui/Views/ListViewEventArgs.cs index 7a718b536..e7a8c2686 100644 --- a/Terminal.Gui/Views/ListViewEventArgs.cs +++ b/Terminal.Gui/Views/ListViewEventArgs.cs @@ -1,5 +1,4 @@ -#nullable disable -namespace Terminal.Gui.Views; +namespace Terminal.Gui.Views; /// for events. public class ListViewItemEventArgs : EventArgs @@ -7,17 +6,17 @@ public class ListViewItemEventArgs : EventArgs /// Initializes a new instance of /// The index of the item. /// The item - public ListViewItemEventArgs (int item, object value) + public ListViewItemEventArgs (int? item, object? value) { Item = item; Value = value; } /// The index of the item. - public int Item { get; } + public int? Item { get; } /// The item. - public object Value { get; } + public object? Value { get; } } /// used by the event. diff --git a/Terminal.Gui/Views/ListWrapper.cs b/Terminal.Gui/Views/ListWrapper.cs new file mode 100644 index 000000000..5f10b4e06 --- /dev/null +++ b/Terminal.Gui/Views/ListWrapper.cs @@ -0,0 +1,256 @@ +#nullable enable +using System.Collections; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace Terminal.Gui.Views; + +/// +/// Provides a default implementation of that renders items +/// using . +/// +public class ListWrapper : IListDataSource, IDisposable +{ + /// + /// Creates a new instance of that wraps the specified + /// . + /// + /// + public ListWrapper (ObservableCollection? source) + { + if (source is { }) + { + _count = source.Count; + _marks = new (_count); + _source = source; + _source.CollectionChanged += Source_CollectionChanged; + Length = GetMaxLengthItem (); + } + } + + private readonly ObservableCollection? _source; + private int _count; + private BitArray? _marks; + + private bool _suspendCollectionChangedEvent; + + /// + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + /// + public int Count => _source?.Count ?? 0; + + /// + public int Length { get; private set; } + + /// + public bool SuspendCollectionChangedEvent + { + get => _suspendCollectionChangedEvent; + set + { + _suspendCollectionChangedEvent = value; + + if (!_suspendCollectionChangedEvent) + { + CheckAndResizeMarksIfRequired (); + } + } + } + + /// + public void Render ( + ListView container, + bool marked, + int item, + int col, + int line, + int width, + int viewportX = 0 + ) + { + container.Move (Math.Max (col - viewportX, 0), line); + + if (_source is null) + { + return; + } + + object? t = _source [item]; + + if (t is null) + { + RenderString (container, "", col, line, width); + } + else + { + if (t is string s) + { + RenderString (container, s, col, line, width, viewportX); + } + else + { + RenderString (container, t.ToString ()!, col, line, width, viewportX); + } + } + } + + /// + public bool IsMarked (int item) + { + if (item >= 0 && item < _count) + { + return _marks! [item]; + } + + return false; + } + + /// + public void SetMark (int item, bool value) + { + if (item >= 0 && item < _count) + { + _marks! [item] = value; + } + } + + /// + public IList ToList () { return _source ?? []; } + + /// + public void Dispose () + { + if (_source is { }) + { + _source.CollectionChanged -= Source_CollectionChanged; + } + } + + /// + /// INTERNAL: Searches the underlying collection for the first string element that starts with the specified search value, + /// using a case-insensitive comparison. + /// + /// + /// The comparison is performed in a case-insensitive manner using invariant culture rules. Only + /// elements of type string are considered; other types in the collection are ignored. + /// + /// + /// The string value to compare against the start of each string element in the collection. Cannot be + /// null. + /// + /// + /// The zero-based index of the first matching string element if found; otherwise, -1 if no match is found or the + /// collection is empty. + /// + internal int StartsWith (string search) + { + if (_source is null || _source?.Count == 0) + { + return -1; + } + + for (var i = 0; i < _source!.Count; i++) + { + object? t = _source [i]; + + if (t is string u) + { + if (u.ToUpper ().StartsWith (search.ToUpperInvariant ())) + { + return i; + } + } + else if (t is string s && s.StartsWith (search, StringComparison.InvariantCultureIgnoreCase)) + { + return i; + } + } + + return -1; + } + + private void CheckAndResizeMarksIfRequired () + { + if (_source != null && _count != _source.Count && _marks is { }) + { + _count = _source.Count; + var newMarks = new BitArray (_count); + + for (var i = 0; i < Math.Min (_marks.Length, newMarks.Length); i++) + { + newMarks [i] = _marks [i]; + } + + _marks = newMarks; + + Length = GetMaxLengthItem (); + } + } + + private int GetMaxLengthItem () + { + if (_source is null || _source?.Count == 0) + { + return 0; + } + + var maxLength = 0; + + for (var i = 0; i < _source!.Count; i++) + { + object? t = _source [i]; + + if (t is null) + { + continue; + } + + int l; + + l = t is string u ? u.GetColumns () : t.ToString ()!.Length; + + if (l > maxLength) + { + maxLength = l; + } + } + + return maxLength; + } + + private static void RenderString (View driver, string str, int col, int line, int width, int viewportX = 0) + { + if (string.IsNullOrEmpty (str) || viewportX >= str.GetColumns ()) + { + // Empty string or viewport beyond string - just fill with spaces + for (var i = 0; i < width; i++) + { + driver.AddRune ((Rune)' '); + } + + return; + } + + int runeLength = str.ToRunes ().Length; + int startIndex = Math.Min (viewportX, Math.Max (0, runeLength - 1)); + string substring = str.Substring (startIndex); + string u = TextFormatter.ClipAndJustify (substring, width, Alignment.Start); + driver.AddStr (u); + width -= u.GetColumns (); + + while (width-- > 0) + { + driver.AddRune ((Rune)' '); + } + } + + private void Source_CollectionChanged (object? sender, NotifyCollectionChangedEventArgs e) + { + if (!SuspendCollectionChangedEvent) + { + CheckAndResizeMarksIfRequired (); + CollectionChanged?.Invoke (sender, e); + } + } +} diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index f09532ed2..1c5f298e0 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -1607,11 +1607,11 @@ public class TableView : View, IDesignable return false; } - int match = CollectionNavigator.GetNextMatchingItem (row, (char)key); + int? match = CollectionNavigator.GetNextMatchingItem (row, (char)key); - if (match != -1) + if (match != null) { - SelectedRow = match; + SelectedRow = match.Value; EnsureValidSelection (); EnsureSelectedCellIsVisible (); SetNeedsDraw (); diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index 9f3854f7e..fedc501f4 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -82,7 +82,7 @@ public partial class Toplevel : View // TODO: IRunnable: Re-implement as a property on IRunnable /// Gets or sets whether the main loop for this is running or not. - /// Setting this property directly is discouraged. Use instead. + /// Setting this property directly is discouraged. Use instead. public bool Running { get; set; } // TODO: IRunnable: Re-implement in IRunnable diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings index 4161569b1..ef25662a4 100644 --- a/Terminal.sln.DotSettings +++ b/Terminal.sln.DotSettings @@ -421,6 +421,7 @@ True True True + True True True True diff --git a/Tests/IntegrationTests/UICatalog/ScenarioTests.cs b/Tests/IntegrationTests/UICatalog/ScenarioTests.cs index 69021ab15..39376e87b 100644 --- a/Tests/IntegrationTests/UICatalog/ScenarioTests.cs +++ b/Tests/IntegrationTests/UICatalog/ScenarioTests.cs @@ -317,7 +317,7 @@ public class ScenarioTests : TestsAllViews hostPane.FillRect (hostPane.Viewport); } - curView = CreateClass (viewClasses.Values.ToArray () [classListView.SelectedItem]); + curView = CreateClass (viewClasses.Values.ToArray () [classListView.SelectedItem!.Value]); }; xOptionSelector.ValueChanged += (_, _) => DimPosChanged (curView); @@ -404,7 +404,7 @@ public class ScenarioTests : TestsAllViews { Assert.Equal ( curView.GetType ().Name, - viewClasses.Values.ToArray () [classListView.SelectedItem].Name); + viewClasses.Values.ToArray () [classListView.SelectedItem!.Value].Name); } } else diff --git a/Tests/TerminalGuiFluentTesting/GuiTestContext.ContextMenu.cs b/Tests/TerminalGuiFluentTesting/GuiTestContext.ContextMenu.cs index 359fd7a0a..136d4d085 100644 --- a/Tests/TerminalGuiFluentTesting/GuiTestContext.ContextMenu.cs +++ b/Tests/TerminalGuiFluentTesting/GuiTestContext.ContextMenu.cs @@ -24,7 +24,7 @@ public partial class GuiTestContext { // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused // and the context menu is disposed when it is closed. - App.Popover?.Register (contextMenu); + App?.Popover?.Register (contextMenu); contextMenu?.MakeVisible (e.ScreenPosition); } }; diff --git a/Tests/TerminalGuiFluentTesting/GuiTestContext.ViewBase.cs b/Tests/TerminalGuiFluentTesting/GuiTestContext.ViewBase.cs index 74eafc77e..f96505840 100644 --- a/Tests/TerminalGuiFluentTesting/GuiTestContext.ViewBase.cs +++ b/Tests/TerminalGuiFluentTesting/GuiTestContext.ViewBase.cs @@ -28,11 +28,11 @@ public partial class GuiTestContext /// /// The last view added (e.g. with ) or the root/current top. /// - public View LastView => _lastView ?? App.Current ?? throw new ("Could not determine which view to add to"); + public View LastView => _lastView ?? App?.Current ?? throw new ("Could not determine which view to add to"); private T Find (Func evaluator) where T : View { - Toplevel? t = App.Current; + Toplevel? t = App?.Current; if (t == null) { diff --git a/Tests/UnitTests/SetupFakeApplicationAttribute.cs b/Tests/UnitTests/SetupFakeApplicationAttribute.cs index 0b8633da7..06d338436 100644 --- a/Tests/UnitTests/SetupFakeApplicationAttribute.cs +++ b/Tests/UnitTests/SetupFakeApplicationAttribute.cs @@ -32,7 +32,10 @@ public class SetupFakeApplicationAttribute : BeforeAfterTestAttribute _appDispose?.Dispose (); _appDispose = null; - ApplicationImpl.SetInstance (null); + + // TODO: This is troublesome; it seems to cause tests to hang when enabled, but shouldn't have any impact. + // TODO: Uncomment after investigation. + //ApplicationImpl.SetInstance (null); base.After (methodUnderTest); } diff --git a/Tests/UnitTests/Views/ListViewTests.cs b/Tests/UnitTests/Views/ListViewTests.cs deleted file mode 100644 index d42827760..000000000 --- a/Tests/UnitTests/Views/ListViewTests.cs +++ /dev/null @@ -1,1225 +0,0 @@ -using System.Collections; -using System.Collections.ObjectModel; -using System.Collections.Specialized; -using Moq; -using UnitTests; -using Xunit.Abstractions; - -namespace UnitTests.ViewsTests; - -public class ListViewTests (ITestOutputHelper output) -{ - [Fact] - public void Constructors_Defaults () - { - var lv = new ListView (); - Assert.Null (lv.Source); - Assert.True (lv.CanFocus); - Assert.Equal (-1, lv.SelectedItem); - Assert.False (lv.AllowsMultipleSelection); - - lv = new () { Source = new ListWrapper (["One", "Two", "Three"]) }; - Assert.NotNull (lv.Source); - Assert.Equal (-1, lv.SelectedItem); - - lv = new () { Source = new NewListDataSource () }; - Assert.NotNull (lv.Source); - Assert.Equal (-1, lv.SelectedItem); - - lv = new () - { - Y = 1, Width = 10, Height = 20, Source = new ListWrapper (["One", "Two", "Three"]) - }; - Assert.NotNull (lv.Source); - Assert.Equal (-1, lv.SelectedItem); - Assert.Equal (new (0, 1, 10, 20), lv.Frame); - - lv = new () { Y = 1, Width = 10, Height = 20, Source = new NewListDataSource () }; - Assert.NotNull (lv.Source); - Assert.Equal (-1, lv.SelectedItem); - Assert.Equal (new (0, 1, 10, 20), lv.Frame); - - } - - [Fact] - [AutoInitShutdown] - public void Ensures_Visibility_SelectedItem_On_MoveDown_And_MoveUp () - { - ObservableCollection source = []; - - for (var i = 0; i < 20; i++) - { - source.Add ($"Line{i}"); - } - - var lv = new ListView { Width = Dim.Fill (), Height = Dim.Fill (), Source = new ListWrapper (source) }; - var win = new Window (); - win.Add (lv); - var top = new Toplevel (); - top.Add (win); - SessionToken rs = Application.Begin (top); - Application.Driver!.SetScreenSize (12, 12); - AutoInitShutdownAttribute.RunIteration (); - - Assert.Equal (-1, lv.SelectedItem); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────┐ -│Line0 │ -│Line1 │ -│Line2 │ -│Line3 │ -│Line4 │ -│Line5 │ -│Line6 │ -│Line7 │ -│Line8 │ -│Line9 │ -└──────────┘", - output - ); - - Assert.True (lv.ScrollVertical (10)); - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (-1, lv.SelectedItem); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────┐ -│Line10 │ -│Line11 │ -│Line12 │ -│Line13 │ -│Line14 │ -│Line15 │ -│Line16 │ -│Line17 │ -│Line18 │ -│Line19 │ -└──────────┘", - output - ); - - Assert.True (lv.MoveDown ()); - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (0, lv.SelectedItem); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────┐ -│Line0 │ -│Line1 │ -│Line2 │ -│Line3 │ -│Line4 │ -│Line5 │ -│Line6 │ -│Line7 │ -│Line8 │ -│Line9 │ -└──────────┘", - output - ); - - Assert.True (lv.MoveEnd ()); - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (19, lv.SelectedItem); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────┐ -│Line10 │ -│Line11 │ -│Line12 │ -│Line13 │ -│Line14 │ -│Line15 │ -│Line16 │ -│Line17 │ -│Line18 │ -│Line19 │ -└──────────┘", - output - ); - - Assert.True (lv.ScrollVertical (-20)); - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (19, lv.SelectedItem); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────┐ -│Line0 │ -│Line1 │ -│Line2 │ -│Line3 │ -│Line4 │ -│Line5 │ -│Line6 │ -│Line7 │ -│Line8 │ -│Line9 │ -└──────────┘", - output - ); - - Assert.True (lv.MoveDown ()); - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (19, lv.SelectedItem); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────┐ -│Line10 │ -│Line11 │ -│Line12 │ -│Line13 │ -│Line14 │ -│Line15 │ -│Line16 │ -│Line17 │ -│Line18 │ -│Line19 │ -└──────────┘", - output - ); - - Assert.True (lv.ScrollVertical (-20)); - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (19, lv.SelectedItem); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────┐ -│Line0 │ -│Line1 │ -│Line2 │ -│Line3 │ -│Line4 │ -│Line5 │ -│Line6 │ -│Line7 │ -│Line8 │ -│Line9 │ -└──────────┘", - output - ); - - Assert.True (lv.MoveDown ()); - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (19, lv.SelectedItem); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────┐ -│Line10 │ -│Line11 │ -│Line12 │ -│Line13 │ -│Line14 │ -│Line15 │ -│Line16 │ -│Line17 │ -│Line18 │ -│Line19 │ -└──────────┘", - output - ); - - Assert.True (lv.MoveHome ()); - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (0, lv.SelectedItem); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────┐ -│Line0 │ -│Line1 │ -│Line2 │ -│Line3 │ -│Line4 │ -│Line5 │ -│Line6 │ -│Line7 │ -│Line8 │ -│Line9 │ -└──────────┘", - output - ); - - Assert.True (lv.ScrollVertical (20)); - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (0, lv.SelectedItem); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────┐ -│Line19 │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────┘", - output - ); - - Assert.True (lv.MoveUp ()); - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (0, lv.SelectedItem); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────┐ -│Line0 │ -│Line1 │ -│Line2 │ -│Line3 │ -│Line4 │ -│Line5 │ -│Line6 │ -│Line7 │ -│Line8 │ -│Line9 │ -└──────────┘", - output - ); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void EnsureSelectedItemVisible_SelectedItem () - { - ObservableCollection source = []; - - for (var i = 0; i < 10; i++) - { - source.Add ($"Item {i}"); - } - - var lv = new ListView { Width = 10, Height = 5, Source = new ListWrapper (source) }; - var top = new Toplevel (); - top.Add (lv); - Application.Begin (top); - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -Item 0 -Item 1 -Item 2 -Item 3 -Item 4", - output - ); - - // EnsureSelectedItemVisible is auto enabled on the OnSelectedChanged - lv.SelectedItem = 6; - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -Item 2 -Item 3 -Item 4 -Item 5 -Item 6", - output - ); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void EnsureSelectedItemVisible_Top () - { - ObservableCollection source = ["First", "Second"]; - var lv = new ListView { Width = Dim.Fill (), Height = 1, Source = new ListWrapper (source) }; - lv.SelectedItem = 1; - var top = new Toplevel (); - top.Add (lv); - Application.Begin (top); - AutoInitShutdownAttribute.RunIteration (); - - Assert.Equal ("Second ", GetContents (0)); - Assert.Equal (new (' ', 7), GetContents (1)); - - lv.MoveUp (); - lv.Draw (); - - Assert.Equal ("First ", GetContents (0)); - Assert.Equal (new (' ', 7), GetContents (1)); - - string GetContents (int line) - { - var item = ""; - - for (var i = 0; i < 7; i++) - { - item += Application.Driver?.Contents [line, i].Rune; - } - - return item; - } - top.Dispose (); - } - - [Fact] - public void KeyBindings_Command () - { - ObservableCollection source = ["One", "Two", "Three"]; - var lv = new ListView { Height = 2, AllowsMarking = true, Source = new ListWrapper (source) }; - lv.BeginInit (); - lv.EndInit (); - Assert.Equal (-1, lv.SelectedItem); - Assert.True (lv.NewKeyDownEvent (Key.CursorDown)); - Assert.Equal (0, lv.SelectedItem); - Assert.True (lv.NewKeyDownEvent (Key.CursorUp)); - Assert.Equal (0, lv.SelectedItem); - Assert.True (lv.NewKeyDownEvent (Key.PageDown)); - Assert.Equal (2, lv.SelectedItem); - Assert.Equal (2, lv.TopItem); - Assert.True (lv.NewKeyDownEvent (Key.PageUp)); - Assert.Equal (0, lv.SelectedItem); - Assert.Equal (0, lv.TopItem); - Assert.False (lv.Source.IsMarked (lv.SelectedItem)); - Assert.True (lv.NewKeyDownEvent (Key.Space)); - Assert.True (lv.Source.IsMarked (lv.SelectedItem)); - var opened = false; - lv.OpenSelectedItem += (s, _) => opened = true; - Assert.True (lv.NewKeyDownEvent (Key.Enter)); - Assert.True (opened); - Assert.True (lv.NewKeyDownEvent (Key.End)); - Assert.Equal (2, lv.SelectedItem); - Assert.True (lv.NewKeyDownEvent (Key.Home)); - Assert.Equal (0, lv.SelectedItem); - } - - [Fact] - public void HotKey_Command_SetsFocus () - { - var view = new ListView (); - - view.CanFocus = true; - Assert.False (view.HasFocus); - view.InvokeCommand (Command.HotKey); - Assert.True (view.HasFocus); - } - - [Fact] - public void HotKey_Command_Does_Not_Accept () - { - var listView = new ListView (); - var accepted = false; - - listView.Accepting += OnAccepted; - listView.InvokeCommand (Command.HotKey); - - Assert.False (accepted); - - return; - - void OnAccepted (object sender, CommandEventArgs e) { accepted = true; } - } - - [Fact] - public void Accept_Command_Accepts_and_Opens_Selected_Item () - { - ObservableCollection source = ["One", "Two", "Three"]; - var listView = new ListView { Source = new ListWrapper (source) }; - listView.SelectedItem = 0; - - var accepted = false; - var opened = false; - var selectedValue = string.Empty; - - listView.Accepting += Accepted; - listView.OpenSelectedItem += OpenSelectedItem; - - listView.InvokeCommand (Command.Accept); - - Assert.True (accepted); - Assert.True (opened); - Assert.Equal (source [0], selectedValue); - - return; - - void OpenSelectedItem (object sender, ListViewItemEventArgs e) - { - opened = true; - selectedValue = e.Value.ToString (); - } - - void Accepted (object sender, CommandEventArgs e) { accepted = true; } - } - - [Fact] - public void Accept_Cancel_Event_Prevents_OpenSelectedItem () - { - ObservableCollection source = ["One", "Two", "Three"]; - var listView = new ListView { Source = new ListWrapper (source) }; - listView.SelectedItem = 0; - - var accepted = false; - var opened = false; - var selectedValue = string.Empty; - - listView.Accepting += Accepted; - listView.OpenSelectedItem += OpenSelectedItem; - - listView.InvokeCommand (Command.Accept); - - Assert.True (accepted); - Assert.False (opened); - Assert.Equal (string.Empty, selectedValue); - - return; - - void OpenSelectedItem (object sender, ListViewItemEventArgs e) - { - opened = true; - selectedValue = e.Value.ToString (); - } - - void Accepted (object sender, CommandEventArgs e) - { - accepted = true; - e.Handled = true; - } - } - - /// - /// Tests that when none of the Commands in a chained keybinding are possible the - /// returns the appropriate result - /// - [Fact] - public void ListViewProcessKeyReturnValue_WithMultipleCommands () - { - var lv = new ListView { Source = new ListWrapper (["One", "Two", "Three", "Four"]) }; - - Assert.NotNull (lv.Source); - - // first item should be deselected by default - Assert.Equal (-1, lv.SelectedItem); - - // bind shift down to move down twice in control - lv.KeyBindings.Add (Key.CursorDown.WithShift, Command.Down, Command.Down); - - Key ev = Key.CursorDown.WithShift; - - Assert.True (lv.NewKeyDownEvent (ev), "The first time we move down 2 it should be possible"); - - // After moving down twice from -1 we should be at 'Two' - Assert.Equal (1, lv.SelectedItem); - - // clear the items - lv.SetSource (null); - - // Press key combo again - return should be false this time as none of the Commands are allowable - Assert.False (lv.NewKeyDownEvent (ev), "We cannot move down so will not respond to this"); - } - - [Fact] - public void AllowsMarking_True_SpaceWithShift_SelectsThenDown_SingleSelection () - { - var lv = new ListView { Source = new ListWrapper (["One", "Two", "Three"]) }; - lv.AllowsMarking = true; - lv.AllowsMultipleSelection = false; - - Assert.NotNull (lv.Source); - - // first item should be deselected by default - Assert.Equal (-1, lv.SelectedItem); - - // nothing is ticked - Assert.False (lv.Source.IsMarked (0)); - Assert.False (lv.Source.IsMarked (1)); - Assert.False (lv.Source.IsMarked (2)); - - // view should indicate that it has accepted and consumed the event - Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); - - // first item should now be selected - Assert.Equal (0, lv.SelectedItem); - - // none of the items should be ticked - Assert.False (lv.Source.IsMarked (0)); - Assert.False (lv.Source.IsMarked (1)); - Assert.False (lv.Source.IsMarked (2)); - - // Press key combo again - Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); - - // second item should now be selected - Assert.Equal (1, lv.SelectedItem); - - // first item only should be ticked - Assert.True (lv.Source.IsMarked (0)); - Assert.False (lv.Source.IsMarked (1)); - Assert.False (lv.Source.IsMarked (2)); - - // Press key combo again - Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); - Assert.Equal (2, lv.SelectedItem); - Assert.False (lv.Source.IsMarked (0)); - Assert.True (lv.Source.IsMarked (1)); - Assert.False (lv.Source.IsMarked (2)); - - // Press key combo again - Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); - Assert.Equal (2, lv.SelectedItem); // cannot move down any further - Assert.False (lv.Source.IsMarked (0)); - Assert.False (lv.Source.IsMarked (1)); - Assert.True (lv.Source.IsMarked (2)); // but can toggle marked - - // Press key combo again - Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); - Assert.Equal (2, lv.SelectedItem); // cannot move down any further - Assert.False (lv.Source.IsMarked (0)); - Assert.False (lv.Source.IsMarked (1)); - Assert.False (lv.Source.IsMarked (2)); // untoggle toggle marked - } - - [Fact] - public void AllowsMarking_True_SpaceWithShift_SelectsThenDown_MultipleSelection () - { - var lv = new ListView { Source = new ListWrapper (["One", "Two", "Three"]) }; - lv.AllowsMarking = true; - lv.AllowsMultipleSelection = true; - - Assert.NotNull (lv.Source); - - // first item should be deselected by default - Assert.Equal (-1, lv.SelectedItem); - - // nothing is ticked - Assert.False (lv.Source.IsMarked (0)); - Assert.False (lv.Source.IsMarked (1)); - Assert.False (lv.Source.IsMarked (2)); - - // view should indicate that it has accepted and consumed the event - Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); - - // first item should now be selected - Assert.Equal (0, lv.SelectedItem); - - // none of the items should be ticked - Assert.False (lv.Source.IsMarked (0)); - Assert.False (lv.Source.IsMarked (1)); - Assert.False (lv.Source.IsMarked (2)); - - // Press key combo again - Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); - - // second item should now be selected - Assert.Equal (1, lv.SelectedItem); - - // first item only should be ticked - Assert.True (lv.Source.IsMarked (0)); - Assert.False (lv.Source.IsMarked (1)); - Assert.False (lv.Source.IsMarked (2)); - - // Press key combo again - Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); - Assert.Equal (2, lv.SelectedItem); - Assert.True (lv.Source.IsMarked (0)); - Assert.True (lv.Source.IsMarked (1)); - Assert.False (lv.Source.IsMarked (2)); - - // Press key combo again - Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); - Assert.Equal (2, lv.SelectedItem); // cannot move down any further - Assert.True (lv.Source.IsMarked (0)); - Assert.True (lv.Source.IsMarked (1)); - Assert.True (lv.Source.IsMarked (2)); // but can toggle marked - - // Press key combo again - Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); - Assert.Equal (2, lv.SelectedItem); // cannot move down any further - Assert.True (lv.Source.IsMarked (0)); - Assert.True (lv.Source.IsMarked (1)); - Assert.False (lv.Source.IsMarked (2)); // untoggle toggle marked - } - - [Fact] - public void ListWrapper_StartsWith () - { - var lw = new ListWrapper (["One", "Two", "Three"]); - - Assert.Equal (1, lw.StartsWith ("t")); - Assert.Equal (1, lw.StartsWith ("tw")); - Assert.Equal (2, lw.StartsWith ("th")); - Assert.Equal (1, lw.StartsWith ("T")); - Assert.Equal (1, lw.StartsWith ("TW")); - Assert.Equal (2, lw.StartsWith ("TH")); - - lw = new (["One", "Two", "Three"]); - - Assert.Equal (1, lw.StartsWith ("t")); - Assert.Equal (1, lw.StartsWith ("tw")); - Assert.Equal (2, lw.StartsWith ("th")); - Assert.Equal (1, lw.StartsWith ("T")); - Assert.Equal (1, lw.StartsWith ("TW")); - Assert.Equal (2, lw.StartsWith ("TH")); - } - - [Fact] - public void OnEnter_Does_Not_Throw_Exception () - { - var lv = new ListView (); - var top = new View (); - top.Add (lv); - Exception exception = Record.Exception (() => lv.SetFocus ()); - Assert.Null (exception); - } - - [Fact] - [AutoInitShutdown] - public void RowRender_Event () - { - var rendered = false; - ObservableCollection source = ["one", "two", "three"]; - var lv = new ListView { Width = Dim.Fill (), Height = Dim.Fill () }; - lv.RowRender += (s, _) => rendered = true; - var top = new Toplevel (); - top.Add (lv); - Application.Begin (top); - Assert.False (rendered); - - lv.SetSource (source); - lv.Draw (); - Assert.True (rendered); - top.Dispose (); - } - - [Fact] - public void SelectedItem_Get_Set () - { - var lv = new ListView { Source = new ListWrapper (["One", "Two", "Three"]) }; - Assert.Equal (-1, lv.SelectedItem); - Assert.Throws (() => lv.SelectedItem = 3); - Exception exception = Record.Exception (() => lv.SelectedItem = -1); - Assert.Null (exception); - } - - [Fact] - public void SetSource_Preserves_ListWrapper_Instance_If_Not_Null () - { - var lv = new ListView { Source = new ListWrapper (["One", "Two"]) }; - - Assert.NotNull (lv.Source); - - lv.SetSource (null); - Assert.NotNull (lv.Source); - - lv.Source = null; - Assert.Null (lv.Source); - - lv = new () { Source = new ListWrapper (["One", "Two"]) }; - Assert.NotNull (lv.Source); - - lv.SetSourceAsync (null); - Assert.NotNull (lv.Source); - } - - [Fact] - public void SettingEmptyKeybindingThrows () - { - var lv = new ListView { Source = new ListWrapper (["One", "Two", "Three"]) }; - Assert.Throws (() => lv.KeyBindings.Add (Key.Space)); - } - - private class NewListDataSource : IListDataSource - { -#pragma warning disable CS0067 - /// - public event NotifyCollectionChangedEventHandler CollectionChanged; -#pragma warning restore CS0067 - - public int Count => 0; - public int Length => 0; - - public bool SuspendCollectionChangedEvent { get => throw new NotImplementedException (); set => throw new NotImplementedException (); } - - public bool IsMarked (int item) { throw new NotImplementedException (); } - - public void Render ( - ListView container, - bool selected, - int item, - int col, - int line, - int width, - int start = 0 - ) - { - throw new NotImplementedException (); - } - - public void SetMark (int item, bool value) { throw new NotImplementedException (); } - public IList ToList () { return new List { "One", "Two", "Three" }; } - - public void Dispose () - { - throw new NotImplementedException (); - } - } - - [Fact] - [AutoInitShutdown] - public void Clicking_On_Border_Is_Ignored () - { - var selected = ""; - - var lv = new ListView - { - Height = 5, - Width = 7, - BorderStyle = LineStyle.Single - }; - lv.SetSource (["One", "Two", "Three", "Four"]); - lv.SelectedItemChanged += (s, e) => selected = e.Value.ToString (); - var top = new Toplevel (); - top.Add (lv); - Application.Begin (top); - AutoInitShutdownAttribute.RunIteration (); - - Assert.Equal (new (1), lv.Border!.Thickness); - Assert.Equal (-1, lv.SelectedItem); - Assert.Equal ("", lv.Text); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌─────┐ -│One │ -│Two │ -│Three│ -└─────┘", - output); - - Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Clicked }); - Assert.Equal ("", selected); - Assert.Equal (-1, lv.SelectedItem); - - Application.RaiseMouseEvent ( - new () - { - ScreenPosition = new (1, 1), Flags = MouseFlags.Button1Clicked - }); - Assert.Equal ("One", selected); - Assert.Equal (0, lv.SelectedItem); - - Application.RaiseMouseEvent ( - new () - { - ScreenPosition = new (1, 2), Flags = MouseFlags.Button1Clicked - }); - Assert.Equal ("Two", selected); - Assert.Equal (1, lv.SelectedItem); - - Application.RaiseMouseEvent ( - new () - { - ScreenPosition = new (1, 3), Flags = MouseFlags.Button1Clicked - }); - Assert.Equal ("Three", selected); - Assert.Equal (2, lv.SelectedItem); - - Application.RaiseMouseEvent ( - new () - { - ScreenPosition = new (1, 4), Flags = MouseFlags.Button1Clicked - }); - Assert.Equal ("Three", selected); - Assert.Equal (2, lv.SelectedItem); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void LeftItem_TopItem_Tests () - { - ObservableCollection source = []; - - for (int i = 0; i < 5; i++) - { - source.Add ($"Item {i}"); - } - - var lv = new ListView - { - X = 1, - Source = new ListWrapper (source) - }; - lv.Height = lv.Source.Count; - lv.Width = lv.MaxLength; - var top = new Toplevel (); - top.Add (lv); - Application.Begin (top); - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - Item 0 - Item 1 - Item 2 - Item 3 - Item 4", - output); - - lv.LeftItem = 1; - lv.TopItem = 1; - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - tem 1 - tem 2 - tem 3 - tem 4", - output); - top.Dispose (); - } - - [Fact] - public void CollectionChanged_Event () - { - var added = 0; - var removed = 0; - ObservableCollection source = []; - var lv = new ListView { Source = new ListWrapper (source) }; - - lv.CollectionChanged += (sender, args) => - { - if (args.Action == NotifyCollectionChangedAction.Add) - { - added++; - } - else if (args.Action == NotifyCollectionChangedAction.Remove) - { - removed++; - } - }; - - for (int i = 0; i < 3; i++) - { - source.Add ($"Item{i}"); - } - Assert.Equal (3, added); - Assert.Equal (0, removed); - - added = 0; - - for (int i = 0; i < 3; i++) - { - source.Remove (source [0]); - } - Assert.Equal (0, added); - Assert.Equal (3, removed); - Assert.Empty (source); - } - - [Fact] - public void CollectionChanged_Event_Is_Only_Subscribed_Once () - { - var added = 0; - var removed = 0; - var otherActions = 0; - IList source1 = []; - var lv = new ListView { Source = new ListWrapper (new (source1)) }; - - lv.CollectionChanged += (sender, args) => - { - if (args.Action == NotifyCollectionChangedAction.Add) - { - added++; - } - else if (args.Action == NotifyCollectionChangedAction.Remove) - { - removed++; - } - else - { - otherActions++; - } - }; - - ObservableCollection source2 = []; - lv.Source = new ListWrapper (source2); - ObservableCollection source3 = []; - lv.Source = new ListWrapper (source3); - Assert.Equal (0, added); - Assert.Equal (0, removed); - Assert.Equal (0, otherActions); - - for (int i = 0; i < 3; i++) - { - source1.Add ($"Item{i}"); - source2.Add ($"Item{i}"); - source3.Add ($"Item{i}"); - } - Assert.Equal (3, added); - Assert.Equal (0, removed); - Assert.Equal (0, otherActions); - - added = 0; - - for (int i = 0; i < 3; i++) - { - source1.Remove (source1 [0]); - source2.Remove (source2 [0]); - source3.Remove (source3 [0]); - } - Assert.Equal (0, added); - Assert.Equal (3, removed); - Assert.Equal (0, otherActions); - Assert.Empty (source1); - Assert.Empty (source2); - Assert.Empty (source3); - } - - [Fact] - public void CollectionChanged_Event_UnSubscribe_Previous_If_New_Is_Null () - { - var added = 0; - var removed = 0; - var otherActions = 0; - ObservableCollection source1 = []; - var lv = new ListView { Source = new ListWrapper (source1) }; - - lv.CollectionChanged += (sender, args) => - { - if (args.Action == NotifyCollectionChangedAction.Add) - { - added++; - } - else if (args.Action == NotifyCollectionChangedAction.Remove) - { - removed++; - } - else - { - otherActions++; - } - }; - - lv.Source = new ListWrapper (null); - Assert.Equal (0, added); - Assert.Equal (0, removed); - Assert.Equal (0, otherActions); - - for (int i = 0; i < 3; i++) - { - source1.Add ($"Item{i}"); - } - Assert.Equal (0, added); - Assert.Equal (0, removed); - Assert.Equal (0, otherActions); - - for (int i = 0; i < 3; i++) - { - source1.Remove (source1 [0]); - } - Assert.Equal (0, added); - Assert.Equal (0, removed); - Assert.Equal (0, otherActions); - Assert.Empty (source1); - } - - [Fact] - public void ListWrapper_CollectionChanged_Event_Is_Only_Subscribed_Once () - { - var added = 0; - var removed = 0; - var otherActions = 0; - ObservableCollection source1 = []; - ListWrapper lw = new (source1); - - lw.CollectionChanged += (sender, args) => - { - if (args.Action == NotifyCollectionChangedAction.Add) - { - added++; - } - else if (args.Action == NotifyCollectionChangedAction.Remove) - { - removed++; - } - else - { - otherActions++; - } - }; - - ObservableCollection source2 = []; - lw = new (source2); - ObservableCollection source3 = []; - lw = new (source3); - Assert.Equal (0, added); - Assert.Equal (0, removed); - Assert.Equal (0, otherActions); - - for (int i = 0; i < 3; i++) - { - source1.Add ($"Item{i}"); - source2.Add ($"Item{i}"); - source3.Add ($"Item{i}"); - } - - Assert.Equal (3, added); - Assert.Equal (0, removed); - Assert.Equal (0, otherActions); - - added = 0; - - for (int i = 0; i < 3; i++) - { - source1.Remove (source1 [0]); - source2.Remove (source2 [0]); - source3.Remove (source3 [0]); - } - Assert.Equal (0, added); - Assert.Equal (3, removed); - Assert.Equal (0, otherActions); - Assert.Empty (source1); - Assert.Empty (source2); - Assert.Empty (source3); - } - - [Fact] - public void ListWrapper_CollectionChanged_Event_UnSubscribe_Previous_Is_Disposed () - { - var added = 0; - var removed = 0; - var otherActions = 0; - ObservableCollection source1 = []; - ListWrapper lw = new (source1); - - lw.CollectionChanged += Lw_CollectionChanged; - - lw.Dispose (); - lw = new (null); - Assert.Equal (0, lw.Count); - Assert.Equal (0, added); - Assert.Equal (0, removed); - Assert.Equal (0, otherActions); - - for (int i = 0; i < 3; i++) - { - source1.Add ($"Item{i}"); - } - Assert.Equal (0, added); - Assert.Equal (0, removed); - Assert.Equal (0, otherActions); - - for (int i = 0; i < 3; i++) - { - source1.Remove (source1 [0]); - } - Assert.Equal (0, added); - Assert.Equal (0, removed); - Assert.Equal (0, otherActions); - Assert.Empty (source1); - - - void Lw_CollectionChanged (object sender, NotifyCollectionChangedEventArgs e) - { - if (e.Action == NotifyCollectionChangedAction.Add) - { - added++; - } - else if (e.Action == NotifyCollectionChangedAction.Remove) - { - removed++; - } - else - { - otherActions++; - } - } - } - - [Fact] - public void ListWrapper_SuspendCollectionChangedEvent_ResumeSuspendCollectionChangedEvent_Tests () - { - var added = 0; - ObservableCollection source = []; - ListWrapper lw = new (source); - - lw.CollectionChanged += Lw_CollectionChanged; - - lw.SuspendCollectionChangedEvent = true; - - for (int i = 0; i < 3; i++) - { - source.Add ($"Item{i}"); - } - Assert.Equal (0, added); - Assert.Equal (3, lw.Count); - Assert.Equal (3, source.Count); - - lw.SuspendCollectionChangedEvent = false; - - for (int i = 3; i < 6; i++) - { - source.Add ($"Item{i}"); - } - Assert.Equal (3, added); - Assert.Equal (6, lw.Count); - Assert.Equal (6, source.Count); - - - void Lw_CollectionChanged (object sender, NotifyCollectionChangedEventArgs e) - { - if (e.Action == NotifyCollectionChangedAction.Add) - { - added++; - } - } - } - - [Fact] - public void ListView_SuspendCollectionChangedEvent_ResumeSuspendCollectionChangedEvent_Tests () - { - var added = 0; - ObservableCollection source = []; - ListView lv = new ListView { Source = new ListWrapper (source) }; - - lv.CollectionChanged += Lw_CollectionChanged; - - lv.SuspendCollectionChangedEvent (); - - for (int i = 0; i < 3; i++) - { - source.Add ($"Item{i}"); - } - Assert.Equal (0, added); - Assert.Equal (3, lv.Source.Count); - Assert.Equal (3, source.Count); - - lv.ResumeSuspendCollectionChangedEvent (); - - for (int i = 3; i < 6; i++) - { - source.Add ($"Item{i}"); - } - Assert.Equal (3, added); - Assert.Equal (6, lv.Source.Count); - Assert.Equal (6, source.Count); - - - void Lw_CollectionChanged (object sender, NotifyCollectionChangedEventArgs e) - { - if (e.Action == NotifyCollectionChangedAction.Add) - { - added++; - } - } - } -} \ No newline at end of file diff --git a/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs b/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs index aa3017d51..174788369 100644 --- a/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs @@ -7,7 +7,7 @@ public class SourcesManagerTests #region Update (Stream) [Fact] - public void Update_WithNullSettingsScope_ReturnsFalse () + public void Load_WithNullSettingsScope_ReturnsFalse () { // Arrange var sourcesManager = new SourcesManager (); @@ -23,7 +23,7 @@ public class SourcesManagerTests } [Fact] - public void Update_WithValidStream_UpdatesSettingsScope () + public void Load_WithValidStream_UpdatesSettingsScope () { // Arrange var sourcesManager = new SourcesManager (); @@ -56,7 +56,7 @@ public class SourcesManagerTests } [Fact] - public void Update_WithInvalidJson_AddsJsonError () + public void Load_WithInvalidJson_AddsJsonError () { // Arrange var sourcesManager = new SourcesManager (); @@ -86,7 +86,7 @@ public class SourcesManagerTests #region Update (FilePath) [Fact] - public void Update_WithNonExistentFile_AddsToSourcesAndReturnsTrue () + public void Load_WithNonExistentFile_AddsToSourcesAndReturnsTrue () { // Arrange var sourcesManager = new SourcesManager (); @@ -104,7 +104,7 @@ public class SourcesManagerTests } [Fact] - public void Update_WithValidFile_UpdatesSettingsScope () + public void Load_WithValidFile_UpdatesSettingsScope () { // Arrange var sourcesManager = new SourcesManager (); @@ -140,7 +140,7 @@ public class SourcesManagerTests } [Fact] - public void Update_WithIOException_RetriesAndFailsGracefully () + public void Load_WithIOException_RetriesAndFailsGracefully () { // Arrange var sourcesManager = new SourcesManager (); @@ -174,7 +174,7 @@ public class SourcesManagerTests #region Update (Json String) [Fact] - public void Update_WithNullOrEmptyJson_ReturnsFalse () + public void Load_WithNullOrEmptyJson_ReturnsFalse () { // Arrange var sourcesManager = new SourcesManager (); @@ -193,7 +193,7 @@ public class SourcesManagerTests } [Fact] - public void Update_WithValidJson_UpdatesSettingsScope () + public void Load_WithValidJson_UpdatesSettingsScope () { // Arrange var sourcesManager = new SourcesManager (); @@ -381,7 +381,7 @@ public class SourcesManagerTests } [Fact] - public void Update_WhenCalledMultipleTimes_MaintainsLastSourceForLocation () + public void Load_WhenCalledMultipleTimes_MaintainsLastSourceForLocation () { // Arrange var sourcesManager = new SourcesManager (); @@ -401,7 +401,7 @@ public class SourcesManagerTests } [Fact] - public void Update_WithDifferentLocations_AddsAllSourcesToCollection () + public void Load_WithDifferentLocations_AddsAllSourcesToCollection () { // Arrange var sourcesManager = new SourcesManager (); @@ -452,7 +452,7 @@ public class SourcesManagerTests } [Fact] - public void Update_WithNonExistentFileAndDifferentLocations_TracksAllSources () + public void Load_WithNonExistentFileAndDifferentLocations_TracksAllSources () { // Arrange var sourcesManager = new SourcesManager (); diff --git a/Tests/UnitTestsParallelizable/Text/CollectionNavigatorTests.cs b/Tests/UnitTestsParallelizable/Text/CollectionNavigatorTests.cs index c3fce7af4..5d3923d50 100644 --- a/Tests/UnitTestsParallelizable/Text/CollectionNavigatorTests.cs +++ b/Tests/UnitTestsParallelizable/Text/CollectionNavigatorTests.cs @@ -42,7 +42,7 @@ public class CollectionNavigatorTests // cycling with 'a' n = new CollectionNavigator (simpleStrings); - Assert.Equal (0, n.GetNextMatchingItem (-1, 'a')); + Assert.Equal (0, n.GetNextMatchingItem (null, 'a')); Assert.Equal (1, n.GetNextMatchingItem (0, 'a')); // if 4 (candle) is selected it should loop back to apricot @@ -53,7 +53,7 @@ public class CollectionNavigatorTests public void Delay () { var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "apricot" }; - var current = 0; + int? current = 0; var n = new CollectionNavigator (strings); // No delay @@ -96,7 +96,7 @@ public class CollectionNavigatorTests var strings = new [] { "apricot", "arm", "ta", "target", "text", "egg", "candle" }; var n = new CollectionNavigator (strings); - var current = 0; + int? current = 0; Assert.Equal (strings.IndexOf ("ta"), current = n.GetNextMatchingItem (current, 't')); // should match "te" in "text" @@ -137,7 +137,7 @@ public class CollectionNavigatorTests public void MinimizeMovement_False_ShouldMoveIfMultipleMatches () { var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "apricot", "c", "car", "cart" }; - var current = 0; + int? current = 0; var n = new CollectionNavigator (strings); Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$")); Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$")); @@ -166,14 +166,14 @@ public class CollectionNavigatorTests Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car")); Assert.Equal (strings.IndexOf ("cart"), current = n.GetNextMatchingItem (current, "car")); - Assert.Equal (-1, current = n.GetNextMatchingItem (current, "x")); + Assert.Null (n.GetNextMatchingItem (current, "x")); } [Fact] public void MinimizeMovement_True_ShouldStayOnCurrentIfMultipleMatches () { var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "apricot", "c", "car", "cart" }; - var current = 0; + int? current = 0; var n = new CollectionNavigator (strings); Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", true)); Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$", true)); @@ -185,14 +185,14 @@ public class CollectionNavigatorTests Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car", true)); Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car", true)); - Assert.Equal (-1, current = n.GetNextMatchingItem (current, "x", true)); + Assert.Null (n.GetNextMatchingItem (current, "x", true)); } [Fact] public void MutliKeySearchPlusWrongKeyStays () { var strings = new [] { "a", "c", "can", "candle", "candy", "yellow", "zebra" }; - var current = 0; + int? current = 0; var n = new CollectionNavigator (strings); // https://github.com/gui-cs/Terminal.Gui/pull/2132#issuecomment-1298425573 @@ -240,20 +240,20 @@ public class CollectionNavigatorTests } [Fact] - public void ShouldAcceptNegativeOne () + public void ShouldAcceptNull () { var n = new CollectionNavigator (simpleStrings); - // Expect that index of -1 (i.e. no selection) should work correctly + // Expect that index of null (i.e. no selection) should work correctly // and select the first entry of the letter 'b' - Assert.Equal (2, n.GetNextMatchingItem (-1, 'b')); + Assert.Equal (2, n.GetNextMatchingItem (null, 'b')); } [Fact] public void Symbols () { var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "apricot" }; - var current = 0; + int? current = 0; var n = new CollectionNavigator (strings); Assert.Equal (strings.IndexOf ("apricot"), current = n.GetNextMatchingItem (current, 'a')); Assert.Equal ("a", n.SearchString); @@ -293,7 +293,7 @@ public class CollectionNavigatorTests var strings = new [] { "apricot", "arm", "ta", "丗丙业丞", "丗丙丛", "text", "egg", "candle" }; var n = new CollectionNavigator (strings); - var current = 0; + int? current = 0; Assert.Equal (strings.IndexOf ("丗丙业丞"), current = n.GetNextMatchingItem (current, '丗')); // 丗丙业丞 is as good a match as 丗丙丛 @@ -319,7 +319,7 @@ public class CollectionNavigatorTests public void Word () { var strings = new [] { "apricot", "arm", "bat", "batman", "bates hotel", "candle" }; - var current = 0; + int? current = 0; var n = new CollectionNavigator (strings); Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'b')); // match bat Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'a')); // match bat @@ -344,7 +344,7 @@ public class CollectionNavigatorTests public void CustomMatcher_NeverMatches () { var strings = new [] { "apricot", "arm", "bat", "batman", "bates hotel", "candle" }; - var current = 0; + int? current = 0; var n = new CollectionNavigator (strings); var matchNone = new Mock (); diff --git a/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj b/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj index 35233cc03..5024415aa 100644 --- a/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj +++ b/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj @@ -27,6 +27,11 @@ true + + + + + @@ -69,7 +74,4 @@ - - - \ No newline at end of file diff --git a/Tests/UnitTestsParallelizable/View/Draw/NeedsDrawTests.cs b/Tests/UnitTestsParallelizable/View/Draw/NeedsDrawTests.cs index b8b3e7d43..780ffb94c 100644 --- a/Tests/UnitTestsParallelizable/View/Draw/NeedsDrawTests.cs +++ b/Tests/UnitTestsParallelizable/View/Draw/NeedsDrawTests.cs @@ -130,14 +130,14 @@ public class NeedsDrawTests : FakeDriverBase Assert.False (view.NeedsLayout); // SRL won't change anything since the view frame wasn't changed - view.SetRelativeLayout (Application.Screen.Size); + view.SetRelativeLayout (new (100, 100)); Assert.False (view.NeedsDraw); view.SetNeedsLayout (); // SRL won't change anything since the view frame wasn't changed // SRL doesn't depend on NeedsLayout, but LayoutSubViews does - view.SetRelativeLayout (Application.Screen.Size); + view.SetRelativeLayout (new (100, 100)); Assert.False (view.NeedsDraw); Assert.True (view.NeedsLayout); @@ -180,7 +180,7 @@ public class NeedsDrawTests : FakeDriverBase Assert.True (view.NeedsDraw); Assert.True (superView.NeedsDraw); - superView.SetRelativeLayout (Application.Screen.Size); + superView.SetRelativeLayout (new (100, 100)); Assert.True (view.NeedsDraw); Assert.True (superView.NeedsDraw); } @@ -216,7 +216,7 @@ public class NeedsDrawTests : FakeDriverBase view.EndInit (); Assert.True (view.NeedsDraw); - view.SetRelativeLayout (Application.Screen.Size); + view.SetRelativeLayout (new (100, 100)); Assert.True (view.NeedsDraw); view.LayoutSubViews (); @@ -235,7 +235,7 @@ public class NeedsDrawTests : FakeDriverBase view.EndInit (); Assert.True (view.NeedsDraw); - view.SetRelativeLayout (Application.Screen.Size); + view.SetRelativeLayout (new (100, 100)); Assert.True (view.NeedsDraw); view.LayoutSubViews (); diff --git a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs b/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs index 0078736c5..61aadaea7 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs +++ b/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs @@ -597,7 +597,7 @@ public partial class DimAutoTests // Without a subview, width should be 10 // Without a subview, height should be 1 - view.SetRelativeLayout (Application.Screen.Size); + view.SetRelativeLayout (new (100, 100)); Assert.Equal (10, view.Frame.Width); Assert.Equal (1, view.Frame.Height); diff --git a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.cs b/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.cs index b42b0bc9c..4c1da89b5 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.cs +++ b/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.cs @@ -304,10 +304,10 @@ public partial class DimAutoTests (ITestOutputHelper output) Width = Auto (), Height = 1 }; - view.SetRelativeLayout (Application.Screen.Size); + view.SetRelativeLayout (new (100, 100)); lastSize = view.Frame.Size; view.HotKeySpecifier = (Rune)'*'; - view.SetRelativeLayout (Application.Screen.Size); + view.SetRelativeLayout (new (100, 100)); Assert.NotEqual (lastSize, view.Frame.Size); view = new () @@ -316,10 +316,10 @@ public partial class DimAutoTests (ITestOutputHelper output) Width = Auto (), Height = 1 }; - view.SetRelativeLayout (Application.Screen.Size); + view.SetRelativeLayout (new (100, 100)); lastSize = view.Frame.Size; view.Text = "*ABCD"; - view.SetRelativeLayout (Application.Screen.Size); + view.SetRelativeLayout (new (100, 100)); Assert.NotEqual (lastSize, view.Frame.Size); } @@ -703,7 +703,7 @@ public partial class DimAutoTests (ITestOutputHelper output) view.Text = text; - view.SetRelativeLayout (Application.Screen.Size); + view.SetRelativeLayout (new (100, 100)); Assert.Equal (new (expectedW, expectedH), view.Frame.Size); } @@ -812,7 +812,7 @@ public partial class DimAutoTests (ITestOutputHelper output) view.Text = text; - view.SetRelativeLayout (Application.Screen.Size); + view.SetRelativeLayout (new (100, 100)); Assert.Equal (new (expectedW, expectedH), view.Frame.Size); } @@ -831,7 +831,7 @@ public partial class DimAutoTests (ITestOutputHelper output) view.Text = text; - view.SetRelativeLayout (Application.Screen.Size); + view.SetRelativeLayout (new (100, 100)); Assert.Equal (new (expectedW, expectedH), view.Frame.Size); } diff --git a/Tests/UnitTestsParallelizable/View/Layout/FrameTests.cs b/Tests/UnitTestsParallelizable/View/Layout/FrameTests.cs index 364012cc3..1f9b24a90 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/FrameTests.cs +++ b/Tests/UnitTestsParallelizable/View/Layout/FrameTests.cs @@ -120,7 +120,7 @@ public class FrameTests Assert.True (view.NeedsLayout); view.Layout (); Assert.False (view.NeedsLayout); - Assert.Equal (Application.Screen, view.Frame); + Assert.Equal (new Size (2048, 2048), view.Frame.Size); view.Frame = Rectangle.Empty; Assert.Equal (Rectangle.Empty, view.Frame); @@ -165,7 +165,7 @@ public class FrameTests Assert.Equal (Rectangle.Empty, v.Frame); v.Dispose (); - v = new() { Frame = frame }; + v = new () { Frame = frame }; Assert.Equal (frame, v.Frame); v.Frame = newFrame; @@ -181,7 +181,7 @@ public class FrameTests Assert.Equal (Dim.Absolute (40), v.Height); v.Dispose (); - v = new() { X = frame.X, Y = frame.Y, Text = "v" }; + v = new () { X = frame.X, Y = frame.Y, Text = "v" }; v.Frame = newFrame; Assert.Equal (newFrame, v.Frame); @@ -196,7 +196,7 @@ public class FrameTests v.Dispose (); newFrame = new (10, 20, 30, 40); - v = new() { Frame = frame }; + v = new () { Frame = frame }; v.Frame = newFrame; Assert.Equal (newFrame, v.Frame); @@ -210,7 +210,7 @@ public class FrameTests Assert.Equal (Dim.Absolute (40), v.Height); v.Dispose (); - v = new() { X = frame.X, Y = frame.Y, Text = "v" }; + v = new () { X = frame.X, Y = frame.Y, Text = "v" }; v.Frame = newFrame; Assert.Equal (newFrame, v.Frame); diff --git a/Tests/UnitTestsParallelizable/View/SubviewTests.cs b/Tests/UnitTestsParallelizable/View/SubviewTests.cs index f02650d54..a447acd6e 100644 --- a/Tests/UnitTestsParallelizable/View/SubviewTests.cs +++ b/Tests/UnitTestsParallelizable/View/SubviewTests.cs @@ -14,11 +14,11 @@ public class SubViewTests super.SuperViewChanged += (s, e) => { - superRaisedCount++; + superRaisedCount++; }; sub.SuperViewChanged += (s, e) => { - if (e.SuperView is {}) + if (e.SuperView is { }) { subRaisedCount++; } @@ -266,14 +266,14 @@ public class SubViewTests superView.Add (subview1, subview2, subview3); superView.MoveSubViewTowardsEnd (subview2); - Assert.Equal (subview2, superView.SubViews.ToArray() [^1]); + Assert.Equal (subview2, superView.SubViews.ToArray () [^1]); superView.MoveSubViewTowardsEnd (subview1); - Assert.Equal (subview1, superView.SubViews.ToArray() [1]); + Assert.Equal (subview1, superView.SubViews.ToArray () [1]); // Already at end, what happens? superView.MoveSubViewTowardsEnd (subview2); - Assert.Equal (subview2, superView.SubViews.ToArray() [^1]); + Assert.Equal (subview2, superView.SubViews.ToArray () [^1]); } [Fact] @@ -517,7 +517,7 @@ public class SubViewTests Assert.False (v2AddedToWin.CanFocus); Assert.False (svAddedTov1.CanFocus); - Application.LayoutAndDraw (); + top.Layout (); }; winAddedToTop.Initialized += (s, e) => diff --git a/Tests/UnitTestsParallelizable/Views/IListDataSourceTests.cs b/Tests/UnitTestsParallelizable/Views/IListDataSourceTests.cs new file mode 100644 index 000000000..29faa1aab --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/IListDataSourceTests.cs @@ -0,0 +1,513 @@ +#nullable enable +using System.Collections; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Text; +using Xunit.Abstractions; + +// ReSharper disable InconsistentNaming + +namespace UnitTests_Parallelizable.ViewTests; + +public class IListDataSourceTests (ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + + #region Concurrent Modification Tests + + [Fact] + public void ListWrapper_SuspendAndModify_NoEventsUntilResume () + { + ObservableCollection source = ["Item1"]; + ListWrapper wrapper = new (source); + var eventCount = 0; + + wrapper.CollectionChanged += (s, e) => eventCount++; + + wrapper.SuspendCollectionChangedEvent = true; + + source.Add ("Item2"); + source.Add ("Item3"); + source.RemoveAt (0); + + Assert.Equal (0, eventCount); + + wrapper.SuspendCollectionChangedEvent = false; + + // Should have adjusted marks for the removals that happened while suspended + Assert.Equal (2, wrapper.Count); + } + + #endregion + + /// + /// Test implementation of IListDataSource for testing custom implementations + /// + private class TestListDataSource : IListDataSource + { + private readonly List _items = ["Custom Item 00", "Custom Item 01", "Custom Item 02"]; + private readonly BitArray _marks = new (3); + + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + public int Count => _items.Count; + + public int Length => _items.Any () ? _items.Max (s => s?.Length ?? 0) : 0; + + public bool SuspendCollectionChangedEvent { get; set; } + + public bool IsMarked (int item) + { + if (item < 0 || item >= _items.Count) + { + return false; + } + + return _marks [item]; + } + + public void SetMark (int item, bool value) + { + if (item >= 0 && item < _items.Count) + { + _marks [item] = value; + } + } + + public void Render (ListView listView, bool selected, int item, int col, int line, int width, int viewportX = 0) + { + if (item < 0 || item >= _items.Count) + { + return; + } + + listView.Move (col, line); + string text = _items [item] ?? ""; + + if (viewportX < text.Length) + { + text = text.Substring (viewportX); + } + else + { + text = ""; + } + + if (text.Length > width) + { + text = text.Substring (0, width); + } + + listView.AddStr (text); + + // Fill remaining width + for (int i = text.Length; i < width; i++) + { + listView.AddRune ((Rune)' '); + } + } + + public IList ToList () { return _items; } + + public void Dispose () { IsDisposed = true; } + + public void AddItem (string item) + { + _items.Add (item); + + // Resize marks + var newMarks = new BitArray (_items.Count); + + for (var i = 0; i < Math.Min (_marks.Length, newMarks.Length); i++) + { + newMarks [i] = _marks [i]; + } + + if (!SuspendCollectionChangedEvent) + { + CollectionChanged?.Invoke (this, new (NotifyCollectionChangedAction.Add, item, _items.Count - 1)); + } + } + + public bool IsDisposed { get; private set; } + } + + #region ListWrapper Render Tests + + [Fact] + public void ListWrapper_Render_NullItem_RendersEmpty () + { + ObservableCollection source = [null, "Item2"]; + ListWrapper wrapper = new (source); + var listView = new ListView { Width = 20, Height = 2 }; + listView.BeginInit (); + listView.EndInit (); + + // Render the null item (index 0) + wrapper.Render (listView, false, 0, 0, 0, 20); + + // Should not throw and should render empty/spaces + Assert.Equal (2, wrapper.Count); + } + + [Fact] + public void ListWrapper_Render_EmptyString_RendersSpaces () + { + ObservableCollection source = [""]; + ListWrapper wrapper = new (source); + var listView = new ListView { Width = 20, Height = 1 }; + listView.BeginInit (); + listView.EndInit (); + + wrapper.Render (listView, false, 0, 0, 0, 20); + + Assert.Equal (1, wrapper.Count); + Assert.Equal (0, wrapper.Length); // Empty string has zero length + } + + [Fact] + public void ListWrapper_Render_UnicodeText_CalculatesWidthCorrectly () + { + ObservableCollection source = ["Hello 你好", "Test"]; + ListWrapper wrapper = new (source); + + // "Hello 你好" should be: "Hello " (6) + "你" (2) + "好" (2) = 10 columns + Assert.True (wrapper.Length >= 10); + } + + [Fact] + public void ListWrapper_Render_LongString_ClipsToWidth () + { + var longString = new string ('X', 100); + ObservableCollection source = [longString]; + ListWrapper wrapper = new (source); + var listView = new ListView { Width = 20, Height = 1 }; + listView.BeginInit (); + listView.EndInit (); + + wrapper.Render (listView, false, 0, 0, 0, 10); + + Assert.Equal (100, wrapper.Length); + } + + [Fact] + public void ListWrapper_Render_WithViewportX_ScrollsHorizontally () + { + ObservableCollection source = ["0123456789ABCDEF"]; + ListWrapper wrapper = new (source); + var listView = new ListView { Width = 10, Height = 1 }; + listView.BeginInit (); + listView.EndInit (); + + // Render with horizontal scroll offset of 5 + wrapper.Render (listView, false, 0, 0, 0, 10, 5); + + // Should render "56789ABCDE" (starting at position 5) + Assert.Equal (16, wrapper.Length); + } + + [Fact] + public void ListWrapper_Render_ViewportXBeyondLength_RendersEmpty () + { + ObservableCollection source = ["Short"]; + ListWrapper wrapper = new (source); + var listView = new ListView { Width = 20, Height = 1 }; + listView.BeginInit (); + listView.EndInit (); + + // Render with viewport beyond string length + wrapper.Render (listView, false, 0, 0, 0, 10, 100); + + Assert.Equal (5, wrapper.Length); + } + + [Fact] + public void ListWrapper_Render_ColAndLine_PositionsCorrectly () + { + ObservableCollection source = ["Item1", "Item2"]; + ListWrapper wrapper = new (source); + var listView = new ListView { Width = 20, Height = 5 }; + listView.BeginInit (); + listView.EndInit (); + + // Render at different positions + wrapper.Render (listView, false, 0, 2, 1, 10); // col=2, line=1 + wrapper.Render (listView, false, 1, 0, 3, 10); // col=0, line=3 + + Assert.Equal (2, wrapper.Count); + } + + [Fact] + public void ListWrapper_Render_WidthConstraint_FillsRemaining () + { + ObservableCollection source = ["Hi"]; + ListWrapper wrapper = new (source); + var listView = new ListView { Width = 20, Height = 1 }; + listView.BeginInit (); + listView.EndInit (); + + // Render "Hi" in width of 10 - should fill remaining 8 with spaces + wrapper.Render (listView, false, 0, 0, 0, 10); + + Assert.Equal (2, wrapper.Length); + } + + [Fact] + public void ListWrapper_Render_NonStringType_UsesToString () + { + ObservableCollection source = [42, 100, -5]; + ListWrapper wrapper = new (source); + var listView = new ListView { Width = 20, Height = 3 }; + listView.BeginInit (); + listView.EndInit (); + + wrapper.Render (listView, false, 0, 0, 0, 10); + wrapper.Render (listView, false, 1, 0, 1, 10); + wrapper.Render (listView, false, 2, 0, 2, 10); + + Assert.Equal (3, wrapper.Count); + Assert.True (wrapper.Length >= 2); // "42" is 2 chars, "100" is 3 chars + } + + #endregion + + #region Custom IListDataSource Implementation Tests + + [Fact] + public void CustomDataSource_AllMembers_WorkCorrectly () + { + var customSource = new TestListDataSource (); + var listView = new ListView { Source = customSource, Width = 20, Height = 5 }; + + Assert.Equal (3, customSource.Count); + Assert.Equal (14, customSource.Length); // "Custom Item 00" is 14 chars + + // Test marking + Assert.False (customSource.IsMarked (0)); + customSource.SetMark (0, true); + Assert.True (customSource.IsMarked (0)); + customSource.SetMark (0, false); + Assert.False (customSource.IsMarked (0)); + + // Test ToList + IList list = customSource.ToList (); + Assert.Equal (3, list.Count); + Assert.Equal ("Custom Item 00", list [0]); + + // Test render doesn't throw + listView.BeginInit (); + listView.EndInit (); + Exception ex = Record.Exception (() => customSource.Render (listView, false, 0, 0, 0, 20)); + Assert.Null (ex); + } + + [Fact] + public void CustomDataSource_CollectionChanged_RaisedOnModification () + { + var customSource = new TestListDataSource (); + var eventRaised = false; + NotifyCollectionChangedAction? action = null; + + customSource.CollectionChanged += (s, e) => + { + eventRaised = true; + action = e.Action; + }; + + customSource.AddItem ("New Item"); + + Assert.True (eventRaised); + Assert.Equal (NotifyCollectionChangedAction.Add, action); + Assert.Equal (4, customSource.Count); + } + + [Fact] + public void CustomDataSource_SuspendCollectionChanged_SuppressesEvents () + { + var customSource = new TestListDataSource (); + var eventCount = 0; + + customSource.CollectionChanged += (s, e) => eventCount++; + + customSource.SuspendCollectionChangedEvent = true; + customSource.AddItem ("Item 1"); + customSource.AddItem ("Item 2"); + Assert.Equal (0, eventCount); // No events raised + + customSource.SuspendCollectionChangedEvent = false; + customSource.AddItem ("Item 3"); + Assert.Equal (1, eventCount); // Event raised after resume + } + + [Fact] + public void CustomDataSource_Dispose_CleansUp () + { + var customSource = new TestListDataSource (); + + customSource.Dispose (); + + // After dispose, adding should not raise events (if implemented correctly) + customSource.AddItem ("New Item"); + + // The test source doesn't unsubscribe in dispose, but this shows the pattern + Assert.True (customSource.IsDisposed); + } + + #endregion + + #region Edge Cases + + [Fact] + public void ListWrapper_EmptyCollection_PropertiesReturnZero () + { + ObservableCollection source = []; + ListWrapper wrapper = new (source); + + Assert.Equal (0, wrapper.Count); + Assert.Equal (0, wrapper.Length); + } + + [Fact] + public void ListWrapper_NullSource_HandledGracefully () + { + ListWrapper wrapper = new (null); + + Assert.Equal (0, wrapper.Count); + Assert.Equal (0, wrapper.Length); + + // ToList should not throw + IList list = wrapper.ToList (); + Assert.Empty (list); + } + + [Fact] + public void ListWrapper_IsMarked_OutOfBounds_ReturnsFalse () + { + ObservableCollection source = ["Item1"]; + ListWrapper wrapper = new (source); + + Assert.False (wrapper.IsMarked (-1)); + Assert.False (wrapper.IsMarked (1)); + Assert.False (wrapper.IsMarked (100)); + } + + [Fact] + public void ListWrapper_SetMark_OutOfBounds_DoesNotThrow () + { + ObservableCollection source = ["Item1"]; + ListWrapper wrapper = new (source); + + Exception ex = Record.Exception (() => wrapper.SetMark (-1, true)); + Assert.Null (ex); + + ex = Record.Exception (() => wrapper.SetMark (100, true)); + Assert.Null (ex); + } + + [Fact] + public void ListWrapper_CollectionShrinks_MarksAdjusted () + { + ObservableCollection source = ["Item1", "Item2", "Item3"]; + ListWrapper wrapper = new (source); + + wrapper.SetMark (0, true); + wrapper.SetMark (2, true); + + Assert.True (wrapper.IsMarked (0)); + Assert.True (wrapper.IsMarked (2)); + + // Remove item 1 (middle item) + source.RemoveAt (1); + + Assert.Equal (2, wrapper.Count); + Assert.True (wrapper.IsMarked (0)); // Still marked + + // Item that was at index 2 is now at index 1 + } + + [Fact] + public void ListWrapper_CollectionGrows_MarksPreserved () + { + ObservableCollection source = ["Item1"]; + ListWrapper wrapper = new (source); + + wrapper.SetMark (0, true); + Assert.True (wrapper.IsMarked (0)); + + source.Add ("Item2"); + source.Add ("Item3"); + + Assert.Equal (3, wrapper.Count); + Assert.True (wrapper.IsMarked (0)); // Original mark preserved + Assert.False (wrapper.IsMarked (1)); + Assert.False (wrapper.IsMarked (2)); + } + + [Fact] + public void ListWrapper_StartsWith_EmptyString_ReturnsFirst () + { + ObservableCollection source = ["Apple", "Banana", "Cherry"]; + ListWrapper wrapper = new (source); + + // Searching for empty string might return -1 or 0 depending on implementation + int result = wrapper.StartsWith (""); + Assert.True (result == -1 || result == 0); + } + + [Fact] + public void ListWrapper_StartsWith_NoMatch_ReturnsNegative () + { + ObservableCollection source = ["Apple", "Banana", "Cherry"]; + ListWrapper wrapper = new (source); + + int result = wrapper.StartsWith ("Zebra"); + Assert.Equal (-1, result); + } + + [Fact] + public void ListWrapper_StartsWith_CaseInsensitive () + { + ObservableCollection source = ["Apple", "Banana", "Cherry"]; + ListWrapper wrapper = new (source); + + Assert.Equal (0, wrapper.StartsWith ("app")); + Assert.Equal (0, wrapper.StartsWith ("APP")); + Assert.Equal (1, wrapper.StartsWith ("ban")); + Assert.Equal (1, wrapper.StartsWith ("BAN")); + } + + [Fact] + public void ListWrapper_MaxLength_UpdatesOnCollectionChange () + { + ObservableCollection source = ["Hi"]; + ListWrapper wrapper = new (source); + + Assert.Equal (2, wrapper.Length); + + source.Add ("Very Long String Indeed"); + Assert.Equal (23, wrapper.Length); + + source.Clear (); + source.Add ("X"); + Assert.Equal (1, wrapper.Length); + } + + [Fact] + public void ListWrapper_Dispose_UnsubscribesFromCollectionChanged () + { + ObservableCollection source = ["Item1"]; + ListWrapper wrapper = new (source); + + wrapper.CollectionChanged += (s, e) => { }; + + wrapper.Dispose (); + + // After dispose, source changes should not raise wrapper events + source.Add ("Item2"); + + // The wrapper's event might still fire, but the wrapper won't propagate source events + // This depends on implementation + } + + #endregion +} diff --git a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs index 9b942d681..363412165 100644 --- a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs @@ -1,32 +1,89 @@ -using System.Collections.ObjectModel; +#nullable enable +using System.Collections; +using System.Collections.ObjectModel; +using System.Collections.Specialized; using Moq; +using Terminal.Gui; +using UnitTests; +using Xunit; +using Xunit.Abstractions; + +// ReSharper disable AccessToModifiedClosure namespace UnitTests_Parallelizable.ViewsTests; -public class ListViewTests +public class ListViewTests (ITestOutputHelper output) { + private readonly ITestOutputHelper _output = output; + [Fact] + public void CollectionNavigatorMatcher_KeybindingsOverrideNavigator () + { + ObservableCollection source = ["apricot", "arm", "bat", "batman", "bates hotel", "candle"]; + var lv = new ListView { Source = new ListWrapper (source) }; + + lv.SetFocus (); + + lv.KeyBindings.Add (Key.B, Command.Down); + + Assert.Null (lv.SelectedItem); + + // Keys should be consumed to move down the navigation i.e. to apricot + Assert.True (lv.NewKeyDownEvent (Key.B)); + Assert.Equal (0, lv.SelectedItem); + + Assert.True (lv.NewKeyDownEvent (Key.B)); + Assert.Equal (1, lv.SelectedItem); + + // There is no keybinding for Key.C so it hits collection navigator i.e. we jump to candle + Assert.True (lv.NewKeyDownEvent (Key.C)); + Assert.Equal (5, lv.SelectedItem); + } + + [Fact] + public void ListView_CollectionNavigatorMatcher_KeybindingsOverrideNavigator () + { + ObservableCollection source = ["apricot", "arm", "bat", "batman", "bates hotel", "candle"]; + var lv = new ListView { Source = new ListWrapper (source) }; + + lv.SetFocus (); + + lv.KeyBindings.Add (Key.B, Command.Down); + + Assert.Null (lv.SelectedItem); + + // Keys should be consumed to move down the navigation i.e. to apricot + Assert.True (lv.NewKeyDownEvent (Key.B)); + Assert.Equal (0, lv.SelectedItem); + + Assert.True (lv.NewKeyDownEvent (Key.B)); + Assert.Equal (1, lv.SelectedItem); + + // There is no keybinding for Key.C so it hits collection navigator i.e. we jump to candle + Assert.True (lv.NewKeyDownEvent (Key.C)); + Assert.Equal (5, lv.SelectedItem); + } + [Fact] public void ListViewCollectionNavigatorMatcher_DefaultBehaviour () { - ObservableCollection source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" }; - ListView lv = new ListView { Source = new ListWrapper (source) }; + ObservableCollection source = ["apricot", "arm", "bat", "batman", "bates hotel", "candle"]; + var lv = new ListView { Source = new ListWrapper (source) }; // Keys are consumed during navigation Assert.True (lv.NewKeyDownEvent (Key.B)); Assert.True (lv.NewKeyDownEvent (Key.A)); Assert.True (lv.NewKeyDownEvent (Key.T)); - Assert.Equal ("bat", (string)lv.Source.ToList () [lv.SelectedItem]); + Assert.Equal ("bat", (string)lv.Source.ToList () [lv.SelectedItem!.Value]!); } [Fact] public void ListViewCollectionNavigatorMatcher_IgnoreKeys () { - ObservableCollection source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" }; - ListView lv = new ListView { Source = new ListWrapper (source) }; + ObservableCollection source = ["apricot", "arm", "bat", "batman", "bates hotel", "candle"]; + var lv = new ListView { Source = new ListWrapper (source) }; - - var matchNone = new Mock (); + Mock matchNone = new (); matchNone.Setup (m => m.IsCompatibleKey (It.IsAny ())) .Returns (false); @@ -45,11 +102,10 @@ public class ListViewTests [Fact] public void ListViewCollectionNavigatorMatcher_OverrideMatching () { - ObservableCollection source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" }; - ListView lv = new ListView { Source = new ListWrapper (source) }; + ObservableCollection source = ["apricot", "arm", "bat", "batman", "bates hotel", "candle"]; + var lv = new ListView { Source = new ListWrapper (source) }; - - var matchNone = new Mock (); + Mock matchNone = new (); matchNone.Setup (m => m.IsCompatibleKey (It.IsAny ())) .Returns (true); @@ -59,6 +115,7 @@ public class ListViewTests .Returns ((string s, object key) => s.StartsWith ('B') && key?.ToString () == "candle"); lv.KeystrokeNavigator.Matcher = matchNone.Object; + // Keys are consumed during navigation Assert.True (lv.NewKeyDownEvent (Key.B)); Assert.Equal (5, lv.SelectedItem); @@ -67,54 +124,1461 @@ public class ListViewTests Assert.True (lv.NewKeyDownEvent (Key.T)); Assert.Equal (5, lv.SelectedItem); - Assert.Equal ("candle", (string)lv.Source.ToList () [lv.SelectedItem]); + Assert.Equal ("candle", (string)lv.Source.ToList () [lv.SelectedItem!.Value]!); + } + + #region ListView Tests (from ListViewTests.cs - parallelizable) + + [Fact] + public void Constructors_Defaults () + { + var lv = new ListView (); + Assert.Null (lv.Source); + Assert.True (lv.CanFocus); + Assert.Null (lv.SelectedItem); + Assert.False (lv.AllowsMultipleSelection); + + lv = new () { Source = new ListWrapper (["One", "Two", "Three"]) }; + Assert.NotNull (lv.Source); + Assert.Null (lv.SelectedItem); + + lv = new () { Source = new NewListDataSource () }; + Assert.NotNull (lv.Source); + Assert.Null (lv.SelectedItem); + + lv = new () + { + Y = 1, Width = 10, Height = 20, Source = new ListWrapper (["One", "Two", "Three"]) + }; + Assert.NotNull (lv.Source); + Assert.Null (lv.SelectedItem); + Assert.Equal (new (0, 1, 10, 20), lv.Frame); + + lv = new () { Y = 1, Width = 10, Height = 20, Source = new NewListDataSource () }; + Assert.NotNull (lv.Source); + Assert.Null (lv.SelectedItem); + Assert.Equal (new (0, 1, 10, 20), lv.Frame); + } + + private class NewListDataSource : IListDataSource + { +#pragma warning disable CS0067 + public event NotifyCollectionChangedEventHandler? CollectionChanged; +#pragma warning restore CS0067 + + public int Count => 0; + public int Length => 0; + + public bool SuspendCollectionChangedEvent + { + get => throw new NotImplementedException (); + set => throw new NotImplementedException (); + } + + public bool IsMarked (int item) { throw new NotImplementedException (); } + + public void Render ( + ListView container, + bool selected, + int item, + int col, + int line, + int width, + int viewportX = 0 + ) + { + throw new NotImplementedException (); + } + + public void SetMark (int item, bool value) { throw new NotImplementedException (); } + public IList ToList () { return new List { "One", "Two", "Three" }; } + + public void Dispose () { throw new NotImplementedException (); } } [Fact] - public void ListView_CollectionNavigatorMatcher_KeybindingsOverrideNavigator () + public void KeyBindings_Command () { - ObservableCollection source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" }; - ListView lv = new ListView { Source = new ListWrapper (source) }; - - lv.SetFocus (); - - lv.KeyBindings.Add (Key.B, Command.Down); - - Assert.Equal (-1, lv.SelectedItem); - - // Keys should be consumed to move down the navigation i.e. to apricot - Assert.True (lv.NewKeyDownEvent (Key.B)); + ObservableCollection source = ["One", "Two", "Three"]; + var lv = new ListView { Height = 2, AllowsMarking = true, Source = new ListWrapper (source) }; + lv.BeginInit (); + lv.EndInit (); + Assert.Null (lv.SelectedItem); + Assert.True (lv.NewKeyDownEvent (Key.CursorDown)); + Assert.Equal (0, lv.SelectedItem); + Assert.True (lv.NewKeyDownEvent (Key.CursorUp)); + Assert.Equal (0, lv.SelectedItem); + Assert.True (lv.NewKeyDownEvent (Key.PageDown)); + Assert.Equal (2, lv.SelectedItem); + Assert.Equal (2, lv.TopItem); + Assert.True (lv.NewKeyDownEvent (Key.PageUp)); + Assert.Equal (0, lv.SelectedItem); + Assert.Equal (0, lv.TopItem); + Assert.False (lv.Source.IsMarked (lv.SelectedItem!.Value)); + Assert.True (lv.NewKeyDownEvent (Key.Space)); + Assert.True (lv.Source.IsMarked (lv.SelectedItem!.Value)); + var opened = false; + lv.OpenSelectedItem += (s, _) => opened = true; + Assert.True (lv.NewKeyDownEvent (Key.Enter)); + Assert.True (opened); + Assert.True (lv.NewKeyDownEvent (Key.End)); + Assert.Equal (2, lv.SelectedItem); + Assert.True (lv.NewKeyDownEvent (Key.Home)); Assert.Equal (0, lv.SelectedItem); - - Assert.True (lv.NewKeyDownEvent (Key.B)); - Assert.Equal (1, lv.SelectedItem); - - // There is no keybinding for Key.C so it hits collection navigator i.e. we jump to candle - Assert.True (lv.NewKeyDownEvent (Key.C)); - Assert.Equal (5, lv.SelectedItem); } [Fact] - public void CollectionNavigatorMatcher_KeybindingsOverrideNavigator () + public void HotKey_Command_SetsFocus () { - ObservableCollection source = new () { "apricot", "arm", "bat", "batman", "bates hotel", "candle" }; - ListView lv = new ListView { Source = new ListWrapper (source) }; + var view = new ListView (); - lv.SetFocus (); + view.CanFocus = true; + Assert.False (view.HasFocus); + view.InvokeCommand (Command.HotKey); + Assert.True (view.HasFocus); + } - lv.KeyBindings.Add (Key.B, Command.Down); + [Fact] + public void HotKey_Command_Does_Not_Accept () + { + var listView = new ListView (); + var accepted = false; - Assert.Equal (-1, lv.SelectedItem); + listView.Accepting += OnAccepted; + listView.InvokeCommand (Command.HotKey); - // Keys should be consumed to move down the navigation i.e. to apricot - Assert.True (lv.NewKeyDownEvent (Key.B)); - Assert.Equal (0, lv.SelectedItem); + Assert.False (accepted); - Assert.True (lv.NewKeyDownEvent (Key.B)); + return; + + void OnAccepted (object? sender, CommandEventArgs e) { accepted = true; } + } + + [Fact] + public void Accept_Command_Accepts_and_Opens_Selected_Item () + { + ObservableCollection source = ["One", "Two", "Three"]; + var listView = new ListView { Source = new ListWrapper (source) }; + listView.SelectedItem = 0; + + var accepted = false; + var opened = false; + var selectedValue = string.Empty; + + listView.Accepting += Accepted; + listView.OpenSelectedItem += OpenSelectedItem; + + listView.InvokeCommand (Command.Accept); + + Assert.True (accepted); + Assert.True (opened); + Assert.Equal (source [0], selectedValue); + + return; + + void OpenSelectedItem (object? sender, ListViewItemEventArgs e) + { + opened = true; + selectedValue = e.Value!.ToString (); + } + + void Accepted (object? sender, CommandEventArgs e) { accepted = true; } + } + + [Fact] + public void Accept_Cancel_Event_Prevents_OpenSelectedItem () + { + ObservableCollection source = ["One", "Two", "Three"]; + var listView = new ListView { Source = new ListWrapper (source) }; + listView.SelectedItem = 0; + + var accepted = false; + var opened = false; + var selectedValue = string.Empty; + + listView.Accepting += Accepted; + listView.OpenSelectedItem += OpenSelectedItem; + + listView.InvokeCommand (Command.Accept); + + Assert.True (accepted); + Assert.False (opened); + Assert.Equal (string.Empty, selectedValue); + + return; + + void OpenSelectedItem (object? sender, ListViewItemEventArgs e) + { + opened = true; + selectedValue = e.Value!.ToString (); + } + + void Accepted (object? sender, CommandEventArgs e) + { + accepted = true; + e.Handled = true; + } + } + + [Fact] + public void ListViewProcessKeyReturnValue_WithMultipleCommands () + { + var lv = new ListView { Source = new ListWrapper (["One", "Two", "Three", "Four"]) }; + + Assert.NotNull (lv.Source); + + // first item should be deselected by default + Assert.Null (lv.SelectedItem); + + // bind shift down to move down twice in control + lv.KeyBindings.Add (Key.CursorDown.WithShift, Command.Down, Command.Down); + + Key ev = Key.CursorDown.WithShift; + + Assert.True (lv.NewKeyDownEvent (ev), "The first time we move down 2 it should be possible"); + + // After moving down twice from null we should be at 'Two' Assert.Equal (1, lv.SelectedItem); - // There is no keybinding for Key.C so it hits collection navigator i.e. we jump to candle - Assert.True (lv.NewKeyDownEvent (Key.C)); - Assert.Equal (5, lv.SelectedItem); + // clear the items + lv.SetSource (null); + + // Press key combo again - return should be false this time as none of the Commands are allowable + Assert.False (lv.NewKeyDownEvent (ev), "We cannot move down so will not respond to this"); + } + + [Fact] + public void AllowsMarking_True_SpaceWithShift_SelectsThenDown_SingleSelection () + { + var lv = new ListView { Source = new ListWrapper (["One", "Two", "Three"]) }; + lv.AllowsMarking = true; + lv.AllowsMultipleSelection = false; + + Assert.NotNull (lv.Source); + + // first item should be deselected by default + Assert.Null (lv.SelectedItem); + + // nothing is ticked + Assert.False (lv.Source.IsMarked (0)); + Assert.False (lv.Source.IsMarked (1)); + Assert.False (lv.Source.IsMarked (2)); + + // view should indicate that it has accepted and consumed the event + Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); + + // first item should now be selected + Assert.Equal (0, lv.SelectedItem); + + // none of the items should be ticked + Assert.False (lv.Source.IsMarked (0)); + Assert.False (lv.Source.IsMarked (1)); + Assert.False (lv.Source.IsMarked (2)); + + // Press key combo again + Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); + + // second item should now be selected + Assert.Equal (1, lv.SelectedItem); + + // first item only should be ticked + Assert.True (lv.Source.IsMarked (0)); + Assert.False (lv.Source.IsMarked (1)); + Assert.False (lv.Source.IsMarked (2)); + + // Press key combo again + Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); + Assert.Equal (2, lv.SelectedItem); + Assert.False (lv.Source.IsMarked (0)); + Assert.True (lv.Source.IsMarked (1)); + Assert.False (lv.Source.IsMarked (2)); + + // Press key combo again + Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); + Assert.Equal (2, lv.SelectedItem); // cannot move down any further + Assert.False (lv.Source.IsMarked (0)); + Assert.False (lv.Source.IsMarked (1)); + Assert.True (lv.Source.IsMarked (2)); // but can toggle marked + + // Press key combo again + Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); + Assert.Equal (2, lv.SelectedItem); // cannot move down any further + Assert.False (lv.Source.IsMarked (0)); + Assert.False (lv.Source.IsMarked (1)); + Assert.False (lv.Source.IsMarked (2)); // untoggle toggle marked + } + + [Fact] + public void AllowsMarking_True_SpaceWithShift_SelectsThenDown_MultipleSelection () + { + var lv = new ListView { Source = new ListWrapper (["One", "Two", "Three"]) }; + lv.AllowsMarking = true; + lv.AllowsMultipleSelection = true; + + Assert.NotNull (lv.Source); + + // first item should be deselected by default + Assert.Null (lv.SelectedItem); + + // nothing is ticked + Assert.False (lv.Source.IsMarked (0)); + Assert.False (lv.Source.IsMarked (1)); + Assert.False (lv.Source.IsMarked (2)); + + // view should indicate that it has accepted and consumed the event + Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); + + // first item should now be selected + Assert.Equal (0, lv.SelectedItem); + + // none of the items should be ticked + Assert.False (lv.Source.IsMarked (0)); + Assert.False (lv.Source.IsMarked (1)); + Assert.False (lv.Source.IsMarked (2)); + + // Press key combo again + Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); + + // second item should now be selected + Assert.Equal (1, lv.SelectedItem); + + // first item only should be ticked + Assert.True (lv.Source.IsMarked (0)); + Assert.False (lv.Source.IsMarked (1)); + Assert.False (lv.Source.IsMarked (2)); + + // Press key combo again + Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); + Assert.Equal (2, lv.SelectedItem); + Assert.True (lv.Source.IsMarked (0)); + Assert.True (lv.Source.IsMarked (1)); + Assert.False (lv.Source.IsMarked (2)); + + // Press key combo again + Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); + Assert.Equal (2, lv.SelectedItem); // cannot move down any further + Assert.True (lv.Source.IsMarked (0)); + Assert.True (lv.Source.IsMarked (1)); + Assert.True (lv.Source.IsMarked (2)); // but can toggle marked + + // Press key combo again + Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); + Assert.Equal (2, lv.SelectedItem); // cannot move down any further + Assert.True (lv.Source.IsMarked (0)); + Assert.True (lv.Source.IsMarked (1)); + Assert.False (lv.Source.IsMarked (2)); // untoggle toggle marked + } + + [Fact] + public void ListWrapper_StartsWith () + { + ListWrapper lw = new (["One", "Two", "Three"]); + + Assert.Equal (1, lw.StartsWith ("t")); + Assert.Equal (1, lw.StartsWith ("tw")); + Assert.Equal (2, lw.StartsWith ("th")); + Assert.Equal (1, lw.StartsWith ("T")); + Assert.Equal (1, lw.StartsWith ("TW")); + Assert.Equal (2, lw.StartsWith ("TH")); + + lw = new (["One", "Two", "Three"]); + + Assert.Equal (1, lw.StartsWith ("t")); + Assert.Equal (1, lw.StartsWith ("tw")); + Assert.Equal (2, lw.StartsWith ("th")); + Assert.Equal (1, lw.StartsWith ("T")); + Assert.Equal (1, lw.StartsWith ("TW")); + Assert.Equal (2, lw.StartsWith ("TH")); + } + + [Fact] + public void OnEnter_Does_Not_Throw_Exception () + { + var lv = new ListView (); + var top = new View (); + top.Add (lv); + Exception exception = Record.Exception (() => lv.SetFocus ()); + Assert.Null (exception); + } + + [Fact] + public void SelectedItem_Get_Set () + { + var lv = new ListView { Source = new ListWrapper (["One", "Two", "Three"]) }; + Assert.Null (lv.SelectedItem); + Assert.Throws (() => lv.SelectedItem = 3); + Exception exception = Record.Exception (() => lv.SelectedItem = null); + Assert.Null (exception); + } + + [Fact] + public void SetSource_Preserves_ListWrapper_Instance_If_Not_Null () + { + var lv = new ListView { Source = new ListWrapper (["One", "Two"]) }; + + Assert.NotNull (lv.Source); + + lv.SetSource (null); + Assert.NotNull (lv.Source); + + lv.Source = null; + Assert.Null (lv.Source); + + lv = new () { Source = new ListWrapper (["One", "Two"]) }; + Assert.NotNull (lv.Source); + + lv.SetSourceAsync (null); + Assert.NotNull (lv.Source); + } + + [Fact] + public void SettingEmptyKeybindingThrows () + { + var lv = new ListView { Source = new ListWrapper (["One", "Two", "Three"]) }; + Assert.Throws (() => lv.KeyBindings.Add (Key.Space)); + } + + [Fact] + public void CollectionChanged_Event () + { + var added = 0; + var removed = 0; + ObservableCollection source = []; + var lv = new ListView { Source = new ListWrapper (source) }; + + lv.CollectionChanged += (sender, args) => + { + if (args.Action == NotifyCollectionChangedAction.Add) + { + added++; + } + else if (args.Action == NotifyCollectionChangedAction.Remove) + { + removed++; + } + }; + + for (var i = 0; i < 3; i++) + { + source.Add ($"Item{i}"); + } + + Assert.Equal (3, added); + Assert.Equal (0, removed); + + added = 0; + + for (var i = 0; i < 3; i++) + { + source.Remove (source [0]); + } + + Assert.Equal (0, added); + Assert.Equal (3, removed); + Assert.Empty (source); + } + + [Fact] + public void CollectionChanged_Event_Is_Only_Subscribed_Once () + { + var added = 0; + var removed = 0; + var otherActions = 0; + IList source1 = []; + var lv = new ListView { Source = new ListWrapper (new (source1)) }; + + lv.CollectionChanged += (sender, args) => + { + if (args.Action == NotifyCollectionChangedAction.Add) + { + added++; + } + else if (args.Action == NotifyCollectionChangedAction.Remove) + { + removed++; + } + else + { + otherActions++; + } + }; + + ObservableCollection source2 = []; + lv.Source = new ListWrapper (source2); + ObservableCollection source3 = []; + lv.Source = new ListWrapper (source3); + Assert.Equal (0, added); + Assert.Equal (0, removed); + Assert.Equal (0, otherActions); + + for (var i = 0; i < 3; i++) + { + source1.Add ($"Item{i}"); + source2.Add ($"Item{i}"); + source3.Add ($"Item{i}"); + } + + Assert.Equal (3, added); + Assert.Equal (0, removed); + Assert.Equal (0, otherActions); + + added = 0; + + for (var i = 0; i < 3; i++) + { + source1.Remove (source1 [0]); + source2.Remove (source2 [0]); + source3.Remove (source3 [0]); + } + + Assert.Equal (0, added); + Assert.Equal (3, removed); + Assert.Equal (0, otherActions); + Assert.Empty (source1); + Assert.Empty (source2); + Assert.Empty (source3); + } + + [Fact] + public void CollectionChanged_Event_UnSubscribe_Previous_If_New_Is_Null () + { + var added = 0; + var removed = 0; + var otherActions = 0; + ObservableCollection source1 = []; + var lv = new ListView { Source = new ListWrapper (source1) }; + + lv.CollectionChanged += (sender, args) => + { + if (args.Action == NotifyCollectionChangedAction.Add) + { + added++; + } + else if (args.Action == NotifyCollectionChangedAction.Remove) + { + removed++; + } + else + { + otherActions++; + } + }; + + lv.Source = new ListWrapper (null); + Assert.Equal (0, added); + Assert.Equal (0, removed); + Assert.Equal (0, otherActions); + + for (var i = 0; i < 3; i++) + { + source1.Add ($"Item{i}"); + } + + Assert.Equal (0, added); + Assert.Equal (0, removed); + Assert.Equal (0, otherActions); + + for (var i = 0; i < 3; i++) + { + source1.Remove (source1 [0]); + } + + Assert.Equal (0, added); + Assert.Equal (0, removed); + Assert.Equal (0, otherActions); + Assert.Empty (source1); + } + + [Fact] + public void ListWrapper_CollectionChanged_Event_Is_Only_Subscribed_Once () + { + var added = 0; + var removed = 0; + var otherActions = 0; + ObservableCollection source1 = []; + ListWrapper lw = new (source1); + + lw.CollectionChanged += (sender, args) => + { + if (args.Action == NotifyCollectionChangedAction.Add) + { + added++; + } + else if (args.Action == NotifyCollectionChangedAction.Remove) + { + removed++; + } + else + { + otherActions++; + } + }; + + ObservableCollection source2 = []; + lw = new (source2); + ObservableCollection source3 = []; + lw = new (source3); + Assert.Equal (0, added); + Assert.Equal (0, removed); + Assert.Equal (0, otherActions); + + for (var i = 0; i < 3; i++) + { + source1.Add ($"Item{i}"); + source2.Add ($"Item{i}"); + source3.Add ($"Item{i}"); + } + + Assert.Equal (3, added); + Assert.Equal (0, removed); + Assert.Equal (0, otherActions); + + added = 0; + + for (var i = 0; i < 3; i++) + { + source1.Remove (source1 [0]); + source2.Remove (source2 [0]); + source3.Remove (source3 [0]); + } + + Assert.Equal (0, added); + Assert.Equal (3, removed); + Assert.Equal (0, otherActions); + Assert.Empty (source1); + Assert.Empty (source2); + Assert.Empty (source3); + } + + [Fact] + public void ListWrapper_CollectionChanged_Event_UnSubscribe_Previous_Is_Disposed () + { + var added = 0; + var removed = 0; + var otherActions = 0; + ObservableCollection source1 = []; + ListWrapper lw = new (source1); + + lw.CollectionChanged += Lw_CollectionChanged; + + lw.Dispose (); + lw = new (null); + Assert.Equal (0, lw.Count); + Assert.Equal (0, added); + Assert.Equal (0, removed); + Assert.Equal (0, otherActions); + + for (var i = 0; i < 3; i++) + { + source1.Add ($"Item{i}"); + } + + Assert.Equal (0, added); + Assert.Equal (0, removed); + Assert.Equal (0, otherActions); + + for (var i = 0; i < 3; i++) + { + source1.Remove (source1 [0]); + } + + Assert.Equal (0, added); + Assert.Equal (0, removed); + Assert.Equal (0, otherActions); + Assert.Empty (source1); + + void Lw_CollectionChanged (object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + added++; + } + } + } + + [Fact] + public void ListWrapper_SuspendCollectionChangedEvent_ResumeSuspendCollectionChangedEvent_Tests () + { + var added = 0; + ObservableCollection source = []; + ListWrapper lw = new (source); + + lw.CollectionChanged += Lw_CollectionChanged; + + lw.SuspendCollectionChangedEvent = true; + + for (var i = 0; i < 3; i++) + { + source.Add ($"Item{i}"); + } + + Assert.Equal (0, added); + Assert.Equal (3, lw.Count); + Assert.Equal (3, source.Count); + + lw.SuspendCollectionChangedEvent = false; + + for (var i = 3; i < 6; i++) + { + source.Add ($"Item{i}"); + } + + Assert.Equal (3, added); + Assert.Equal (6, lw.Count); + Assert.Equal (6, source.Count); + + void Lw_CollectionChanged (object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + added++; + } + } + } + + [Fact] + public void ListView_SuspendCollectionChangedEvent_ResumeSuspendCollectionChangedEvent_Tests () + { + var added = 0; + ObservableCollection source = []; + var lv = new ListView { Source = new ListWrapper (source) }; + + lv.CollectionChanged += Lw_CollectionChanged; + + lv.SuspendCollectionChangedEvent (); + + for (var i = 0; i < 3; i++) + { + source.Add ($"Item{i}"); + } + + Assert.Equal (0, added); + Assert.Equal (3, lv.Source.Count); + Assert.Equal (3, source.Count); + + lv.ResumeSuspendCollectionChangedEvent (); + + for (var i = 3; i < 6; i++) + { + source.Add ($"Item{i}"); + } + + Assert.Equal (3, added); + Assert.Equal (6, lv.Source.Count); + Assert.Equal (6, source.Count); + + void Lw_CollectionChanged (object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + added++; + } + } + } + + #endregion + + [Fact] + public void Clicking_On_Border_Is_Ignored () + { + IApplication? app = Application.Create (); + app.Init ("fake"); + + var selected = ""; + + var lv = new ListView + { + Height = 5, + Width = 7, + BorderStyle = LineStyle.Single + }; + lv.SetSource (["One", "Two", "Three", "Four"]); + lv.SelectedItemChanged += (s, e) => selected = e.Value.ToString (); + var top = new Toplevel (); + top.Add (lv); + app.Begin (top); + + //AutoInitShutdownAttribute.RunIteration (); + + Assert.Equal (new (1), lv.Border!.Thickness); + Assert.Null (lv.SelectedItem); + Assert.Equal ("", lv.Text); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌─────┐ +│One │ +│Two │ +│Three│ +└─────┘", + _output, app?.Driver); + + app?.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Clicked }); + Assert.Equal ("", selected); + Assert.Null (lv.SelectedItem); + + app?.Mouse.RaiseMouseEvent ( + new () + { + ScreenPosition = new (1, 1), Flags = MouseFlags.Button1Clicked + }); + Assert.Equal ("One", selected); + Assert.Equal (0, lv.SelectedItem); + + app?.Mouse.RaiseMouseEvent ( + new () + { + ScreenPosition = new (1, 2), Flags = MouseFlags.Button1Clicked + }); + Assert.Equal ("Two", selected); + Assert.Equal (1, lv.SelectedItem); + + app?.Mouse.RaiseMouseEvent ( + new () + { + ScreenPosition = new (1, 3), Flags = MouseFlags.Button1Clicked + }); + Assert.Equal ("Three", selected); + Assert.Equal (2, lv.SelectedItem); + + app?.Mouse.RaiseMouseEvent ( + new () + { + ScreenPosition = new (1, 4), Flags = MouseFlags.Button1Clicked + }); + Assert.Equal ("Three", selected); + Assert.Equal (2, lv.SelectedItem); + top.Dispose (); + + app.Shutdown (); + } + + [Fact] + public void Ensures_Visibility_SelectedItem_On_MoveDown_And_MoveUp () + { + IApplication? app = Application.Create (); + app.Init ("fake"); + app.Driver?.SetScreenSize (12, 12); + + ObservableCollection source = []; + + for (var i = 0; i < 20; i++) + { + source.Add ($"Line{i}"); + } + + var lv = new ListView { Width = Dim.Fill (), Height = Dim.Fill (), Source = new ListWrapper (source) }; + var win = new Window (); + win.Add (lv); + var top = new Toplevel (); + top.Add (win); + app.Begin (top); + + Assert.Null (lv.SelectedItem); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────┐ +│Line0 │ +│Line1 │ +│Line2 │ +│Line3 │ +│Line4 │ +│Line5 │ +│Line6 │ +│Line7 │ +│Line8 │ +│Line9 │ +└──────────┘", + _output, app.Driver + ); + + Assert.True (lv.ScrollVertical (10)); + app.LayoutAndDraw (); + Assert.Null (lv.SelectedItem); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────┐ +│Line10 │ +│Line11 │ +│Line12 │ +│Line13 │ +│Line14 │ +│Line15 │ +│Line16 │ +│Line17 │ +│Line18 │ +│Line19 │ +└──────────┘", + _output, app.Driver + ); + + Assert.True (lv.MoveDown ()); + app.LayoutAndDraw (); + Assert.Equal (0, lv.SelectedItem); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────┐ +│Line0 │ +│Line1 │ +│Line2 │ +│Line3 │ +│Line4 │ +│Line5 │ +│Line6 │ +│Line7 │ +│Line8 │ +│Line9 │ +└──────────┘", + _output, app.Driver + ); + + Assert.True (lv.MoveEnd ()); + app.LayoutAndDraw (); + Assert.Equal (19, lv.SelectedItem); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────┐ +│Line10 │ +│Line11 │ +│Line12 │ +│Line13 │ +│Line14 │ +│Line15 │ +│Line16 │ +│Line17 │ +│Line18 │ +│Line19 │ +└──────────┘", + _output, app.Driver + ); + + Assert.True (lv.ScrollVertical (-20)); + app.LayoutAndDraw (); + Assert.Equal (19, lv.SelectedItem); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────┐ +│Line0 │ +│Line1 │ +│Line2 │ +│Line3 │ +│Line4 │ +│Line5 │ +│Line6 │ +│Line7 │ +│Line8 │ +│Line9 │ +└──────────┘", + _output, app.Driver + ); + + Assert.True (lv.MoveDown ()); + app.LayoutAndDraw (); + Assert.Equal (19, lv.SelectedItem); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────┐ +│Line10 │ +│Line11 │ +│Line12 │ +│Line13 │ +│Line14 │ +│Line15 │ +│Line16 │ +│Line17 │ +│Line18 │ +│Line19 │ +└──────────┘", + _output, app.Driver + ); + + Assert.True (lv.ScrollVertical (-20)); + app.LayoutAndDraw (); + Assert.Equal (19, lv.SelectedItem); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────┐ +│Line0 │ +│Line1 │ +│Line2 │ +│Line3 │ +│Line4 │ +│Line5 │ +│Line6 │ +│Line7 │ +│Line8 │ +│Line9 │ +└──────────┘", + _output, app.Driver + ); + + Assert.True (lv.MoveDown ()); + app.LayoutAndDraw (); + Assert.Equal (19, lv.SelectedItem); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────┐ +│Line10 │ +│Line11 │ +│Line12 │ +│Line13 │ +│Line14 │ +│Line15 │ +│Line16 │ +│Line17 │ +│Line18 │ +│Line19 │ +└──────────┘", + _output, app.Driver + ); + + Assert.True (lv.MoveHome ()); + app.LayoutAndDraw (); + Assert.Equal (0, lv.SelectedItem); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────┐ +│Line0 │ +│Line1 │ +│Line2 │ +│Line3 │ +│Line4 │ +│Line5 │ +│Line6 │ +│Line7 │ +│Line8 │ +│Line9 │ +└──────────┘", + _output, app.Driver + ); + + Assert.True (lv.ScrollVertical (20)); + app.LayoutAndDraw (); + Assert.Equal (0, lv.SelectedItem); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────┐ +│Line19 │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────┘", + _output, app.Driver + ); + + Assert.True (lv.MoveUp ()); + app.LayoutAndDraw (); + Assert.Equal (0, lv.SelectedItem); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────┐ +│Line0 │ +│Line1 │ +│Line2 │ +│Line3 │ +│Line4 │ +│Line5 │ +│Line6 │ +│Line7 │ +│Line8 │ +│Line9 │ +└──────────┘", + _output, app.Driver + ); + top.Dispose (); + app.Shutdown (); + } + + [Fact] + public void EnsureSelectedItemVisible_SelectedItem () + { + IApplication? app = Application.Create (); + app.Init ("fake"); + app.Driver?.SetScreenSize (12, 12); + + ObservableCollection source = []; + + for (var i = 0; i < 10; i++) + { + source.Add ($"Item {i}"); + } + + var lv = new ListView { Width = 10, Height = 5, Source = new ListWrapper (source) }; + var top = new Toplevel (); + top.Add (lv); + app.Begin (top); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +Item 0 +Item 1 +Item 2 +Item 3 +Item 4", + _output, app.Driver + ); + + // EnsureSelectedItemVisible is auto enabled on the OnSelectedChanged + lv.SelectedItem = 6; + app.LayoutAndDraw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +Item 2 +Item 3 +Item 4 +Item 5 +Item 6", + _output, app.Driver + ); + top.Dispose (); + app.Shutdown (); + } + + [Fact] + public void EnsureSelectedItemVisible_Top () + { + IApplication? app = Application.Create (); + app.Init ("fake"); + IDriver? driver = app.Driver; + driver.SetScreenSize (8, 2); + + ObservableCollection source = ["First", "Second"]; + var lv = new ListView { Width = Dim.Fill (), Height = 1, Source = new ListWrapper (source) }; + lv.SelectedItem = 1; + var top = new Toplevel (); + top.Add (lv); + app.Begin (top); + + Assert.Equal ("Second ", GetContents (0)); + Assert.Equal (new (' ', 7), GetContents (1)); + + lv.MoveUp (); + lv.Draw (); + + Assert.Equal ("First ", GetContents (0)); + Assert.Equal (new (' ', 7), GetContents (1)); + + string GetContents (int line) + { + var item = ""; + + for (var i = 0; i < 7; i++) + { + item += app.Driver?.Contents [line, i].Rune; + } + + return item; + } + + top.Dispose (); + app.Shutdown (); + } + + [Fact] + public void LeftItem_TopItem_Tests () + { + IApplication? app = Application.Create (); + app.Init ("fake"); + app.Driver?.SetScreenSize (12, 12); + + ObservableCollection source = []; + + for (var i = 0; i < 5; i++) + { + source.Add ($"Item {i}"); + } + + var lv = new ListView + { + X = 1, + Source = new ListWrapper (source) + }; + lv.Height = lv.Source.Count; + lv.Width = lv.MaxLength; + var top = new Toplevel (); + top.Add (lv); + app.Begin (top); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" + Item 0 + Item 1 + Item 2 + Item 3 + Item 4", + _output, app.Driver); + + lv.LeftItem = 1; + lv.TopItem = 1; + app.LayoutAndDraw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" + tem 1 + tem 2 + tem 3 + tem 4", + _output, app.Driver); + top.Dispose (); + app.Shutdown (); + } + + [Fact] + public void RowRender_Event () + { + IApplication? app = Application.Create (); + app.Init ("fake"); + + var rendered = false; + ObservableCollection source = ["one", "two", "three"]; + var lv = new ListView { Width = Dim.Fill (), Height = Dim.Fill () }; + lv.RowRender += (s, _) => rendered = true; + var top = new Toplevel (); + top.Add (lv); + app.Begin (top); + Assert.False (rendered); + + lv.SetSource (source); + lv.Draw (); + Assert.True (rendered); + top.Dispose (); + app.Shutdown (); + } + + [Fact] + public void Vertical_ScrollBar_Hides_And_Shows_As_Needed () + { + IApplication? app = Application.Create (); + app.Init ("fake"); + + var lv = new ListView + { + Width = 10, + Height = 3 + }; + lv.VerticalScrollBar.AutoShow = true; + lv.SetSource (["One", "Two", "Three", "Four", "Five"]); + var top = new Toplevel (); + top.Add (lv); + app.Begin (top); + + Assert.True (lv.VerticalScrollBar.Visible); + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +One ▲ +Two █ +Three ▼", + _output, app?.Driver); + + lv.Height = 5; + app?.LayoutAndDraw (); + + Assert.False (lv.VerticalScrollBar.Visible); + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +One +Two +Three +Four +Five ", + _output, app?.Driver); + top.Dispose (); + app?.Shutdown (); + } + + [Fact] + public void Mouse_Wheel_Scrolls () + { + IApplication? app = Application.Create (); + app.Init ("fake"); + + var lv = new ListView + { + Width = 10, + Height = 3, + }; + lv.SetSource (["One", "Two", "Three", "Four", "Five"]); + var top = new Toplevel (); + top.Add (lv); + app.Begin (top); + + // Initially, we are at the top. + Assert.Equal (0, lv.TopItem); + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +One +Two +Three", + _output, app?.Driver); + + // Scroll down + app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledDown }); + app.LayoutAndDraw (); + Assert.Equal (1, lv.TopItem); + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +Two +Three +Four ", + _output, app?.Driver); + + // Scroll up + app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledUp }); + app.LayoutAndDraw (); + Assert.Equal (0, lv.TopItem); + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +One +Two +Three", + _output, app?.Driver); + + top.Dispose (); + app.Shutdown (); + } + + [Fact] + public void SelectedItem_With_Source_Null_Does_Nothing () + { + var lv = new ListView (); + Assert.Null (lv.Source); + + // should not throw + lv.SelectedItem = 0; + + Assert.Null (lv.SelectedItem); + } + + [Fact] + public void Horizontal_Scroll () + { + IApplication? app = Application.Create (); + app.Init ("fake"); + + var lv = new ListView + { + Width = 10, + Height = 3, + }; + lv.SetSource (["One", "Two", "Three - long", "Four", "Five"]); + var top = new Toplevel (); + top.Add (lv); + app.Begin (top); + + Assert.Equal (0, lv.LeftItem); + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +One +Two +Three - lo", + _output, app?.Driver); + + lv.ScrollHorizontal (1); + app.LayoutAndDraw (); + Assert.Equal (1, lv.LeftItem); + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +ne +wo +hree - lon", + _output, app?.Driver); + + // Scroll right with mouse + app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledRight }); + app.LayoutAndDraw (); + Assert.Equal (2, lv.LeftItem); + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +e +o +ree - long", + _output, app?.Driver); + + // Scroll left with mouse + app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledLeft }); + app.LayoutAndDraw (); + Assert.Equal (1, lv.LeftItem); + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +ne +wo +hree - lon", + _output, app?.Driver); + + top.Dispose (); + app.Shutdown (); + } + + [Fact] + public async Task SetSourceAsync_SetsSource () + { + var lv = new ListView (); + var source = new ObservableCollection { "One", "Two", "Three" }; + + await lv.SetSourceAsync (source); + + Assert.NotNull (lv.Source); + Assert.Equal (3, lv.Source.Count); + } + + [Fact] + public void AllowsMultipleSelection_Set_To_False_Unmarks_All_But_Selected () + { + var lv = new ListView { AllowsMarking = true, AllowsMultipleSelection = true }; + var source = new ListWrapper (["One", "Two", "Three"]); + lv.Source = source; + + lv.SelectedItem = 0; + source.SetMark (0, true); + source.SetMark (1, true); + source.SetMark (2, true); + + Assert.True (source.IsMarked (0)); + Assert.True (source.IsMarked (1)); + Assert.True (source.IsMarked (2)); + + lv.AllowsMultipleSelection = false; + + Assert.True (source.IsMarked (0)); + Assert.False (source.IsMarked (1)); + Assert.False (source.IsMarked (2)); + } + + [Fact] + public void Source_CollectionChanged_Remove () + { + var source = new ObservableCollection { "One", "Two", "Three" }; + var lv = new ListView { Source = new ListWrapper (source) }; + + lv.SelectedItem = 2; + Assert.Equal (2, lv.SelectedItem); + Assert.Equal (3, lv.Source.Count); + + source.RemoveAt (0); + + Assert.Equal (2, lv.Source.Count); + Assert.Equal (1, lv.SelectedItem); + + source.RemoveAt (1); + Assert.Equal (1, lv.Source.Count); + Assert.Equal (0, lv.SelectedItem); } } diff --git a/Tests/UnitTestsParallelizable/Views/NumericUpDownTests.cs b/Tests/UnitTestsParallelizable/Views/NumericUpDownTests.cs index 4a9975244..7e72ebe5f 100644 --- a/Tests/UnitTestsParallelizable/Views/NumericUpDownTests.cs +++ b/Tests/UnitTestsParallelizable/Views/NumericUpDownTests.cs @@ -112,7 +112,7 @@ public class NumericUpDownTests public void WhenCreated_ShouldHaveDefaultWidthAndHeight_int () { NumericUpDown numericUpDown = new (); - numericUpDown.SetRelativeLayout (Application.Screen.Size); + numericUpDown.SetRelativeLayout (new (100, 100)); Assert.Equal (3, numericUpDown.Frame.Width); Assert.Equal (1, numericUpDown.Frame.Height); @@ -122,7 +122,7 @@ public class NumericUpDownTests public void WhenCreated_ShouldHaveDefaultWidthAndHeight_float () { NumericUpDown numericUpDown = new (); - numericUpDown.SetRelativeLayout (Application.Screen.Size); + numericUpDown.SetRelativeLayout (new (100, 100)); Assert.Equal (3, numericUpDown.Frame.Width); Assert.Equal (1, numericUpDown.Frame.Height); @@ -132,7 +132,7 @@ public class NumericUpDownTests public void WhenCreated_ShouldHaveDefaultWidthAndHeight_double () { NumericUpDown numericUpDown = new (); - numericUpDown.SetRelativeLayout (Application.Screen.Size); + numericUpDown.SetRelativeLayout (new (100, 100)); Assert.Equal (3, numericUpDown.Frame.Width); Assert.Equal (1, numericUpDown.Frame.Height); @@ -142,7 +142,7 @@ public class NumericUpDownTests public void WhenCreated_ShouldHaveDefaultWidthAndHeight_long () { NumericUpDown numericUpDown = new (); - numericUpDown.SetRelativeLayout (Application.Screen.Size); + numericUpDown.SetRelativeLayout (new (100, 100)); Assert.Equal (3, numericUpDown.Frame.Width); Assert.Equal (1, numericUpDown.Frame.Height); @@ -152,7 +152,7 @@ public class NumericUpDownTests public void WhenCreated_ShouldHaveDefaultWidthAndHeight_decimal () { NumericUpDown numericUpDown = new (); - numericUpDown.SetRelativeLayout (Application.Screen.Size); + numericUpDown.SetRelativeLayout (new (100, 100)); Assert.Equal (3, numericUpDown.Frame.Width); Assert.Equal (1, numericUpDown.Frame.Height); diff --git a/docfx/docs/application.md b/docfx/docs/application.md index b999b1fc0..298cf6ff5 100644 --- a/docfx/docs/application.md +++ b/docfx/docs/application.md @@ -8,7 +8,7 @@ Terminal.Gui v2 uses an instance-based application architecture that decouples v graph TB subgraph ViewTree["View Hierarchy (SuperView/SubView)"] direction TB - Top[Application.Current
Window] + Top[app.Current
Window] Menu[MenuBar] Status[StatusBar] Content[Content View] @@ -22,7 +22,7 @@ graph TB Content --> Button2 end - subgraph Stack["Application.SessionStack"] + subgraph Stack["app.SessionStack"] direction TB S1[Window
Currently Active] S2[Previous Toplevel
Waiting] @@ -41,7 +41,7 @@ graph TB ```mermaid sequenceDiagram - participant App as Application + participant App as IApplication participant Main as Main Window participant Dialog as Dialog @@ -68,24 +68,29 @@ sequenceDiagram ### Instance-Based vs Static -**Terminal.Gui v2** has transitioned from a static singleton pattern to an instance-based architecture: +**Terminal.Gui v2** supports both static and instance-based patterns. The static `Application` class is marked obsolete but still functional for backward compatibility. The recommended pattern is to use `Application.Create()` to get an `IApplication` instance: ```csharp -// OLD (v1 / early v2 - now obsolete): +// OLD (v1 / early v2 - still works but obsolete): Application.Init(); -Application.Top.Add(myView); -Application.Run(); +var top = new Toplevel(); +top.Add(myView); +Application.Run(top); +top.Dispose(); Application.Shutdown(); -// NEW (v2 instance-based): -var app = Application.Create (); +// NEW (v2 recommended - instance-based): +var app = Application.Create(); app.Init(); var top = new Toplevel(); top.Add(myView); app.Run(top); +top.Dispose(); app.Shutdown(); ``` +**Note:** The static `Application` class delegates to `ApplicationImpl.Instance` (a singleton). `Application.Create()` creates a **new** `ApplicationImpl` instance, enabling multiple application contexts and better testability. + ### View.App Property Every view now has an `App` property that references its application context: @@ -226,19 +231,23 @@ int sessionCount = App?.SessionStack.Count ?? 0; ## Migration from Static Application -The static `Application` class now delegates to `ApplicationImpl.Instance` and is marked obsolete: +The static `Application` class delegates to `ApplicationImpl.Instance` (a singleton) and is marked obsolete. All static methods and properties are marked with `[Obsolete]` but remain functional for backward compatibility: ```csharp -public static class Application +public static partial class Application { - [Obsolete("Use ApplicationImpl.Instance.Current or view.App?.Current")] - public static Toplevel? Current => Instance?.Current; + [Obsolete("The legacy static Application object is going away.")] + public static Toplevel? Current => ApplicationImpl.Instance.Current; - [Obsolete("Use ApplicationImpl.Instance.SessionStack or view.App?.SessionStack")] - public static ConcurrentStack SessionStack => Instance?.SessionStack ?? new(); + [Obsolete("The legacy static Application object is going away.")] + public static ConcurrentStack SessionStack => ApplicationImpl.Instance.SessionStack; + + // ... other obsolete static members } ``` +**Important:** The static `Application` class uses a singleton (`ApplicationImpl.Instance`), while `Application.Create()` creates new instances. For new code, prefer the instance-based pattern using `Application.Create()`. + ### Migration Strategies **Strategy 1: Use View.App** @@ -472,16 +481,19 @@ public class Service } ``` -### DON'T: Assume Application.Instance Exists +### DON'T: Use Static Application in New Code ```csharp -❌ AVOID: -public class Service +❌ AVOID (obsolete pattern): +public void Refresh() { - public void DoWork() - { - var app = Application.Instance; // Might be null! - } + Application.Current?.SetNeedsDraw(); // Obsolete static access +} + +✅ PREFERRED: +public void Refresh() +{ + App?.Current?.SetNeedsDraw(); // Use View.App property } ``` diff --git a/docfx/docs/config.md b/docfx/docs/config.md index 4a549d5ce..e260cad34 100644 --- a/docfx/docs/config.md +++ b/docfx/docs/config.md @@ -459,7 +459,8 @@ ThemeManager.ThemeChanged += (sender, e) => { // Theme has changed // Refresh all views to use new theme - Application.Current?.SetNeedsDraw(); + // From within a View, use: App?.Current?.SetNeedsDraw(); + // Or access via IApplication instance: app.Current?.SetNeedsDraw(); }; ``` diff --git a/docfx/docs/index.md b/docfx/docs/index.md index 830ec3c19..437d34ffc 100644 --- a/docfx/docs/index.md +++ b/docfx/docs/index.md @@ -13,10 +13,13 @@ Welcome to the Terminal.Gui documentation! This comprehensive guide covers every - [Getting Started](~/docs/getting-started.md) - Quick start guide to create your first Terminal.Gui application - [Migrating from v1 to v2](~/docs/migratingfromv1.md) - Complete guide for upgrading existing applications - [What's New in v2](~/docs/newinv2.md) - Overview of new features and improvements +- [Showcase](~/docs/showcase.md) - Showcase of TUI apps built with Terminal.Gui ## Deep Dives - [ANSI Response Parser](~/docs/ansiparser.md) - Terminal sequence parsing and state management +- [Application](~/docs/application.md) - Application lifecycle, initialization, and main loop +- [Arrangement](~/docs/arrangement.md) - View arrangement and positioning strategies - [Cancellable Work Pattern](~/docs/cancellable-work-pattern.md) - Core design pattern for extensible workflows - [Character Map Scenario](~/docs/CharacterMap.md) - Complex drawing, scrolling, and Unicode rendering example - [Command System](~/docs/command.md) - Command execution, key bindings, and the Selecting/Accepting concepts @@ -24,6 +27,7 @@ Welcome to the Terminal.Gui documentation! This comprehensive guide covers every - [Cross-Platform Driver Model](~/docs/drivers.md) - Platform abstraction and console driver architecture - [Cursor System](~/docs/cursor.md) - Modern cursor management and positioning (proposed design) - [Dim.Auto](~/docs/dimauto.md) - Automatic view sizing based on content +- [Drawing](~/docs/drawing.md) - Drawing primitives, rendering, and graphics operations - [Events](~/docs/events.md) - Event patterns and handling throughout the framework - [Keyboard Input](~/docs/keyboard.md) - Key handling, bindings, commands, and shortcuts - [Layout System](~/docs/layout.md) - View positioning, sizing, and arrangement @@ -33,7 +37,11 @@ Welcome to the Terminal.Gui documentation! This comprehensive guide covers every - [Mouse Input](~/docs/mouse.md) - Mouse event handling and interaction patterns - [Navigation](~/docs/navigation.md) - Focus management, keyboard navigation, and accessibility - [Popovers](~/docs/Popovers.md) - Drawing outside viewport boundaries for menus and popups +- [Scheme](~/docs/scheme.md) - Color schemes, styling, and visual theming - [Scrolling](~/docs/scrolling.md) - Built-in scrolling, virtual content areas, and scroll bars +- [TableView](~/docs/tableview.md) - Table view component, data binding, and column management +- [TreeView](~/docs/treeview.md) - Tree view component, hierarchical data, and node management +- [View](~/docs/View.md) - Base view class, view hierarchy, and core view functionality ## API Reference diff --git a/docfx/docs/migratingfromv1.md b/docfx/docs/migratingfromv1.md index 8a459e0a0..d7fd2c0b0 100644 --- a/docfx/docs/migratingfromv1.md +++ b/docfx/docs/migratingfromv1.md @@ -93,6 +93,74 @@ In v1, @Terminal.Gui./Terminal.Gui.Application.Init) automatically created a top * Update any code that assumes `Application.Init` automatically created a toplevel view and set `Application.Current`. * Update any code that assumes `Application.Init` automatically disposed of the toplevel view when the application exited. +## Instance-Based Application Architecture + +See the [Application Deep Dive](application.md) for complete details on the new application architecture. + +Terminal.Gui v2 introduces an instance-based application architecture. While the static `Application` class still works (marked obsolete), the recommended pattern is to use `Application.Create()` to get an `IApplication` instance. + +### Key Changes + +- **Static Application is Obsolete**: The static `Application` class delegates to `ApplicationImpl.Instance` (a singleton) and is marked `[Obsolete]` but remains functional for backward compatibility. +- **Recommended Pattern**: Use `Application.Create()` to get a new `IApplication` instance for better testability and multiple application contexts. +- **View.App Property**: Every view has an `App` property that references its `IApplication` context, enabling views to access application services without static dependencies. + +### Migration Strategies + +**Option 1: Continue Using Static Application (Backward Compatible)** + +The static `Application` class still works, so existing v1 code can continue to work with minimal changes: + +```csharp +// v1 code (still works in v2, but obsolete) +Application.Init(); +var top = new Toplevel(); +top.Add(myView); +Application.Run(top); +top.Dispose(); +Application.Shutdown(); +``` + +**Option 2: Migrate to Instance-Based Pattern (Recommended)** + +For new code or when refactoring, use the instance-based pattern: + +```csharp +// v2 recommended pattern +var app = Application.Create(); +app.Init(); +var top = new Toplevel(); +top.Add(myView); +app.Run(top); +top.Dispose(); +app.Shutdown(); +``` + +**Option 3: Use View.App Property** + +When accessing application services from within views, use the `App` property instead of static `Application`: + +```csharp +// OLD (v1 / obsolete static): +public void Refresh() +{ + Application.Current?.SetNeedsDraw(); +} + +// NEW (v2 - use View.App): +public void Refresh() +{ + App?.Current?.SetNeedsDraw(); +} +``` + +### Benefits of Instance-Based Architecture + +- **Testability**: Views can be tested without `Application.Init()` by setting `view.App = mockApp` +- **Multiple Contexts**: Multiple `IApplication` instances can coexist +- **Clear Ownership**: Views explicitly know their application context +- **Reduced Global State**: Less reliance on static singletons + ## @Terminal.Gui.Pos and @Terminal.Gui.Dim types now adhere to standard C# idioms * In v1, the @Terminal.Gui.Pos and @Terminal.Gui.Dim types (e.g. @Terminal.Gui.Pos.PosView) were nested classes and marked @Terminal.Gui.internal. In v2, they are no longer nested, and have appropriate public APIs. diff --git a/docfx/docs/multitasking.md b/docfx/docs/multitasking.md index a4e98b8c5..165913925 100644 --- a/docfx/docs/multitasking.md +++ b/docfx/docs/multitasking.md @@ -9,7 +9,7 @@ Terminal.Gui applications run on a single main thread with an event loop that pr Terminal.Gui follows the standard UI toolkit pattern where **all UI operations must happen on the main thread**. Attempting to modify views or their properties from background threads will result in undefined behavior and potential crashes. ### The Golden Rule -> Always use `Application.Invoke()` to update the UI from background threads. +> Always use `Application.Invoke()` (static, obsolete) or `app.Invoke()` (instance-based, recommended) to update the UI from background threads. From within a View, use `App?.Invoke()`. ## Background Operations @@ -47,6 +47,7 @@ private async void LoadDataButton_Clicked() When working with traditional threading APIs or when async/await isn't suitable: +**From within a View (recommended):** ```csharp private void StartBackgroundWork() { @@ -58,14 +59,14 @@ private void StartBackgroundWork() Thread.Sleep(50); // Simulate work // Marshal back to main thread for UI updates - Application.Invoke(() => + App?.Invoke(() => { progressBar.Fraction = i / 100f; statusLabel.Text = $"Progress: {i}%"; }); } - Application.Invoke(() => + App?.Invoke(() => { statusLabel.Text = "Complete!"; }); @@ -73,6 +74,41 @@ private void StartBackgroundWork() } ``` +**Using IApplication instance (recommended):** +```csharp +var app = Application.Create(); +app.Init(); + +private void StartBackgroundWork(IApplication app) +{ + Task.Run(() => + { + // This code runs on a background thread + for (int i = 0; i <= 100; i++) + { + Thread.Sleep(50); // Simulate work + + // Marshal back to main thread for UI updates + app.Invoke(() => + { + progressBar.Fraction = i / 100f; + statusLabel.Text = $"Progress: {i}%"; + }); + } + + app.Invoke(() => + { + statusLabel.Text = "Complete!"; + }); + }); +} +``` + +**Using static Application (obsolete but still works):** +```csharp +Application.Invoke(() => { /* ... */ }); +``` + ## Timers Use timers for periodic updates like clocks, status refreshes, or animations: @@ -89,10 +125,11 @@ public class ClockView : View Add(timeLabel); // Update every second - timerToken = Application.AddTimeout( + // Use App?.AddTimeout() when available, or Application.AddTimeout() (obsolete) + timerToken = App?.AddTimeout( TimeSpan.FromSeconds(1), UpdateTime - ); + ) ?? Application.AddTimeout(TimeSpan.FromSeconds(1), UpdateTime); } private bool UpdateTime() @@ -105,7 +142,7 @@ public class ClockView : View { if (disposing && timerToken != null) { - Application.RemoveTimeout(timerToken); + App?.RemoveTimeout(timerToken) ?? Application.RemoveTimeout(timerToken); } base.Dispose(disposing); } @@ -220,6 +257,13 @@ Task.Run(() => ### ❌ Don't: Forget to clean up timers ```csharp // Memory leak - timer keeps running after view is disposed +// From within a View: +App?.AddTimeout(TimeSpan.FromSeconds(1), UpdateStatus); + +// Or with IApplication instance: +app.AddTimeout(TimeSpan.FromSeconds(1), UpdateStatus); + +// Or static (obsolete but works): Application.AddTimeout(TimeSpan.FromSeconds(1), UpdateStatus); ``` diff --git a/docfx/docs/navigation.md b/docfx/docs/navigation.md index 7fde06795..2c5c2391a 100644 --- a/docfx/docs/navigation.md +++ b/docfx/docs/navigation.md @@ -176,25 +176,30 @@ The @Terminal.Gui.App.ApplicationNavigation.AdvanceFocus method causes the focus The implementation is simple: ```cs -return Application.Current?.AdvanceFocus (direction, behavior); +return app.Current?.AdvanceFocus (direction, behavior); ``` -This method is called from the `Command` handlers bound to the application-scoped keybindings created during `Application.Init`. It is `public` as a convenience. +This method is called from the `Command` handlers bound to the application-scoped keybindings created during `app.Init()`. It is `public` as a convenience. + +**Note:** When accessing from within a View, use `App?.Current` instead of `Application.Current` (which is obsolete). This method replaces about a dozen functions in v1 (scattered across `Application` and `Toplevel`). ### Application Navigation Examples ```csharp +var app = Application.Create(); +app.Init(); + // Listen for global focus changes -Application.Navigation.FocusedChanged += (sender, e) => +app.Navigation.FocusedChanged += (sender, e) => { - var focused = Application.Navigation.GetFocused(); + var focused = app.Navigation.GetFocused(); StatusBar.Text = $"Focused: {focused?.GetType().Name ?? "None"}"; }; // Prevent certain views from getting focus -Application.Navigation.FocusedChanging += (sender, e) => +app.Navigation.FocusedChanging += (sender, e) => { if (e.NewView is SomeRestrictedView) { diff --git a/docfx/docs/newinv2.md b/docfx/docs/newinv2.md index e416dd754..43dec8e87 100644 --- a/docfx/docs/newinv2.md +++ b/docfx/docs/newinv2.md @@ -15,6 +15,47 @@ Terminal.Gui v2 represents a fundamental rethinking of the library's architectur This architectural shift has resulted in the removal of thousands of lines of redundant or overly complex code from v1, replaced with cleaner, more focused implementations. +## Instance-Based Application Architecture + +See the [Application Deep Dive](application.md) for complete details on the new application architecture. + +Terminal.Gui v2 introduces an instance-based application architecture that decouples views from global application state, dramatically improving testability and enabling multiple application contexts. + +### Key Changes + +- **Instance-Based Pattern**: The recommended pattern is to use `Application.Create()` to get an `IApplication` instance, rather than using the static `Application` class (which is marked obsolete but still functional for backward compatibility). +- **View.App Property**: Every view now has an `App` property that references its `IApplication` context, enabling views to access application services without static dependencies. +- **Session Management**: Applications manage sessions through `Begin()` and `End()` methods, with a `SessionStack` tracking nested sessions and `Current` representing the active session. +- **Improved Testability**: Views can be tested in isolation by setting their `App` property to a mock `IApplication`, eliminating the need for `Application.Init()` in unit tests. + +### Example Usage + +```csharp +// Recommended v2 pattern (instance-based) +var app = Application.Create(); +app.Init(); +var top = new Toplevel { Title = "My App" }; +top.Add(myView); +app.Run(top); +top.Dispose(); +app.Shutdown(); + +// Static pattern (obsolete but still works) +Application.Init(); +var top = new Toplevel { Title = "My App" }; +top.Add(myView); +Application.Run(top); +top.Dispose(); +Application.Shutdown(); +``` + +### Benefits + +- **Testability**: Views can be tested without initializing the entire application +- **Multiple Contexts**: Multiple `IApplication` instances can coexist (useful for testing or complex scenarios) +- **Clear Ownership**: Views explicitly know their application context via the `App` property +- **Reduced Global State**: Less reliance on static singletons improves code maintainability + ## Modern Look & Feel - Technical Details ### TrueColor Support diff --git a/docfx/docs/toc.yml b/docfx/docs/toc.yml index 8acb4573f..c25192060 100644 --- a/docfx/docs/toc.yml +++ b/docfx/docs/toc.yml @@ -2,10 +2,16 @@ href: index.md - name: Getting Started href: getting-started.md +- name: Showcase + href: showcase.md - name: What's new in v2 href: newinv2.md - name: v1 To v2 Migration href: migratingfromv1.md +- name: Lexicon & Taxonomy + href: lexicon.md +- name: Application Deep Dive + href: application.md - name: Arrangement href: arrangement.md - name: Cancellable Work Pattern @@ -24,10 +30,6 @@ href: drivers.md - name: Events Deep Dive href: events.md -- name: Lexicon & Taxonomy - href: lexicon.md -- name: Terminology Proposal - href: terminology-index.md - name: Keyboard href: keyboard.md - name: Layout Engine @@ -40,14 +42,16 @@ href: navigation.md - name: Popovers href: Popovers.md -- name: View Deep Dive - href: View.md -- name: View List - href: views.md +- name: Scheme Deep Dive + href: scheme.md - name: Scrolling href: scrolling.md - name: TableView Deep Dive href: tableview.md - name: TreeView Deep Dive href: treeview.md +- name: View Deep Dive + href: View.md +- name: View List + href: views.md From 1bd5e3761ae7d197c89decd4c4faad2af2664383 Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 20 Nov 2025 02:05:05 +0000 Subject: [PATCH 03/16] Fixes #4391. Weird situation where ForceDriver with args doesn't persists on open scenario (#4395) * Fixes #4391. Weird situation where ForceDriver with args doesn't persists on open scenario * Prevents change ForceDriver if app it's already initialized and allowing also initialize with driver instead of only by driver name. * Add dispose into FakeDriverBase and reset ForceDriver * Moving test to Application folder * Fix unit test * Remove unnecessary GlobalTestSetup * Add GC.SuppressFinalize * Revert "Add GC.SuppressFinalize" This reverts commit 2bd7cd7791be49449d84ea1f8011c341f295d8a8. * Reset MouseGrabView * Avoid CI warnings * Add GlobalTestSetup in all test that use Application * Trying to fix unit test * Reverting scope changes * Remove UICatalog testing Run * Force re-run CI test * Fix merge errors * Fix ansi for the red background color code * Fix more ANSI color code unit tests --------- Co-authored-by: Tig --- Examples/UICatalog/UICatalog.cs | 14 ++- Terminal.Gui/App/Application.Driver.cs | 16 ++- Terminal.Gui/App/ApplicationImpl.Driver.cs | 2 +- Terminal.Gui/App/ApplicationImpl.Lifecycle.cs | 22 +++-- Terminal.Gui/App/Mouse/MouseImpl.cs | 1 + Tests/UnitTests/FakeDriverBase.cs | 18 ++-- Tests/UnitTests/TestsAllViews.cs | 1 - .../ApplicationForceDriverTests.cs | 41 ++++++++ .../Application/ApplicationPopoverTests.cs | 2 +- .../Drivers/ToAnsiTests.cs | 97 +++++++++++++------ Tests/UnitTestsParallelizable/TestSetup.cs | 4 +- .../Text/TextFormatterDrawTests.cs | 3 +- .../Text/TextFormatterJustificationTests.cs | 2 +- .../View/Layout/Dim.AutoTests.PosTypes.cs | 4 +- .../View/Layout/Dim.AutoTests.cs | 1 - .../View/Layout/LayoutTests.cs | 2 +- .../View/Layout/ViewLayoutEventTests.cs | 2 +- .../Views/ShortcutTests.cs | 1 + 18 files changed, 176 insertions(+), 57 deletions(-) create mode 100644 Tests/UnitTestsParallelizable/Application/ApplicationForceDriverTests.cs diff --git a/Examples/UICatalog/UICatalog.cs b/Examples/UICatalog/UICatalog.cs index 446057259..8b8eeca71 100644 --- a/Examples/UICatalog/UICatalog.cs +++ b/Examples/UICatalog/UICatalog.cs @@ -55,7 +55,9 @@ namespace UICatalog; /// public class UICatalog { - private static string? _forceDriver = null; + private static string? _forceDriver; + private static string? _uiCatalogDriver; + private static string? _scenarioDriver; public static string LogFilePath { get; set; } = string.Empty; public static LoggingLevelSwitch LogLevelSwitch { get; } = new (); @@ -194,6 +196,8 @@ public class UICatalog UICatalogMain (Options); + Debug.Assert (Application.ForceDriver == string.Empty); + return 0; } @@ -255,7 +259,9 @@ public class UICatalog Application.Init (driverName: _forceDriver); - var top = Application.Run (); + _uiCatalogDriver = Application.Driver!.GetName (); + + Toplevel top = Application.Run (); top.Dispose (); Application.Shutdown (); VerifyObjectsWereDisposed (); @@ -421,6 +427,8 @@ public class UICatalog Application.InitializedChanged += ApplicationOnInitializedChanged; #endif + Application.ForceDriver = _forceDriver; + scenario.Main (); scenario.Dispose (); @@ -439,6 +447,8 @@ public class UICatalog if (e.Value) { sw.Start (); + _scenarioDriver = Application.Driver!.GetName (); + Debug.Assert (_scenarioDriver == _uiCatalogDriver); } else { diff --git a/Terminal.Gui/App/Application.Driver.cs b/Terminal.Gui/App/Application.Driver.cs index 741134d39..635ff854b 100644 --- a/Terminal.Gui/App/Application.Driver.cs +++ b/Terminal.Gui/App/Application.Driver.cs @@ -28,7 +28,21 @@ public static partial class Application // Driver abstractions public static string ForceDriver { get => ApplicationImpl.Instance.ForceDriver; - set => ApplicationImpl.Instance.ForceDriver = value; + set + { + if (!string.IsNullOrEmpty (ApplicationImpl.Instance.ForceDriver) && value != Driver?.GetName ()) + { + // ForceDriver cannot be changed if it has a valid value + return; + } + + if (ApplicationImpl.Instance.Initialized && value != Driver?.GetName ()) + { + throw new InvalidOperationException ($"The {nameof (ForceDriver)} can only be set before initialized."); + } + + ApplicationImpl.Instance.ForceDriver = value; + } } /// diff --git a/Terminal.Gui/App/ApplicationImpl.Driver.cs b/Terminal.Gui/App/ApplicationImpl.Driver.cs index 837024b83..edb6adcbd 100644 --- a/Terminal.Gui/App/ApplicationImpl.Driver.cs +++ b/Terminal.Gui/App/ApplicationImpl.Driver.cs @@ -34,7 +34,7 @@ public partial class ApplicationImpl bool factoryIsFake = _componentFactory is IComponentFactory; // Then check driverName - bool nameIsWindows = driverName?.Contains ("win", StringComparison.OrdinalIgnoreCase) ?? false; + bool nameIsWindows = driverName?.Contains ("windows", StringComparison.OrdinalIgnoreCase) ?? false; bool nameIsDotNet = driverName?.Contains ("dotnet", StringComparison.OrdinalIgnoreCase) ?? false; bool nameIsUnix = driverName?.Contains ("unix", StringComparison.OrdinalIgnoreCase) ?? false; bool nameIsFake = driverName?.Contains ("fake", StringComparison.OrdinalIgnoreCase) ?? false; diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs index 73465bf8a..09f06588d 100644 --- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs +++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs @@ -33,8 +33,8 @@ public partial class ApplicationImpl _driverName = ForceDriver; } - // Debug.Assert (Navigation is null); - // Navigation = new (); + // Debug.Assert (Navigation is null); + // Navigation = new (); //Debug.Assert (Popover is null); //Popover = new (); @@ -62,7 +62,7 @@ public partial class ApplicationImpl _keyboard.PrevTabGroupKey = existingPrevTabGroupKey; } - CreateDriver (driverName ?? _driverName); + CreateDriver (_driverName); Screen = Driver!.Screen; Initialized = true; @@ -145,9 +145,13 @@ public partial class ApplicationImpl } #endif + private bool _isResetingState; + /// public void ResetState (bool ignoreDisposed = false) { + _isResetingState = true; + // Shutdown is the bookend for Init. As such it needs to clean up all resources // Init created. Apps that do any threading will need to code defensively for this. // e.g. see Issue #537 @@ -231,7 +235,14 @@ public partial class ApplicationImpl // === 9. Clear graphics === Sixel.Clear (); - // === 10. Reset synchronization context === + // === 10. Reset ForceDriver === + // Note: ForceDriver and Force16Colors are reset + // If they need to persist across Init/Shutdown cycles + // then the user of the library should manage that state + Force16Colors = false; + ForceDriver = string.Empty; + + // === 11. Reset synchronization context === // IMPORTANT: Always reset sync context, even if not initialized // This ensures cleanup works correctly even if Shutdown is called without Init // Reset synchronization context to allow the user to run async/await, @@ -240,8 +251,7 @@ public partial class ApplicationImpl // (https://github.com/gui-cs/Terminal.Gui/issues/1084). SynchronizationContext.SetSynchronizationContext (null); - // Note: ForceDriver and Force16Colors are NOT reset; - // they need to persist across Init/Shutdown cycles + _isResetingState = false; } /// diff --git a/Terminal.Gui/App/Mouse/MouseImpl.cs b/Terminal.Gui/App/Mouse/MouseImpl.cs index 29116db0a..23a3aed61 100644 --- a/Terminal.Gui/App/Mouse/MouseImpl.cs +++ b/Terminal.Gui/App/Mouse/MouseImpl.cs @@ -254,6 +254,7 @@ internal class MouseImpl : IMouse // Do not clear LastMousePosition; Popover's require it to stay set with last mouse pos. CachedViewsUnderMouse.Clear (); MouseEvent = null; + MouseGrabView = null; } // Mouse grab functionality merged from MouseGrabHandler diff --git a/Tests/UnitTests/FakeDriverBase.cs b/Tests/UnitTests/FakeDriverBase.cs index f692cd914..657c5a588 100644 --- a/Tests/UnitTests/FakeDriverBase.cs +++ b/Tests/UnitTests/FakeDriverBase.cs @@ -4,7 +4,7 @@ namespace UnitTests; /// Enables tests to create a FakeDriver for testing purposes. /// [Collection ("Global Test Setup")] -public abstract class FakeDriverBase +public abstract class FakeDriverBase : IDisposable { /// /// Creates a new FakeDriver instance with the specified buffer size. @@ -19,14 +19,20 @@ public abstract class FakeDriverBase var output = new FakeOutput (); DriverImpl driver = new ( - new FakeInputProcessor (null), - new OutputBufferImpl (), - output, - new AnsiRequestScheduler (new AnsiResponseParser ()), - new SizeMonitorImpl (output)); + new FakeInputProcessor (null), + new OutputBufferImpl (), + output, + new AnsiRequestScheduler (new AnsiResponseParser ()), + new SizeMonitorImpl (output)); driver.SetScreenSize (width, height); return driver; } + + /// + public void Dispose () + { + Application.ResetState (true); + } } diff --git a/Tests/UnitTests/TestsAllViews.cs b/Tests/UnitTests/TestsAllViews.cs index 619334087..7428e26c0 100644 --- a/Tests/UnitTests/TestsAllViews.cs +++ b/Tests/UnitTests/TestsAllViews.cs @@ -1,6 +1,5 @@ #nullable enable using System.Reflection; -using System.Drawing; namespace UnitTests; diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationForceDriverTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationForceDriverTests.cs new file mode 100644 index 000000000..a3136cc11 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/ApplicationForceDriverTests.cs @@ -0,0 +1,41 @@ +using UnitTests; + +namespace UnitTests_Parallelizable.ApplicationTests; + +public class ApplicationForceDriverTests : FakeDriverBase +{ + [Fact] + public void ForceDriver_Does_Not_Changes_If_It_Has_Valid_Value () + { + Assert.False (Application.Initialized); + Assert.Null (Application.Driver); + Assert.Equal (string.Empty, Application.ForceDriver); + + Application.ForceDriver = "fake"; + Assert.Equal ("fake", Application.ForceDriver); + + Application.ForceDriver = "dotnet"; + Assert.Equal ("fake", Application.ForceDriver); + } + + [Fact] + public void ForceDriver_Throws_If_Initialized_Changed_To_Another_Value () + { + IDriver driver = CreateFakeDriver (); + + Assert.False (Application.Initialized); + Assert.Null (Application.Driver); + Assert.Equal (string.Empty, Application.ForceDriver); + + Application.Init (driverName: "fake"); + Assert.True (Application.Initialized); + Assert.NotNull (Application.Driver); + Assert.Equal ("fake", Application.Driver.GetName ()); + Assert.Equal (string.Empty, Application.ForceDriver); + + Assert.Throws (() => Application.ForceDriver = "dotnet"); + + Application.ForceDriver = "fake"; + Assert.Equal ("fake", Application.ForceDriver); + } +} diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs index 914d676ee..3ea1d796e 100644 --- a/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs +++ b/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using Moq; using Terminal.Gui.App; diff --git a/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs b/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs index e17c28cdf..c8b55d191 100644 --- a/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs @@ -40,10 +40,13 @@ public class ToAnsiTests : FakeDriverBase Assert.Equal (3, lines.Length); } - [Fact] - public void ToAnsi_With_Colors () + [Theory] + [InlineData (true, "\u001b[31m", "\u001b[34m")] + [InlineData (false, "\u001b[38;2;255;0;0m", "\u001b[38;2;0;0;255")] + public void ToAnsi_With_Colors (bool force16Colors, string expectedRed, string expectedBue) { IDriver driver = CreateFakeDriver (10, 2); + driver.Force16Colors = force16Colors; // Set red foreground driver.CurrentAttribute = new Attribute (Color.Red, Color.Black); @@ -56,26 +59,42 @@ public class ToAnsiTests : FakeDriverBase string ansi = driver.ToAnsi (); + Assert.True (driver.Force16Colors == force16Colors); // Should contain ANSI color codes - Assert.Contains ("\u001b[31m", ansi); // Red foreground - Assert.Contains ("\u001b[34m", ansi); // Blue foreground + Assert.Contains (expectedRed, ansi); // Red foreground + Assert.Contains (expectedBue, ansi); // Blue foreground Assert.Contains ("Red", ansi); Assert.Contains ("Blue", ansi); } - [Fact] - public void ToAnsi_With_Background_Colors () + [Theory] + [InlineData (false, "\u001b[48;2;")] + [InlineData (true, "\u001b[41m")] + public void ToAnsi_With_Background_Colors (bool force16Colors, string expected) { IDriver driver = CreateFakeDriver (10, 2); + Application.Force16Colors = force16Colors; // Set background color - driver.CurrentAttribute = new Attribute (Color.White, Color.Red); + driver.CurrentAttribute = new (Color.White, Color.Red); driver.AddStr ("WhiteOnRed"); string ansi = driver.ToAnsi (); + /* + The ANSI escape sequence for red background (8-color) is ESC[41m where ESC is \x1b (or \u001b). + Examples: + C# string: "\u001b[41m" or "\x1b[41m" + Reset (clear attributes): "\u001b[0m" + Notes: + Bright/red background (16-color bright variant) uses ESC[101m ("\u001b[101m"). + For 24-bit RGB background use ESC[48;2;;;m, e.g. "\u001b[48;2;255;0;0m" for pure red. + */ + + Assert.True (driver.Force16Colors == force16Colors); + // Should contain ANSI background color code - Assert.Contains ("\u001b[41m", ansi); // Red background + Assert.Contains (expected, ansi); // Red background Assert.Contains ("WhiteOnRed", ansi); } @@ -138,10 +157,13 @@ public class ToAnsiTests : FakeDriverBase Assert.Contains ("???", ansi); } - [Fact] - public void ToAnsi_Attribute_Changes_Within_Line () + [Theory] + [InlineData (true, "\u001b[31m", "\u001b[34m")] + [InlineData (false, "\u001b[38;2;", "\u001b[48;2;")] + public void ToAnsi_Attribute_Changes_Within_Line (bool force16Colors, string expectedRed, string expectedBlue) { IDriver driver = CreateFakeDriver (20, 1); + driver.Force16Colors = force16Colors; driver.AddStr ("Normal"); driver.CurrentAttribute = new Attribute (Color.Red, Color.Black); @@ -151,10 +173,11 @@ public class ToAnsiTests : FakeDriverBase string ansi = driver.ToAnsi (); + Assert.True (driver.Force16Colors == force16Colors); // Should contain color changes within the line Assert.Contains ("Normal", ansi); - Assert.Contains ("\u001b[31m", ansi); // Red - Assert.Contains ("\u001b[34m", ansi); // Blue + Assert.Contains (expectedRed, ansi); // Red + Assert.Contains (expectedBlue, ansi); // Blue } [Fact] @@ -223,40 +246,52 @@ public class ToAnsiTests : FakeDriverBase Assert.DoesNotContain ("\u001b[38;2;", ansi); // No RGB codes } - [Fact] - public void ToAnsi_Multiple_Attributes_Per_Line () + [Theory] + [InlineData (true, "\u001b[31m", "\u001b[32m", "\u001b[34m", "\u001b[33m", "\u001b[35m", "\u001b[36m")] + [InlineData (false, "\u001b[38;2;255;0;0m", "\u001b[38;2;0;128;0m", "\u001b[38;2;0;0;255", "\u001b[38;2;255;255;0m", "\u001b[38;2;255;0;255m", "\u001b[38;2;0;255;255m")] + public void ToAnsi_Multiple_Attributes_Per_Line ( + bool force16Colors, + string expectedRed, + string expectedGreen, + string expectedBlue, + string expectedYellow, + string expectedMagenta, + string expectedCyan + ) { IDriver driver = CreateFakeDriver (50, 1); + driver.Force16Colors = force16Colors; // Create a line with many attribute changes - string[] colors = { "Red", "Green", "Blue", "Yellow", "Magenta", "Cyan" }; + string [] colors = { "Red", "Green", "Blue", "Yellow", "Magenta", "Cyan" }; foreach (string colorName in colors) { Color fg = colorName switch - { - "Red" => Color.Red, - "Green" => Color.Green, - "Blue" => Color.Blue, - "Yellow" => Color.Yellow, - "Magenta" => Color.Magenta, - "Cyan" => Color.Cyan, - _ => Color.White - }; + { + "Red" => Color.Red, + "Green" => Color.Green, + "Blue" => Color.Blue, + "Yellow" => Color.Yellow, + "Magenta" => Color.Magenta, + "Cyan" => Color.Cyan, + _ => Color.White + }; - driver.CurrentAttribute = new Attribute (fg, Color.Black); + driver.CurrentAttribute = new (fg, Color.Black); driver.AddStr (colorName); } string ansi = driver.ToAnsi (); + Assert.True (driver.Force16Colors == force16Colors); // Should contain multiple color codes - Assert.Contains ("\u001b[31m", ansi); // Red - Assert.Contains ("\u001b[32m", ansi); // Green - Assert.Contains ("\u001b[34m", ansi); // Blue - Assert.Contains ("\u001b[33m", ansi); // Yellow - Assert.Contains ("\u001b[35m", ansi); // Magenta - Assert.Contains ("\u001b[36m", ansi); // Cyan + Assert.Contains (expectedRed, ansi); // Red + Assert.Contains (expectedGreen, ansi); // Green + Assert.Contains (expectedBlue, ansi); // Blue + Assert.Contains (expectedYellow, ansi); // Yellow + Assert.Contains (expectedMagenta, ansi); // Magenta + Assert.Contains (expectedCyan, ansi); // Cyan } [Fact] diff --git a/Tests/UnitTestsParallelizable/TestSetup.cs b/Tests/UnitTestsParallelizable/TestSetup.cs index a8d37578e..178a0d4de 100644 --- a/Tests/UnitTestsParallelizable/TestSetup.cs +++ b/Tests/UnitTestsParallelizable/TestSetup.cs @@ -24,8 +24,8 @@ public class GlobalTestSetup : IDisposable // Reset application state just in case a test changed something. // TODO: Add an Assert to ensure none of the state of Application changed. // TODO: Add an Assert to ensure none of the state of ConfigurationManager changed. - CheckDefaultState (); Application.ResetState (true); + CheckDefaultState (); } // IMPORTANT: Ensure this matches the code in Init_ResetState_Resets_Properties @@ -43,7 +43,7 @@ public class GlobalTestSetup : IDisposable Assert.Null (Application.Mouse.MouseGrabView); // Don't check Application.ForceDriver - // Assert.Empty (Application.ForceDriver); + Assert.Empty (Application.ForceDriver); // Don't check Application.Force16Colors //Assert.False (Application.Force16Colors); Assert.Null (Application.Driver); diff --git a/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs b/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs index 17ac27ab5..da433ffc0 100644 --- a/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs +++ b/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs @@ -1,11 +1,12 @@ #nullable enable using System.Text; using UICatalog; +using UnitTests; using Xunit.Abstractions; // Alias Console to MockConsole so we don't accidentally use Console -namespace UnitTests.TextTests; +namespace UnitTests_Parallelizable.TextTests; public class TextFormatterDrawTests (ITestOutputHelper output) : FakeDriverBase { diff --git a/Tests/UnitTestsParallelizable/Text/TextFormatterJustificationTests.cs b/Tests/UnitTestsParallelizable/Text/TextFormatterJustificationTests.cs index 8990e1698..e01f995ae 100644 --- a/Tests/UnitTestsParallelizable/Text/TextFormatterJustificationTests.cs +++ b/Tests/UnitTestsParallelizable/Text/TextFormatterJustificationTests.cs @@ -4,7 +4,7 @@ using Xunit.Abstractions; // Alias Console to MockConsole so we don't accidentally use Console -namespace UnitTests.TextTests; +namespace UnitTests_Parallelizable.TextTests; public class TextFormatterJustificationTests (ITestOutputHelper output) : FakeDriverBase { diff --git a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs b/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs index 61aadaea7..c5a571c10 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs +++ b/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs @@ -1,4 +1,6 @@ -namespace UnitTests_Parallelizable.LayoutTests; +using UnitTests.Parallelizable; + +namespace UnitTests_Parallelizable.LayoutTests; public partial class DimAutoTests { diff --git a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.cs b/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.cs index 4c1da89b5..3c785ae36 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.cs +++ b/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.cs @@ -1,5 +1,4 @@ using System.Text; -using UnitTests; using Xunit.Abstractions; using static Terminal.Gui.ViewBase.Dim; diff --git a/Tests/UnitTestsParallelizable/View/Layout/LayoutTests.cs b/Tests/UnitTestsParallelizable/View/Layout/LayoutTests.cs index 1c45576f2..eccd03179 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/LayoutTests.cs +++ b/Tests/UnitTestsParallelizable/View/Layout/LayoutTests.cs @@ -2,7 +2,7 @@ namespace UnitTests_Parallelizable.LayoutTests; -public class LayoutTests : GlobalTestSetup +public class LayoutTests { #region Constructor Tests diff --git a/Tests/UnitTestsParallelizable/View/Layout/ViewLayoutEventTests.cs b/Tests/UnitTestsParallelizable/View/Layout/ViewLayoutEventTests.cs index bc368764f..c606abbaa 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/ViewLayoutEventTests.cs +++ b/Tests/UnitTestsParallelizable/View/Layout/ViewLayoutEventTests.cs @@ -3,7 +3,7 @@ using UnitTests.Parallelizable; namespace UnitTests_Parallelizable.ViewLayoutEventTests; -public class ViewLayoutEventTests : GlobalTestSetup +public class ViewLayoutEventTests { [Fact] public void View_WidthChanging_Event_Fires () diff --git a/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs b/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs index fc740c63a..7ca25dbad 100644 --- a/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs @@ -1,4 +1,5 @@ using JetBrains.Annotations; +using UnitTests.Parallelizable; namespace UnitTests_Parallelizable.ViewsTests; From 726b15dd2811040bc57dd8b8041ee028cf91513c Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 20 Nov 2025 13:20:01 -0500 Subject: [PATCH 04/16] Fixes #4389 - Add comprehensive unit tests for WindowsKeyConverter (#4390) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add comprehensive unit tests for WindowsKeyConverter - Implement 118 parallelizable unit tests for WindowsKeyConverter - Cover ToKey and ToKeyInfo methods with full bidirectional testing - Test basic characters, modifiers, special keys, function keys - Test VK_PACKET Unicode/IME input - Test OEM keys, NumPad keys, and lock states - Include round-trip conversion tests - All tests passing successfully Fixes #4389 * Add test documenting VK_PACKET surrogate pair limitation VK_PACKET sends surrogate pairs (e.g., emoji) as two separate events. Current WindowsKeyConverter processes each independently without combining. Test documents this limitation for future fix at InputProcessor level. * Mark WindowsKeyConverterTests as Windows-only Tests now skip on non-Windows platforms using SkipException in constructor. This prevents CI failures on macOS and Linux where Windows Console API is not available. * Properly mark WindowsKeyConverter tests as Windows-only Created custom xUnit attributes SkipOnNonWindowsFact and SkipOnNonWindowsTheory that automatically skip tests on non-Windows platforms. This prevents CI failures on macOS and Linux. * Refactor and enhance test coverage for input processors Refactored `ApplicationImpl.cs` to simplify its structure by removing the `_stopAfterFirstIteration` field. Reintroduced and modernized test files with improved structure and coverage: - `NetInputProcessorTests.cs`: Added tests for `ConsoleKeyInfo` to `Rune`/`Key` conversion and queue processing. - `WindowSizeMonitorTests.cs`: Added tests for size change event handling. - `WindowsInputProcessorTests.cs`: Added tests for keyboard and mouse input processing, including mouse flag mapping. - `WindowsKeyConverterTests.cs`: Added comprehensive tests for `InputRecord` to `Key` conversion, covering OEM keys, modifiers, Unicode, and round-trip integrity. Improved test coverage for edge cases, introduced parameterized tests, and documented known limitations for future improvements. * Use Trait-based platform filtering for WindowsKeyConverter tests Added [Trait('Platform', 'Windows')] and [Collection('Global Test Setup')] attributes. Tests will run on Windows but can be filtered in CI on other platforms using --filter 'Platform!=Windows' if needed. This approach doesn't interfere with GlobalTestSetup and works correctly with xUnit. * Filter Windows-specific tests on non-Windows CI platforms Added --filter 'Platform!=Windows' for Linux and macOS runners to exclude WindowsKeyConverterTests which require Windows Console APIs. Windows runner runs all tests normally. * Fix log path typo and remove Codecov upload step Corrected a typo in the log directory path from `logs/UnitTestsParallelable/` to `logs/UnitTestsParallelizable/`. Removed the "Upload Parallelizable UnitTests Coverage to Codecov" step, which was conditional on `matrix.os == 'ubuntu-latest'` and used the `codecov/codecov-action@v4` action. This change improves log handling and removes the Codecov integration. * Refactor application reset logic Replaced `Application.ResetState(true)` with a more explicit reset mechanism. Introduced `ApplicationImpl.SetInstance(null)` to clear the application instance and added `CM.Disable(true)` to disable specific components. This change improves control over the reset process and ensures a more granular approach to application state management. * Improve null safety with ?. and ! operators Enhanced null safety across the codebase by introducing the null-conditional operator (`?.`) and null-forgiving operator (`!`) where appropriate. - Updated `app` and `driver` method calls to use `?.` to prevent potential `NullReferenceException` errors. - Added `!` to assert non-nullability in cases like `e.Value!.ToString()` and `app.Driver?.Contents!`. - Modified `lv.SelectedItemChanged` event handler to ensure safe handling of nullable values. - Updated `app.Shutdown()`, `app.LayoutAndDraw()`, and mouse event handling to use `?.`. - Ensured `driver.SetScreenSize` is invoked only when `driver` is not null. - Improved string concatenation logic with null-forgiving operator for `Contents`. - General improvements to null safety to make the code more robust and resilient to null references. * Improve Unicode tests and clarify surrogate pair handling Updated `WindowsKeyConverterTests` to enhance readability, improve test data, and clarify comments. Key changes include: - Reformatted `[Collection]` and `[Trait]` attributes for consistency. - Replaced placeholder Unicode characters with meaningful examples: - Chinese: `中`, Japanese: `日`, Korean: `한`, Accented: `é`, Euro: `€`, Greek: `Ω`. - Updated comments to replace placeholder emojis (`??`) with `😀` (U+1F600) for clarity. - Adjusted surrogate pair test data to accurately reflect `😀`. - Improved documentation of current limitations and future fixes for surrogate pair handling. These changes ensure more accurate and meaningful test cases while improving code clarity. * Ensure platform-specific test execution on Windows Added `System.Runtime.InteropServices` and `Xunit.Sdk` namespaces to `WindowsKeyConverterTests.cs` to support platform checks and test setup. Marked the test class with `[Collection("Global Test Setup")]` to enable shared test setup. Updated the `ToKey_NumPadKeys_ReturnsExpectedKeyCode` method to include a platform check, ensuring the test only runs on Windows platforms. * Integrate TrueColor support into ColorPicker Merged TrueColors functionality into ColorPicker, enhancing the scenario with TrueColor demonstration and gradient features. Updated `ColorPicker.cs` to include driver information, TrueColor support indicators, and a toggle for `Force16Colors`. Removed `TrueColors.cs` as its functionality is now consolidated. Refined `ColorBar` to use dynamic height with `Dim.Auto` for better flexibility. Added documentation to `HueBar` to clarify its role in representing the Hue component in HSL color space. * Revert workflow change. * Reverted attribute that didn't actualy work. --- Examples/UICatalog/Scenarios/ColorPicker.cs | 29 +- Examples/UICatalog/Scenarios/TrueColors.cs | 157 ---- Terminal.Gui/App/ApplicationImpl.Lifecycle.cs | 6 - Terminal.Gui/Views/Color/ColorBar.cs | 2 +- Terminal.Gui/Views/Color/HueBar.cs | 5 +- .../UICatalog/ScenarioTests.cs | 4 +- .../{ => Dotnet}/NetInputProcessorTests.cs | 0 .../{ => Windows}/WindowSizeMonitorTests.cs | 0 .../WindowsInputProcessorTests.cs | 0 .../Windows/WindowsKeyConverterTests.cs | 798 ++++++++++++++++++ .../Views/ListViewTests.cs | 30 +- 11 files changed, 848 insertions(+), 183 deletions(-) delete mode 100644 Examples/UICatalog/Scenarios/TrueColors.cs rename Tests/UnitTestsParallelizable/Drivers/{ => Dotnet}/NetInputProcessorTests.cs (100%) rename Tests/UnitTestsParallelizable/Drivers/{ => Windows}/WindowSizeMonitorTests.cs (100%) rename Tests/UnitTestsParallelizable/Drivers/{ => Windows}/WindowsInputProcessorTests.cs (100%) create mode 100644 Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs diff --git a/Examples/UICatalog/Scenarios/ColorPicker.cs b/Examples/UICatalog/Scenarios/ColorPicker.cs index 61b71d093..69ae48660 100644 --- a/Examples/UICatalog/Scenarios/ColorPicker.cs +++ b/Examples/UICatalog/Scenarios/ColorPicker.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace UICatalog.Scenarios; -[ScenarioMetadata ("ColorPicker", "Color Picker.")] +[ScenarioMetadata ("ColorPicker", "Color Picker and TrueColor demonstration.")] [ScenarioCategory ("Colors")] [ScenarioCategory ("Controls")] public class ColorPickers : Scenario @@ -220,6 +220,33 @@ public class ColorPickers : Scenario }; app.Add (cbShowName); + var lblDriverName = new Label + { + Y = Pos.Bottom (cbShowName) + 1, Text = $"Driver is `{Application.Driver?.GetName ()}`:" + }; + bool canTrueColor = Application.Driver?.SupportsTrueColor ?? false; + + var cbSupportsTrueColor = new CheckBox + { + X = Pos.Right (lblDriverName) + 1, + Y = Pos.Top (lblDriverName), + CheckedState = canTrueColor ? CheckState.Checked : CheckState.UnChecked, + CanFocus = false, + Enabled = false, + Text = "SupportsTrueColor" + }; + app.Add (cbSupportsTrueColor); + + var cbUseTrueColor = new CheckBox + { + X = Pos.Right (cbSupportsTrueColor) + 1, + Y = Pos.Top (lblDriverName), + CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked, + Enabled = canTrueColor, + Text = "Force16Colors" + }; + cbUseTrueColor.CheckedStateChanging += (_, evt) => { Application.Force16Colors = evt.Result == CheckState.Checked; }; + app.Add (lblDriverName, cbSupportsTrueColor, cbUseTrueColor); // Set default colors. foregroundColorPicker.SelectedColor = _demoView.SuperView!.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 (); backgroundColorPicker.SelectedColor = _demoView.SuperView.GetAttributeForRole (VisualRole.Normal).Background.GetClosestNamedColor16 (); diff --git a/Examples/UICatalog/Scenarios/TrueColors.cs b/Examples/UICatalog/Scenarios/TrueColors.cs deleted file mode 100644 index 43e52dec3..000000000 --- a/Examples/UICatalog/Scenarios/TrueColors.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System; - -namespace UICatalog.Scenarios; - -[ScenarioMetadata ("True Colors", "Demonstration of true color support.")] -[ScenarioCategory ("Colors")] -public class TrueColors : Scenario -{ - public override void Main () - { - Application.Init (); - - Window app = new () - { - Title = GetQuitKeyAndName () - }; - - var x = 2; - var y = 1; - - bool canTrueColor = Application.Driver?.SupportsTrueColor ?? false; - - var lblDriverName = new Label - { - X = x, Y = y++, Text = $"Current driver is {Application.Driver?.GetType ().Name}" - }; - app.Add (lblDriverName); - y++; - - var cbSupportsTrueColor = new CheckBox - { - X = x, - Y = y++, - CheckedState = canTrueColor ? CheckState.Checked : CheckState.UnChecked, - CanFocus = false, - Enabled = false, - Text = "Driver supports true color " - }; - app.Add (cbSupportsTrueColor); - - var cbUseTrueColor = new CheckBox - { - X = x, - Y = y++, - CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked, - Enabled = canTrueColor, - Text = "Force 16 colors" - }; - cbUseTrueColor.CheckedStateChanging += (_, evt) => { Application.Force16Colors = evt.Result == CheckState.Checked; }; - app.Add (cbUseTrueColor); - - y += 2; - SetupGradient ("Red gradient", x, ref y, i => new (i, 0)); - SetupGradient ("Green gradient", x, ref y, i => new (0, i)); - SetupGradient ("Blue gradient", x, ref y, i => new (0, 0, i)); - SetupGradient ("Yellow gradient", x, ref y, i => new (i, i)); - SetupGradient ("Magenta gradient", x, ref y, i => new (i, 0, i)); - SetupGradient ("Cyan gradient", x, ref y, i => new (0, i, i)); - SetupGradient ("Gray gradient", x, ref y, i => new (i, i, i)); - - app.Add ( - new Label { X = Pos.AnchorEnd (44), Y = 2, Text = "Mouse over to get the gradient view color:" } - ); - - app.Add ( - new Label { X = Pos.AnchorEnd (44), Y = 4, Text = "Red:" } - ); - - app.Add ( - new Label { X = Pos.AnchorEnd (44), Y = 5, Text = "Green:" } - ); - - app.Add ( - new Label { X = Pos.AnchorEnd (44), Y = 6, Text = "Blue:" } - ); - - app.Add ( - new Label { X = Pos.AnchorEnd (44), Y = 8, Text = "Darker:" } - ); - - app.Add ( - new Label { X = Pos.AnchorEnd (44), Y = 9, Text = "Lighter:" } - ); - - var lblRed = new Label { X = Pos.AnchorEnd (32), Y = 4, Text = "na" }; - app.Add (lblRed); - var lblGreen = new Label { X = Pos.AnchorEnd (32), Y = 5, Text = "na" }; - app.Add (lblGreen); - var lblBlue = new Label { X = Pos.AnchorEnd (32), Y = 6, Text = "na" }; - app.Add (lblBlue); - - var lblDarker = new Label { X = Pos.AnchorEnd (32), Y = 8, Text = " " }; - app.Add (lblDarker); - - var lblLighter = new Label { X = Pos.AnchorEnd (32), Y = 9, Text = " " }; - app.Add (lblLighter); - - Application.MouseEvent += (s, e) => - { - if (e.View == null) - { - return; - } - - if (e.Flags == MouseFlags.Button1Clicked) - { - Attribute normal = e.View.GetAttributeForRole (VisualRole.Normal); - - lblLighter.SetScheme (new (e.View.GetScheme ()) - { - Normal = new ( - normal.Foreground, - normal.Background.GetBrighterColor () - ) - }); - } - else - { - Attribute normal = e.View.GetAttributeForRole (VisualRole.Normal); - lblRed.Text = normal.Foreground.R.ToString (); - lblGreen.Text = normal.Foreground.G.ToString (); - lblBlue.Text = normal.Foreground.B.ToString (); - } - }; - Application.Run (app); - app.Dispose (); - - Application.Shutdown (); - - return; - - void SetupGradient (string name, int x, ref int y, Func colorFunc) - { - var gradient = new Label { X = x, Y = y++, Text = name }; - app.Add (gradient); - - for (int dx = x, i = 0; i <= 256; i += 4) - { - var l = new Label - { - X = dx++, - Y = y - }; - l.SetScheme (new () - { - Normal = new ( - colorFunc (Math.Clamp (i, 0, 255)), - colorFunc (Math.Clamp (i, 0, 255)) - ) - }); - app.Add (l); - } - - y += 2; - } - } -} diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs index 09f06588d..e20fd7ce8 100644 --- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs +++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs @@ -145,13 +145,9 @@ public partial class ApplicationImpl } #endif - private bool _isResetingState; - /// public void ResetState (bool ignoreDisposed = false) { - _isResetingState = true; - // Shutdown is the bookend for Init. As such it needs to clean up all resources // Init created. Apps that do any threading will need to code defensively for this. // e.g. see Issue #537 @@ -250,8 +246,6 @@ public partial class ApplicationImpl // gui.cs does no longer process any callbacks. See #1084 for more details: // (https://github.com/gui-cs/Terminal.Gui/issues/1084). SynchronizationContext.SetSynchronizationContext (null); - - _isResetingState = false; } /// diff --git a/Terminal.Gui/Views/Color/ColorBar.cs b/Terminal.Gui/Views/Color/ColorBar.cs index 2be624be5..940798eee 100644 --- a/Terminal.Gui/Views/Color/ColorBar.cs +++ b/Terminal.Gui/Views/Color/ColorBar.cs @@ -15,7 +15,7 @@ internal abstract class ColorBar : View, IColorBar /// protected ColorBar () { - Height = 1; + Height = Dim.Auto(minimumContentDim: 1); Width = Dim.Fill (); CanFocus = true; diff --git a/Terminal.Gui/Views/Color/HueBar.cs b/Terminal.Gui/Views/Color/HueBar.cs index a0b0b554d..fef601bb8 100644 --- a/Terminal.Gui/Views/Color/HueBar.cs +++ b/Terminal.Gui/Views/Color/HueBar.cs @@ -1,10 +1,11 @@ - - using ColorHelper; using ColorConverter = ColorHelper.ColorConverter; namespace Terminal.Gui.Views; +/// +/// A bar representing the Hue component of a in HSL color space. +/// internal class HueBar : ColorBar { /// diff --git a/Tests/IntegrationTests/UICatalog/ScenarioTests.cs b/Tests/IntegrationTests/UICatalog/ScenarioTests.cs index 39376e87b..bd8fcb69d 100644 --- a/Tests/IntegrationTests/UICatalog/ScenarioTests.cs +++ b/Tests/IntegrationTests/UICatalog/ScenarioTests.cs @@ -35,7 +35,9 @@ public class ScenarioTests : TestsAllViews return; } - Application.ResetState (true); + // Force a complete reset + ApplicationImpl.SetInstance (null); + CM.Disable (true); _output.WriteLine ($"Running Scenario '{scenarioType}'"); Scenario? scenario = null; diff --git a/Tests/UnitTestsParallelizable/Drivers/NetInputProcessorTests.cs b/Tests/UnitTestsParallelizable/Drivers/Dotnet/NetInputProcessorTests.cs similarity index 100% rename from Tests/UnitTestsParallelizable/Drivers/NetInputProcessorTests.cs rename to Tests/UnitTestsParallelizable/Drivers/Dotnet/NetInputProcessorTests.cs diff --git a/Tests/UnitTestsParallelizable/Drivers/WindowSizeMonitorTests.cs b/Tests/UnitTestsParallelizable/Drivers/Windows/WindowSizeMonitorTests.cs similarity index 100% rename from Tests/UnitTestsParallelizable/Drivers/WindowSizeMonitorTests.cs rename to Tests/UnitTestsParallelizable/Drivers/Windows/WindowSizeMonitorTests.cs diff --git a/Tests/UnitTestsParallelizable/Drivers/WindowsInputProcessorTests.cs b/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsInputProcessorTests.cs similarity index 100% rename from Tests/UnitTestsParallelizable/Drivers/WindowsInputProcessorTests.cs rename to Tests/UnitTestsParallelizable/Drivers/Windows/WindowsInputProcessorTests.cs diff --git a/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs b/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs new file mode 100644 index 000000000..f50ee8809 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs @@ -0,0 +1,798 @@ +using System.Runtime.InteropServices; + +namespace UnitTests_Parallelizable.DriverTests; + +[Collection ("Global Test Setup")] +[Trait ("Platform", "Windows")] +public class WindowsKeyConverterTests +{ + private readonly WindowsKeyConverter _converter = new (); + + #region ToKey Tests - Basic Characters + + [Theory] + [InlineData ('a', ConsoleKey.A, false, false, false, KeyCode.A)] // lowercase a + [InlineData ('A', ConsoleKey.A, true, false, false, KeyCode.A | KeyCode.ShiftMask)] // uppercase A + [InlineData ('z', ConsoleKey.Z, false, false, false, KeyCode.Z)] + [InlineData ('Z', ConsoleKey.Z, true, false, false, KeyCode.Z | KeyCode.ShiftMask)] + public void ToKey_LetterKeys_ReturnsExpectedKeyCode ( + char unicodeChar, + ConsoleKey consoleKey, + bool shift, + bool alt, + bool ctrl, + KeyCode expectedKeyCode + ) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + WindowsConsole.InputRecord inputRecord = CreateInputRecord (unicodeChar, consoleKey, shift, alt, ctrl); + + // Act + var result = _converter.ToKey (inputRecord); + + // Assert + Assert.Equal (expectedKeyCode, result.KeyCode); + } + + [Theory] + [InlineData ('0', ConsoleKey.D0, false, false, false, KeyCode.D0)] + [InlineData ('1', ConsoleKey.D1, false, false, false, KeyCode.D1)] + [InlineData ('9', ConsoleKey.D9, false, false, false, KeyCode.D9)] + public void ToKey_NumberKeys_ReturnsExpectedKeyCode ( + char unicodeChar, + ConsoleKey consoleKey, + bool shift, + bool alt, + bool ctrl, + KeyCode expectedKeyCode + ) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + WindowsConsole.InputRecord inputRecord = CreateInputRecord (unicodeChar, consoleKey, shift, alt, ctrl); + + // Act + var result = _converter.ToKey (inputRecord); + + // Assert + Assert.Equal (expectedKeyCode, result.KeyCode); + } + + #endregion + + #region ToKey Tests - Modifiers + + [Theory] + [InlineData ('a', ConsoleKey.A, false, false, true, KeyCode.A | KeyCode.CtrlMask)] // Ctrl+A + [InlineData ('A', ConsoleKey.A, true, false, true, KeyCode.A | KeyCode.ShiftMask | KeyCode.CtrlMask)] // Ctrl+Shift+A (Windows keeps ShiftMask) + [InlineData ('a', ConsoleKey.A, false, true, false, KeyCode.A | KeyCode.AltMask)] // Alt+A + [InlineData ('A', ConsoleKey.A, true, true, false, KeyCode.A | KeyCode.ShiftMask | KeyCode.AltMask)] // Alt+Shift+A + [InlineData ('a', ConsoleKey.A, false, true, true, KeyCode.A | KeyCode.CtrlMask | KeyCode.AltMask)] // Ctrl+Alt+A + public void ToKey_WithModifiers_ReturnsExpectedKeyCode ( + char unicodeChar, + ConsoleKey consoleKey, + bool shift, + bool alt, + bool ctrl, + KeyCode expectedKeyCode + ) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + WindowsConsole.InputRecord inputRecord = CreateInputRecord (unicodeChar, consoleKey, shift, alt, ctrl); + + // Act + var result = _converter.ToKey (inputRecord); + + // Assert + Assert.Equal (expectedKeyCode, result.KeyCode); + } + + #endregion + + #region ToKey Tests - Special Keys + + [Theory] + [InlineData (ConsoleKey.Enter, KeyCode.Enter)] + [InlineData (ConsoleKey.Escape, KeyCode.Esc)] + [InlineData (ConsoleKey.Tab, KeyCode.Tab)] + [InlineData (ConsoleKey.Backspace, KeyCode.Backspace)] + [InlineData (ConsoleKey.Delete, KeyCode.Delete)] + [InlineData (ConsoleKey.Insert, KeyCode.Insert)] + [InlineData (ConsoleKey.Home, KeyCode.Home)] + [InlineData (ConsoleKey.End, KeyCode.End)] + [InlineData (ConsoleKey.PageUp, KeyCode.PageUp)] + [InlineData (ConsoleKey.PageDown, KeyCode.PageDown)] + [InlineData (ConsoleKey.UpArrow, KeyCode.CursorUp)] + [InlineData (ConsoleKey.DownArrow, KeyCode.CursorDown)] + [InlineData (ConsoleKey.LeftArrow, KeyCode.CursorLeft)] + [InlineData (ConsoleKey.RightArrow, KeyCode.CursorRight)] + public void ToKey_SpecialKeys_ReturnsExpectedKeyCode (ConsoleKey consoleKey, KeyCode expectedKeyCode) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + char unicodeChar = consoleKey switch + { + ConsoleKey.Enter => '\r', + ConsoleKey.Escape => '\u001B', + ConsoleKey.Tab => '\t', + ConsoleKey.Backspace => '\b', + _ => '\0' + }; + WindowsConsole.InputRecord inputRecord = CreateInputRecord (unicodeChar, consoleKey, false, false, false); + + // Act + var result = _converter.ToKey (inputRecord); + + // Assert + Assert.Equal (expectedKeyCode, result.KeyCode); + } + + [Theory] + [InlineData (ConsoleKey.F1, KeyCode.F1)] + [InlineData (ConsoleKey.F2, KeyCode.F2)] + [InlineData (ConsoleKey.F3, KeyCode.F3)] + [InlineData (ConsoleKey.F4, KeyCode.F4)] + [InlineData (ConsoleKey.F5, KeyCode.F5)] + [InlineData (ConsoleKey.F6, KeyCode.F6)] + [InlineData (ConsoleKey.F7, KeyCode.F7)] + [InlineData (ConsoleKey.F8, KeyCode.F8)] + [InlineData (ConsoleKey.F9, KeyCode.F9)] + [InlineData (ConsoleKey.F10, KeyCode.F10)] + [InlineData (ConsoleKey.F11, KeyCode.F11)] + [InlineData (ConsoleKey.F12, KeyCode.F12)] + public void ToKey_FunctionKeys_ReturnsExpectedKeyCode (ConsoleKey consoleKey, KeyCode expectedKeyCode) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + WindowsConsole.InputRecord inputRecord = CreateInputRecord ('\0', consoleKey, false, false, false); + + // Act + var result = _converter.ToKey (inputRecord); + + // Assert + Assert.Equal (expectedKeyCode, result.KeyCode); + } + + #endregion + + #region ToKey Tests - VK_PACKET (Unicode/IME) + + [Theory] + [InlineData ('中')] // Chinese character + [InlineData ('日')] // Japanese character + [InlineData ('한')] // Korean character + [InlineData ('é')] // Accented character + [InlineData ('€')] // Euro symbol + [InlineData ('Ω')] // Greek character + public void ToKey_VKPacket_Unicode_ReturnsExpectedCharacter (char unicodeChar) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + WindowsConsole.InputRecord inputRecord = CreateVKPacketInputRecord (unicodeChar); + + // Act + var result = _converter.ToKey (inputRecord); + + // Assert + Assert.Equal ((KeyCode)unicodeChar, result.KeyCode); + } + + [Fact] + public void ToKey_VKPacket_ZeroChar_ReturnsNull () + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + WindowsConsole.InputRecord inputRecord = CreateVKPacketInputRecord ('\0'); + + // Act + var result = _converter.ToKey (inputRecord); + + // Assert + Assert.Equal (KeyCode.Null, result.KeyCode); + } + + [Fact] + public void ToKey_VKPacket_SurrogatePair_DocumentsCurrentLimitation () + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Emoji '😀' (U+1F600) requires a surrogate pair: High=U+D83D, Low=U+DE00 + // Windows sends this as TWO consecutive VK_PACKET events (one for each char) + // because KeyEventRecord.UnicodeChar is a single 16-bit char field. + // + // CURRENT LIMITATION: WindowsKeyConverter processes each event independently + // and does not combine surrogate pairs into a single Rune/KeyCode. + // This test documents the current (incorrect) behavior. + // + // TODO: Implement proper surrogate pair handling at the InputProcessor level + // to combine consecutive high+low surrogate events into a single Key with the + // complete Unicode codepoint. + // See: https://docs.microsoft.com/en-us/windows/console/key-event-record + + var highSurrogate = '\uD83D'; // High surrogate for 😀 + var lowSurrogate = '\uDE00'; // Low surrogate for 😀 + + // First event with high surrogate + WindowsConsole.InputRecord highRecord = CreateVKPacketInputRecord (highSurrogate); + var highResult = _converter.ToKey (highRecord); + + // Second event with low surrogate + WindowsConsole.InputRecord lowRecord = CreateVKPacketInputRecord (lowSurrogate); + var lowResult = _converter.ToKey (lowRecord); + + // Currently each surrogate half is processed independently as invalid KeyCodes + // These assertions document the current (broken) behavior + Assert.Equal ((KeyCode)highSurrogate, highResult.KeyCode); + Assert.Equal ((KeyCode)lowSurrogate, lowResult.KeyCode); + + // What SHOULD happen (future fix): + // The InputProcessor should detect the surrogate pair and combine them: + // var expectedRune = new Rune(0x1F600); // 😀 + // Assert.Equal((KeyCode)expectedRune.Value, combinedResult.KeyCode); + } + + #endregion + + #region ToKey Tests - OEM Keys + + [Theory] + [InlineData (';', ConsoleKey.Oem1, false, (KeyCode)';')] + [InlineData (':', ConsoleKey.Oem1, true, (KeyCode)':')] + [InlineData ('/', ConsoleKey.Oem2, false, (KeyCode)'/')] + [InlineData ('?', ConsoleKey.Oem2, true, (KeyCode)'?')] + [InlineData (',', ConsoleKey.OemComma, false, (KeyCode)',')] + [InlineData ('<', ConsoleKey.OemComma, true, (KeyCode)'<')] + [InlineData ('.', ConsoleKey.OemPeriod, false, (KeyCode)'.')] + [InlineData ('>', ConsoleKey.OemPeriod, true, (KeyCode)'>')] + [InlineData ('=', ConsoleKey.OemPlus, false, (KeyCode)'=')] // Un-shifted OemPlus is '=' + [InlineData ('+', ConsoleKey.OemPlus, true, (KeyCode)'+')] // Shifted OemPlus is '+' + [InlineData ('-', ConsoleKey.OemMinus, false, (KeyCode)'-')] + [InlineData ('_', ConsoleKey.OemMinus, true, (KeyCode)'_')] // Shifted OemMinus is '_' + public void ToKey_OEMKeys_ReturnsExpectedKeyCode ( + char unicodeChar, + ConsoleKey consoleKey, + bool shift, + KeyCode expectedKeyCode + ) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + WindowsConsole.InputRecord inputRecord = CreateInputRecord (unicodeChar, consoleKey, shift, false, false); + + // Act + var result = _converter.ToKey (inputRecord); + + // Assert + Assert.Equal (expectedKeyCode, result.KeyCode); + } + + #endregion + + #region ToKey Tests - NumPad + + [Theory] + [InlineData ('0', ConsoleKey.NumPad0, KeyCode.D0)] + [InlineData ('1', ConsoleKey.NumPad1, KeyCode.D1)] + [InlineData ('5', ConsoleKey.NumPad5, KeyCode.D5)] + [InlineData ('9', ConsoleKey.NumPad9, KeyCode.D9)] + public void ToKey_NumPadKeys_ReturnsExpectedKeyCode ( + char unicodeChar, + ConsoleKey consoleKey, + KeyCode expectedKeyCode + ) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + WindowsConsole.InputRecord inputRecord = CreateInputRecord (unicodeChar, consoleKey, false, false, false); + + // Act + var result = _converter.ToKey (inputRecord); + + // Assert + Assert.Equal (expectedKeyCode, result.KeyCode); + } + + [Theory] + [InlineData ('*', ConsoleKey.Multiply, (KeyCode)'*')] + [InlineData ('+', ConsoleKey.Add, (KeyCode)'+')] + [InlineData ('-', ConsoleKey.Subtract, (KeyCode)'-')] + [InlineData ('.', ConsoleKey.Decimal, (KeyCode)'.')] + [InlineData ('/', ConsoleKey.Divide, (KeyCode)'/')] + public void ToKey_NumPadOperators_ReturnsExpectedKeyCode ( + char unicodeChar, + ConsoleKey consoleKey, + KeyCode expectedKeyCode + ) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + WindowsConsole.InputRecord inputRecord = CreateInputRecord (unicodeChar, consoleKey, false, false, false); + + // Act + var result = _converter.ToKey (inputRecord); + + // Assert + Assert.Equal (expectedKeyCode, result.KeyCode); + } + + #endregion + + #region ToKey Tests - Null/Empty + + [Fact] + public void ToKey_NullKey_ReturnsEmpty () + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + WindowsConsole.InputRecord inputRecord = CreateInputRecord ('\0', ConsoleKey.None, false, false, false); + + // Act + var result = _converter.ToKey (inputRecord); + + // Assert + Assert.Equal (Key.Empty, result); + } + + #endregion + + #region ToKeyInfo Tests - Basic Keys + + [Theory] + [InlineData (KeyCode.A, ConsoleKey.A, 'a')] + [InlineData (KeyCode.A | KeyCode.ShiftMask, ConsoleKey.A, 'A')] + [InlineData (KeyCode.Z, ConsoleKey.Z, 'z')] + [InlineData (KeyCode.Z | KeyCode.ShiftMask, ConsoleKey.Z, 'Z')] + public void ToKeyInfo_LetterKeys_ReturnsExpectedInputRecord ( + KeyCode keyCode, + ConsoleKey expectedConsoleKey, + char expectedChar + ) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + var key = new Key (keyCode); + + // Act + WindowsConsole.InputRecord result = _converter.ToKeyInfo (key); + + // Assert + Assert.Equal (WindowsConsole.EventType.Key, result.EventType); + Assert.Equal ((VK)expectedConsoleKey, result.KeyEvent.wVirtualKeyCode); + Assert.Equal (expectedChar, result.KeyEvent.UnicodeChar); + Assert.True (result.KeyEvent.bKeyDown); + Assert.Equal ((ushort)1, result.KeyEvent.wRepeatCount); + } + + [Theory] + [InlineData (KeyCode.D0, ConsoleKey.D0, '0')] + [InlineData (KeyCode.D1, ConsoleKey.D1, '1')] + [InlineData (KeyCode.D9, ConsoleKey.D9, '9')] + public void ToKeyInfo_NumberKeys_ReturnsExpectedInputRecord ( + KeyCode keyCode, + ConsoleKey expectedConsoleKey, + char expectedChar + ) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + var key = new Key (keyCode); + + // Act + WindowsConsole.InputRecord result = _converter.ToKeyInfo (key); + + // Assert + Assert.Equal ((VK)expectedConsoleKey, result.KeyEvent.wVirtualKeyCode); + Assert.Equal (expectedChar, result.KeyEvent.UnicodeChar); + } + + #endregion + + #region ToKeyInfo Tests - Special Keys + + [Theory] + [InlineData (KeyCode.Enter, ConsoleKey.Enter, '\r')] + [InlineData (KeyCode.Esc, ConsoleKey.Escape, '\u001B')] + [InlineData (KeyCode.Tab, ConsoleKey.Tab, '\t')] + [InlineData (KeyCode.Backspace, ConsoleKey.Backspace, '\b')] + [InlineData (KeyCode.Space, ConsoleKey.Spacebar, ' ')] + public void ToKeyInfo_SpecialKeys_ReturnsExpectedInputRecord ( + KeyCode keyCode, + ConsoleKey expectedConsoleKey, + char expectedChar + ) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + var key = new Key (keyCode); + + // Act + WindowsConsole.InputRecord result = _converter.ToKeyInfo (key); + + // Assert + Assert.Equal ((VK)expectedConsoleKey, result.KeyEvent.wVirtualKeyCode); + Assert.Equal (expectedChar, result.KeyEvent.UnicodeChar); + } + + [Theory] + [InlineData (KeyCode.Delete, ConsoleKey.Delete)] + [InlineData (KeyCode.Insert, ConsoleKey.Insert)] + [InlineData (KeyCode.Home, ConsoleKey.Home)] + [InlineData (KeyCode.End, ConsoleKey.End)] + [InlineData (KeyCode.PageUp, ConsoleKey.PageUp)] + [InlineData (KeyCode.PageDown, ConsoleKey.PageDown)] + [InlineData (KeyCode.CursorUp, ConsoleKey.UpArrow)] + [InlineData (KeyCode.CursorDown, ConsoleKey.DownArrow)] + [InlineData (KeyCode.CursorLeft, ConsoleKey.LeftArrow)] + [InlineData (KeyCode.CursorRight, ConsoleKey.RightArrow)] + public void ToKeyInfo_NavigationKeys_ReturnsExpectedInputRecord (KeyCode keyCode, ConsoleKey expectedConsoleKey) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + var key = new Key (keyCode); + + // Act + WindowsConsole.InputRecord result = _converter.ToKeyInfo (key); + + // Assert + Assert.Equal ((VK)expectedConsoleKey, result.KeyEvent.wVirtualKeyCode); + } + + [Theory] + [InlineData (KeyCode.F1, ConsoleKey.F1)] + [InlineData (KeyCode.F5, ConsoleKey.F5)] + [InlineData (KeyCode.F10, ConsoleKey.F10)] + [InlineData (KeyCode.F12, ConsoleKey.F12)] + public void ToKeyInfo_FunctionKeys_ReturnsExpectedInputRecord (KeyCode keyCode, ConsoleKey expectedConsoleKey) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + var key = new Key (keyCode); + + // Act + WindowsConsole.InputRecord result = _converter.ToKeyInfo (key); + + // Assert + Assert.Equal ((VK)expectedConsoleKey, result.KeyEvent.wVirtualKeyCode); + } + + #endregion + + #region ToKeyInfo Tests - Modifiers + + [Theory] + [InlineData (KeyCode.A | KeyCode.ShiftMask, WindowsConsole.ControlKeyState.ShiftPressed)] + [InlineData (KeyCode.A | KeyCode.CtrlMask, WindowsConsole.ControlKeyState.LeftControlPressed)] + [InlineData (KeyCode.A | KeyCode.AltMask, WindowsConsole.ControlKeyState.LeftAltPressed)] + [InlineData ( + KeyCode.A | KeyCode.CtrlMask | KeyCode.AltMask, + WindowsConsole.ControlKeyState.LeftControlPressed | WindowsConsole.ControlKeyState.LeftAltPressed)] + [InlineData ( + KeyCode.A | KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask, + WindowsConsole.ControlKeyState.ShiftPressed + | WindowsConsole.ControlKeyState.LeftControlPressed + | WindowsConsole.ControlKeyState.LeftAltPressed)] + public void ToKeyInfo_WithModifiers_ReturnsExpectedControlKeyState ( + KeyCode keyCode, + WindowsConsole.ControlKeyState expectedState + ) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + var key = new Key (keyCode); + + // Act + WindowsConsole.InputRecord result = _converter.ToKeyInfo (key); + + // Assert + Assert.Equal (expectedState, result.KeyEvent.dwControlKeyState); + } + + #endregion + + #region ToKeyInfo Tests - Scan Codes + + [Theory] + [InlineData (KeyCode.A, 30)] + [InlineData (KeyCode.Enter, 28)] + [InlineData (KeyCode.Esc, 1)] + [InlineData (KeyCode.Space, 57)] + [InlineData (KeyCode.F1, 59)] + [InlineData (KeyCode.F10, 68)] + [InlineData (KeyCode.CursorUp, 72)] + [InlineData (KeyCode.Home, 71)] + public void ToKeyInfo_ScanCodes_ReturnsExpectedScanCode (KeyCode keyCode, ushort expectedScanCode) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + var key = new Key (keyCode); + + // Act + WindowsConsole.InputRecord result = _converter.ToKeyInfo (key); + + // Assert + Assert.Equal (expectedScanCode, result.KeyEvent.wVirtualScanCode); + } + + #endregion + + #region Round-Trip Tests + + [Theory] + [InlineData (KeyCode.A)] + [InlineData (KeyCode.A | KeyCode.ShiftMask)] + [InlineData (KeyCode.A | KeyCode.CtrlMask)] + [InlineData (KeyCode.Enter)] + [InlineData (KeyCode.F1)] + [InlineData (KeyCode.CursorUp)] + [InlineData (KeyCode.Delete)] + [InlineData (KeyCode.D5)] + [InlineData (KeyCode.Space)] + public void RoundTrip_ToKeyInfo_ToKey_PreservesKeyCode (KeyCode originalKeyCode) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + var originalKey = new Key (originalKeyCode); + + // Act + WindowsConsole.InputRecord inputRecord = _converter.ToKeyInfo (originalKey); + var roundTrippedKey = _converter.ToKey (inputRecord); + + // Assert + Assert.Equal (originalKeyCode, roundTrippedKey.KeyCode); + } + + [Theory] + [InlineData ('a', ConsoleKey.A, false, false, false)] + [InlineData ('A', ConsoleKey.A, true, false, false)] + [InlineData ('a', ConsoleKey.A, false, false, true)] // Ctrl+A + [InlineData ('0', ConsoleKey.D0, false, false, false)] + public void RoundTrip_ToKey_ToKeyInfo_PreservesData ( + char unicodeChar, + ConsoleKey consoleKey, + bool shift, + bool alt, + bool ctrl + ) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + // Arrange + WindowsConsole.InputRecord originalRecord = CreateInputRecord (unicodeChar, consoleKey, shift, alt, ctrl); + + // Act + var key = _converter.ToKey (originalRecord); + WindowsConsole.InputRecord roundTrippedRecord = _converter.ToKeyInfo (key); + + // Assert + Assert.Equal ((VK)consoleKey, roundTrippedRecord.KeyEvent.wVirtualKeyCode); + + // Check modifiers match + var expectedState = WindowsConsole.ControlKeyState.NoControlKeyPressed; + + if (shift) + { + expectedState |= WindowsConsole.ControlKeyState.ShiftPressed; + } + + if (alt) + { + expectedState |= WindowsConsole.ControlKeyState.LeftAltPressed; + } + + if (ctrl) + { + expectedState |= WindowsConsole.ControlKeyState.LeftControlPressed; + } + + Assert.True (roundTrippedRecord.KeyEvent.dwControlKeyState.HasFlag (expectedState)); + } + + #endregion + + #region CapsLock/NumLock Tests + + [Theory] + [InlineData ('a', ConsoleKey.A, false, true)] // CapsLock on, no shift + [InlineData ('A', ConsoleKey.A, true, true)] // CapsLock on, shift (should be lowercase from mapping) + public void ToKey_WithCapsLock_ReturnsExpectedKeyCode ( + char unicodeChar, + ConsoleKey consoleKey, + bool shift, + bool capsLock + ) + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + WindowsConsole.InputRecord inputRecord = CreateInputRecordWithLockStates ( + unicodeChar, + consoleKey, + shift, + false, + false, + capsLock, + false, + false); + + // Act + var result = _converter.ToKey (inputRecord); + + // Assert + // The mapping should handle CapsLock properly via WindowsKeyHelper.MapKey + Assert.NotEqual (KeyCode.Null, result.KeyCode); + } + + #endregion + + #region Helper Methods + + private static WindowsConsole.InputRecord CreateInputRecord ( + char unicodeChar, + ConsoleKey consoleKey, + bool shift, + bool alt, + bool ctrl + ) => + CreateInputRecordWithLockStates (unicodeChar, consoleKey, shift, alt, ctrl, false, false, false); + + private static WindowsConsole.InputRecord CreateInputRecordWithLockStates ( + char unicodeChar, + ConsoleKey consoleKey, + bool shift, + bool alt, + bool ctrl, + bool capsLock, + bool numLock, + bool scrollLock + ) + { + var controlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed; + + if (shift) + { + controlKeyState |= WindowsConsole.ControlKeyState.ShiftPressed; + } + + if (alt) + { + controlKeyState |= WindowsConsole.ControlKeyState.LeftAltPressed; + } + + if (ctrl) + { + controlKeyState |= WindowsConsole.ControlKeyState.LeftControlPressed; + } + + if (capsLock) + { + controlKeyState |= WindowsConsole.ControlKeyState.CapslockOn; + } + + if (numLock) + { + controlKeyState |= WindowsConsole.ControlKeyState.NumlockOn; + } + + if (scrollLock) + { + controlKeyState |= WindowsConsole.ControlKeyState.ScrolllockOn; + } + + return new () + { + EventType = WindowsConsole.EventType.Key, + KeyEvent = new () + { + bKeyDown = true, + wRepeatCount = 1, + wVirtualKeyCode = (VK)consoleKey, + wVirtualScanCode = 0, + UnicodeChar = unicodeChar, + dwControlKeyState = controlKeyState + } + }; + } + + private static WindowsConsole.InputRecord CreateVKPacketInputRecord (char unicodeChar) => + new () + { + EventType = WindowsConsole.EventType.Key, + KeyEvent = new () + { + bKeyDown = true, + wRepeatCount = 1, + wVirtualKeyCode = VK.PACKET, + wVirtualScanCode = 0, + UnicodeChar = unicodeChar, + dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed + } + }; + + #endregion +} diff --git a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs index 363412165..65a529a48 100644 --- a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs @@ -893,7 +893,7 @@ public class ListViewTests (ITestOutputHelper output) BorderStyle = LineStyle.Single }; lv.SetSource (["One", "Two", "Three", "Four"]); - lv.SelectedItemChanged += (s, e) => selected = e.Value.ToString (); + lv.SelectedItemChanged += (s, e) => selected = e.Value!.ToString (); var top = new Toplevel (); top.Add (lv); app.Begin (top); @@ -950,7 +950,7 @@ public class ListViewTests (ITestOutputHelper output) Assert.Equal (2, lv.SelectedItem); top.Dispose (); - app.Shutdown (); + app?.Shutdown (); } [Fact] @@ -1258,7 +1258,7 @@ Item 6", IApplication? app = Application.Create (); app.Init ("fake"); IDriver? driver = app.Driver; - driver.SetScreenSize (8, 2); + driver?.SetScreenSize (8, 2); ObservableCollection source = ["First", "Second"]; var lv = new ListView { Width = Dim.Fill (), Height = 1, Source = new ListWrapper (source) }; @@ -1282,7 +1282,7 @@ Item 6", for (var i = 0; i < 7; i++) { - item += app.Driver?.Contents [line, i].Rune; + item += (app.Driver?.Contents!) [line, i].Rune; } return item; @@ -1430,8 +1430,8 @@ Three", _output, app?.Driver); // Scroll down - app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledDown }); - app.LayoutAndDraw (); + app?.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledDown }); + app?.LayoutAndDraw (); Assert.Equal (1, lv.TopItem); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -1441,8 +1441,8 @@ Four ", _output, app?.Driver); // Scroll up - app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledUp }); - app.LayoutAndDraw (); + app?.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledUp }); + app?.LayoutAndDraw (); Assert.Equal (0, lv.TopItem); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -1452,7 +1452,7 @@ Three", _output, app?.Driver); top.Dispose (); - app.Shutdown (); + app?.Shutdown (); } [Fact] @@ -1492,7 +1492,7 @@ Three - lo", _output, app?.Driver); lv.ScrollHorizontal (1); - app.LayoutAndDraw (); + app?.LayoutAndDraw (); Assert.Equal (1, lv.LeftItem); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -1502,8 +1502,8 @@ hree - lon", _output, app?.Driver); // Scroll right with mouse - app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledRight }); - app.LayoutAndDraw (); + app?.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledRight }); + app?.LayoutAndDraw (); Assert.Equal (2, lv.LeftItem); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -1513,8 +1513,8 @@ ree - long", _output, app?.Driver); // Scroll left with mouse - app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledLeft }); - app.LayoutAndDraw (); + app?.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledLeft }); + app?.LayoutAndDraw (); Assert.Equal (1, lv.LeftItem); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -1524,7 +1524,7 @@ hree - lon", _output, app?.Driver); top.Dispose (); - app.Shutdown (); + app?.Shutdown (); } [Fact] From cd75a20c6059092b4184ae9e733b4cfa868b624d Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 20 Nov 2025 18:45:13 +0000 Subject: [PATCH 05/16] Fixes #4387. Runes should not be used on a cell, but rather should use a single grapheme rendering 1 or 2 columns (#4388) * Fixes #4382. StringExtensions.GetColumns method should only return the total text width and not the sum of all runes width * Trying to fix unit test error * Update StringExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Resolving merge conflicts * Prevents Runes throwing if Grapheme is null * Add unit test to prove that null and empty string doesn't not throws anything. * Fix unit test failure * Fix IsValidLocation for wide graphemes * Add more combining * Prevent set invalid graphemes * Fix unit tests * Grapheme doesn't support invalid code points like lone surrogates * Fixes more unit tests * Fix unit test * Seems all test are fixed now * Adjust CharMap scenario with graphemes * Upgrade Wcwidth to version 4.0.0 * Reformat * Trying fix CheckDefaultState assertion * Revert "Trying fix CheckDefaultState assertion" This reverts commit c9b46b796ad206f2dd310fa680163b9f7de878f2. * Forgot to include driver.End in the test * Reapply "Trying fix CheckDefaultState assertion" This reverts commit 1060ac9b632c109dee785fa83057959575875ca2. * Remove ToString * Fix merge errors * Change to conditional expression * Assertion to prove that no exception throws during cell initialization. * Remove unnecessary assignment * Remove assignment to end * Replace string concatenation with 'StringBuilder'. * Replace more string concatenation with 'StringBuilder' * Remove redundant call to 'ToString' because Rune cast to a String object. * Replace foreach loop with Sum linq --------- Co-authored-by: Tig Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Directory.Packages.props | 2 +- .../UICatalog/Scenarios/CombiningMarks.cs | 15 +- Examples/UICatalog/Scenarios/LineDrawing.cs | 2 +- Examples/UICatalog/Scenarios/Sliders.cs | 16 +- .../UICatalog/Scenarios/SyntaxHighlighting.cs | 11 +- Terminal.Gui/Drawing/Cell.cs | 166 +- Terminal.Gui/Drawing/GraphemeHelper.cs | 49 + Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs | 9 +- Terminal.Gui/Drivers/DriverImpl.cs | 30 +- Terminal.Gui/Drivers/IDriver.cs | 6 +- Terminal.Gui/Drivers/IOutputBuffer.cs | 8 +- Terminal.Gui/Drivers/OutputBase.cs | 33 +- Terminal.Gui/Drivers/OutputBufferImpl.cs | 308 +-- Terminal.Gui/Text/StringExtensions.cs | 88 +- Terminal.Gui/Text/TextFormatter.cs | 328 +-- Terminal.Gui/ViewBase/Adornment/ShadowView.cs | 8 +- .../ViewBase/View.Drawing.Primitives.cs | 20 +- Terminal.Gui/ViewBase/View.Drawing.cs | 14 +- .../AutocompleteFilepathContext.cs | 26 +- .../Autocomplete/ISuggestionGenerator.cs | 4 +- .../Views/Autocomplete/PopupAutocomplete.cs | 2 +- .../SingleWordSuggestionGenerator.cs | 16 +- Terminal.Gui/Views/CharMap/CharMap.cs | 79 +- Terminal.Gui/Views/Slider/Slider.cs | 50 +- .../Views/TableView/TreeTableSource.cs | 8 +- Terminal.Gui/Views/TextInput/TextField.cs | 34 +- Terminal.Gui/Views/TextInput/TextModel.cs | 110 +- Terminal.Gui/Views/TextInput/TextView.cs | 55 +- Terminal.Gui/Views/TreeView/Branch.cs | 42 +- Tests/UnitTests/DriverAssert.cs | 31 +- Tests/UnitTests/Drivers/ClipRegionTests.cs | 16 +- Tests/UnitTests/View/Draw/ClipTests.cs | 10 +- Tests/UnitTests/View/Draw/DrawTests.cs | 18 +- Tests/UnitTests/View/TextTests.cs | 9 +- Tests/UnitTests/Views/LabelTests.cs | 14 +- Tests/UnitTests/Views/ProgressBarTests.cs | 2352 ++++++++--------- Tests/UnitTests/Views/TextFieldTests.cs | 1 + Tests/UnitTests/Views/TextViewTests.cs | 13 +- Tests/UnitTests/Views/TreeViewTests.cs | 14 +- .../Drawing/CellTests.cs | 166 +- .../Drivers/AddRuneTests.cs | 36 +- .../Drivers/ContentsTests.cs | 8 +- .../Drivers/DriverTests.cs | 48 +- .../Drivers/FakeDriverTests.cs | 2 +- .../UnitTestsParallelizable/Text/RuneTests.cs | 137 +- .../Text/StringTests.cs | 206 +- .../Text/TextFormatterDrawTests.cs | 27 + .../Text/TextFormatterTests.cs | 53 +- .../View/Draw/ViewClearViewportTests.cs | 10 +- .../Draw/ViewDrawTextAndLineCanvasTests.cs | 12 +- .../Views/ListViewTests.cs | 7 +- .../Views/TextFieldTests.cs | 6 +- .../Views/TextViewTests.cs | 64 +- 53 files changed, 2654 insertions(+), 2145 deletions(-) create mode 100644 Terminal.Gui/Drawing/GraphemeHelper.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index d62a1d298..2fdb4633e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,7 +18,7 @@ - + diff --git a/Examples/UICatalog/Scenarios/CombiningMarks.cs b/Examples/UICatalog/Scenarios/CombiningMarks.cs index 47b8dfbc7..62181c500 100644 --- a/Examples/UICatalog/Scenarios/CombiningMarks.cs +++ b/Examples/UICatalog/Scenarios/CombiningMarks.cs @@ -16,7 +16,8 @@ public class CombiningMarks : Scenario Application.Current!.SetNeedsDraw (); var i = -1; - top.AddStr ("Terminal.Gui only supports combining marks that normalize. See Issue #2616."); + top.Move (0, ++i); + top.AddStr ("Terminal.Gui supports all combining sequences that can be rendered as an unique grapheme."); top.Move (0, ++i); top.AddStr ("\u0301<- \"\\u0301\" using AddStr."); top.Move (0, ++i); @@ -38,7 +39,7 @@ public class CombiningMarks : Scenario top.AddRune ('\u0301'); top.AddRune ('\u0328'); top.AddRune (']'); - top.AddStr ("<- \"[a\\u0301\\u0301\\u0328]\" using AddRune for each."); + top.AddStr ("<- \"[a\\u0301\\u0301\\u0328]\" using AddRune for each. Avoid use AddRune for combining sequences because may result with empty blocks at end."); top.Move (0, ++i); top.AddStr ("[a\u0301\u0301\u0328]<- \"[a\\u0301\\u0301\\u0328]\" using AddStr."); top.Move (0, ++i); @@ -82,6 +83,16 @@ public class CombiningMarks : Scenario top.AddStr ("[\U0001F468\U0001F469\U0001F9D2]<- \"[\\U0001F468\\U0001F469\\U0001F9D2]\" using AddStr."); top.Move (0, ++i); top.AddStr ("[\U0001F468\u200D\U0001F469\u200D\U0001F9D2]<- \"[\\U0001F468\\u200D\\U0001F469\\u200D\\U0001F9D2]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[\U0001F468\u200D\U0001F469\u200D\U0001F467\u200D\U0001F466]<- \"[\\U0001F468\\u200D\\U0001F469\\u200D\\U0001F467\\u200D\\U0001F466]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[\u0e32\u0e33]<- \"[\\u0e32\\u0e33]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[\U0001F469\u200D\u2764\uFE0F\u200D\U0001F48B\u200D\U0001F468]<- \"[\\U0001F469\\u200D\\u2764\\uFE0F\\u200D\\U0001F48B\\u200D\\U0001F468]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[\u0061\uFE20\u0065\uFE21]<- \"[\\u0061\\uFE20\\u0065\\uFE21]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[\u1100\uD7B0]<- \"[\\u1100\\uD7B0]\" using AddStr."); }; Application.Run (top); diff --git a/Examples/UICatalog/Scenarios/LineDrawing.cs b/Examples/UICatalog/Scenarios/LineDrawing.cs index cbc8dd27b..217aea27b 100644 --- a/Examples/UICatalog/Scenarios/LineDrawing.cs +++ b/Examples/UICatalog/Scenarios/LineDrawing.cs @@ -284,7 +284,7 @@ public class DrawingArea : View SetCurrentAttribute (c.Value.Value.Attribute ?? GetAttributeForRole (VisualRole.Normal)); // TODO: #2616 - Support combining sequences that don't normalize - AddRune (c.Key.X, c.Key.Y, c.Value.Value.Rune); + AddStr (c.Key.X, c.Key.Y, c.Value.Value.Grapheme); } } } diff --git a/Examples/UICatalog/Scenarios/Sliders.cs b/Examples/UICatalog/Scenarios/Sliders.cs index fae50c015..8d954d100 100644 --- a/Examples/UICatalog/Scenarios/Sliders.cs +++ b/Examples/UICatalog/Scenarios/Sliders.cs @@ -86,17 +86,17 @@ public class Sliders : Scenario { if (single.Orientation == Orientation.Horizontal) { - single.Style.SpaceChar = new () { Rune = Glyphs.HLine }; - single.Style.OptionChar = new () { Rune = Glyphs.HLine }; + single.Style.SpaceChar = new () { Grapheme = Glyphs.HLine.ToString () }; + single.Style.OptionChar = new () { Grapheme = Glyphs.HLine.ToString () }; } else { - single.Style.SpaceChar = new () { Rune = Glyphs.VLine }; - single.Style.OptionChar = new () { Rune = Glyphs.VLine }; + single.Style.SpaceChar = new () { Grapheme = Glyphs.VLine.ToString () }; + single.Style.OptionChar = new () { Grapheme = Glyphs.VLine.ToString () }; } }; - single.Style.SetChar = new () { Rune = Glyphs.ContinuousMeterSegment }; - single.Style.DragChar = new () { Rune = Glyphs.ContinuousMeterSegment }; + single.Style.SetChar = new () { Grapheme = Glyphs.ContinuousMeterSegment.ToString () }; + single.Style.DragChar = new () { Grapheme = Glyphs.ContinuousMeterSegment.ToString () }; v.Add (single); @@ -257,7 +257,7 @@ public class Sliders : Scenario { s.Orientation = Orientation.Horizontal; - s.Style.SpaceChar = new () { Rune = Glyphs.HLine }; + s.Style.SpaceChar = new () { Grapheme = Glyphs.HLine.ToString () }; if (prev == null) { @@ -275,7 +275,7 @@ public class Sliders : Scenario { s.Orientation = Orientation.Vertical; - s.Style.SpaceChar = new () { Rune = Glyphs.VLine }; + s.Style.SpaceChar = new () { Grapheme = Glyphs.VLine.ToString () }; if (prev == null) { diff --git a/Examples/UICatalog/Scenarios/SyntaxHighlighting.cs b/Examples/UICatalog/Scenarios/SyntaxHighlighting.cs index 7b0c05462..ad9f93460 100644 --- a/Examples/UICatalog/Scenarios/SyntaxHighlighting.cs +++ b/Examples/UICatalog/Scenarios/SyntaxHighlighting.cs @@ -152,12 +152,12 @@ public class SyntaxHighlighting : Scenario ), null, new ( - "_Load Rune Cells", + "_Load Text Cells", "", () => ApplyLoadCells () ), new ( - "_Save Rune Cells", + "_Save Text Cells", "", () => SaveCells () ), @@ -240,12 +240,9 @@ public class SyntaxHighlighting : Scenario { string csName = color.Key; - foreach (Rune rune in csName.EnumerateRunes ()) - { - cells.Add (new () { Rune = rune, Attribute = color.Value.Normal }); - } + cells.AddRange (Cell.ToCellList (csName, color.Value.Normal)); - cells.Add (new () { Rune = (Rune)'\n', Attribute = color.Value.Focus }); + cells.Add (new () { Grapheme = "\n", Attribute = color.Value.Focus }); } if (File.Exists (_path)) diff --git a/Terminal.Gui/Drawing/Cell.cs b/Terminal.Gui/Drawing/Cell.cs index 2fa98bef1..f7da577ad 100644 --- a/Terminal.Gui/Drawing/Cell.cs +++ b/Terminal.Gui/Drawing/Cell.cs @@ -1,80 +1,108 @@  - namespace Terminal.Gui.Drawing; /// /// Represents a single row/column in a Terminal.Gui rendering surface (e.g. and /// ). /// -public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Rune Rune = default) +public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, string Grapheme = "") { /// The attributes to use when drawing the Glyph. public Attribute? Attribute { get; set; } = Attribute; /// - /// Gets or sets a value indicating whether this has been modified since the + /// Gets or sets a value indicating whether this has been modified since the /// last time it was drawn. /// public bool IsDirty { get; set; } = IsDirty; - private Rune _rune = Rune; + private string _grapheme = Grapheme; - /// The character to display. If is , then is ignored. - public Rune Rune + /// + /// The single grapheme cluster to display from this cell. If is or + /// , then is ignored. + /// + public string Grapheme { - get => _rune; + readonly get => _grapheme; set { - _combiningMarks?.Clear (); - _rune = value; + if (GraphemeHelper.GetGraphemes(value).ToArray().Length > 1) + { + throw new InvalidOperationException ($"Only a single {nameof (Grapheme)} cluster is allowed per Cell."); + } + + if (!string.IsNullOrEmpty (value) && value.Length == 1 && char.IsSurrogate (value [0])) + { + throw new ArgumentException ($"Only valid Unicode scalar values are allowed in a single {nameof (Grapheme)} cluster."); + } + + try + { + _grapheme = !string.IsNullOrEmpty (value) && !value.IsNormalized (NormalizationForm.FormC) + ? value.Normalize (NormalizationForm.FormC) + : value; + } + catch (ArgumentException) + { + // leave text unnormalized + _grapheme = value; + } } } - private List? _combiningMarks; - /// - /// The combining marks for that when combined makes this Cell a combining sequence. If - /// empty, then is ignored. + /// The rune for or runes for that when combined makes this Cell a combining sequence. /// /// - /// Only valid in the rare case where is a combining sequence that could not be normalized to a - /// single Rune. + /// In the case where has more than one rune it is a combining sequence that is normalized to a + /// single Text which may occupies 1 or 2 columns. /// - internal IReadOnlyList CombiningMarks - { - // PERFORMANCE: Downside of the interface return type is that List struct enumerator cannot be utilized, i.e. enumerator is allocated. - // If enumeration is used heavily in the future then might be better to expose the List Enumerator directly via separate mechanism. - get - { - // Avoid unnecessary list allocation. - if (_combiningMarks == null) - { - return Array.Empty (); - } - return _combiningMarks; - } - } - - /// - /// Adds combining mark to the cell. - /// - /// The combining mark to add to the cell. - internal void AddCombiningMark (Rune combiningMark) - { - _combiningMarks ??= []; - _combiningMarks.Add (combiningMark); - } - - /// - /// Clears combining marks of the cell. - /// - internal void ClearCombiningMarks () - { - _combiningMarks?.Clear (); - } + public IReadOnlyList Runes => string.IsNullOrEmpty (Grapheme) ? [] : Grapheme.EnumerateRunes ().ToList (); /// - public override string ToString () { return $"['{Rune}':{Attribute}]"; } + public override string ToString () + { + string visibleText = EscapeControlAndInvisible (Grapheme); + + return $"[\"{visibleText}\":{Attribute}]"; + } + + private static string EscapeControlAndInvisible (string text) + { + if (string.IsNullOrEmpty (text)) + { + return ""; + } + + var sb = new StringBuilder (); + + foreach (var rune in text.EnumerateRunes ()) + { + switch (rune.Value) + { + case '\0': sb.Append ("␀"); break; + case '\t': sb.Append ("\\t"); break; + case '\r': sb.Append ("\\r"); break; + case '\n': sb.Append ("\\n"); break; + case '\f': sb.Append ("\\f"); break; + case '\v': sb.Append ("\\v"); break; + default: + if (char.IsControl ((char)rune.Value)) + { + // show as \uXXXX + sb.Append ($"\\u{rune.Value:X4}"); + } + else + { + sb.Append (rune); + } + break; + } + } + + return sb.ToString (); + } /// Converts the string into a . /// The string to convert. @@ -82,12 +110,8 @@ public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Ru /// public static List ToCellList (string str, Attribute? attribute = null) { - List cells = new (); - - foreach (Rune rune in str.EnumerateRunes ()) - { - cells.Add (new () { Rune = rune, Attribute = attribute }); - } + List cells = []; + cells.AddRange (GraphemeHelper.GetGraphemes (str).Select (grapheme => new Cell { Grapheme = grapheme, Attribute = attribute })); return cells; } @@ -100,9 +124,7 @@ public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Ru /// A for each line. public static List> StringToLinesOfCells (string content, Attribute? attribute = null) { - List cells = content.EnumerateRunes () - .Select (x => new Cell { Rune = x, Attribute = attribute }) - .ToList (); + List cells = ToCellList (content, attribute); return SplitNewLines (cells); } @@ -112,14 +134,14 @@ public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Ru /// public static string ToString (IEnumerable cells) { - var str = string.Empty; + StringBuilder sb = new (); foreach (Cell cell in cells) { - str += cell.Rune.ToString (); + sb.Append (cell.Grapheme); } - return str; + return sb.ToString (); } /// Converts a generic collection into a string. @@ -147,26 +169,19 @@ public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Ru internal static List StringToCells (string str, Attribute? attribute = null) { - List cells = []; - - foreach (Rune rune in str.ToRunes ()) - { - cells.Add (new () { Rune = rune, Attribute = attribute }); - } - - return cells; + return ToCellList (str, attribute); } - internal static List ToCells (IEnumerable runes, Attribute? attribute = null) + internal static List ToCells (IEnumerable strings, Attribute? attribute = null) { - List cells = new (); + StringBuilder sb = new (); - foreach (Rune rune in runes) + foreach (string str in strings) { - cells.Add (new () { Rune = rune, Attribute = attribute }); + sb.Append (str); } - return cells; + return ToCellList (sb.ToString (), attribute); } private static List> SplitNewLines (List cells) @@ -179,14 +194,15 @@ public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Ru // ASCII code 10 = Line Feed. for (; i < cells.Count; i++) { - if (cells [i].Rune.Value == 13) + if (cells [i].Grapheme.Length == 1 && cells [i].Grapheme [0] == 13) { hasCR = true; continue; } - if (cells [i].Rune.Value == 10) + if ((cells [i].Grapheme.Length == 1 && cells [i].Grapheme [0] == 10) + || cells [i].Grapheme == "\r\n") { if (i - start > 0) { diff --git a/Terminal.Gui/Drawing/GraphemeHelper.cs b/Terminal.Gui/Drawing/GraphemeHelper.cs new file mode 100644 index 000000000..4ae00148c --- /dev/null +++ b/Terminal.Gui/Drawing/GraphemeHelper.cs @@ -0,0 +1,49 @@ +using System.Globalization; + +namespace Terminal.Gui.Drawing; + +/// +/// Provides utility methods for enumerating Unicode grapheme clusters (user-perceived characters) +/// in a string. A grapheme cluster may consist of one or more values, +/// including combining marks or zero-width joiner (ZWJ) sequences such as emoji family groups. +/// +/// +/// +/// This helper uses to enumerate +/// text elements according to the Unicode Standard Annex #29 (UAX #29) rules for +/// extended grapheme clusters. +/// +/// +/// On legacy Windows consoles (e.g., cmd.exe, conhost.exe), complex grapheme +/// sequences such as ZWJ emoji or combining marks may not render correctly, even though +/// the underlying string data is valid. +/// +/// +/// For most accurate visual rendering, prefer modern terminals such as Windows Terminal +/// or Linux-based terminals with full Unicode and font support. +/// +/// +public static class GraphemeHelper +{ + /// + /// Enumerates extended grapheme clusters from a string. + /// Handles surrogate pairs, combining marks, and basic ZWJ sequences. + /// Safe for legacy consoles; memory representation is correct. + /// + public static IEnumerable GetGraphemes (string text) + { + if (string.IsNullOrEmpty (text)) + { + yield break; + } + + TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator (text); + + while (enumerator.MoveNext ()) + { + string element = enumerator.GetTextElement (); + + yield return element; + } + } +} diff --git a/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs b/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs index c426b6896..763467096 100644 --- a/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs +++ b/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs @@ -173,7 +173,7 @@ public class LineCanvas : IDisposable intersectionsBufferList.Clear (); foreach (var line in _lines) { - if (line.Intersects (x, y) is IntersectionDefinition intersect) + if (line.Intersects (x, y) is { } intersect) { intersectionsBufferList.Add (intersect); } @@ -217,9 +217,8 @@ public class LineCanvas : IDisposable for (int x = inArea.X; x < inArea.X + inArea.Width; x++) { IntersectionDefinition [] intersects = _lines - // ! nulls are filtered out by the next Where filter - .Select (l => l.Intersects (x, y)!) - .Where (i => i is not null) + .Select (l => l.Intersects (x, y)) + .OfType () // automatically filters nulls and casts .ToArray (); Rune? rune = GetRuneForIntersects (intersects); @@ -413,7 +412,7 @@ public class LineCanvas : IDisposable if (rune.HasValue) { - cell.Rune = rune.Value; + cell.Grapheme = rune.ToString ()!; } cell.Attribute = GetAttributeForIntersects (intersects); diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index ee125c143..9aebea3dd 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -253,8 +253,16 @@ internal class DriverImpl : IDriver /// public bool IsRuneSupported (Rune rune) => Rune.IsValid (rune.Value); - /// - public bool IsValidLocation (Rune rune, int col, int row) => OutputBuffer.IsValidLocation (rune, col, row); + /// Tests whether the specified coordinate are valid for drawing the specified Text. + /// Used to determine if one or two columns are required. + /// The column. + /// The row. + /// + /// if the coordinate is outside the screen bounds or outside of + /// . + /// otherwise. + /// + public bool IsValidLocation (string text, int col, int row) { return OutputBuffer.IsValidLocation (text, col, row); } /// public void Move (int col, int row) { OutputBuffer.Move (col, row); } @@ -379,26 +387,14 @@ internal class DriverImpl : IDriver { for (var c = 0; c < Cols; c++) { - Rune rune = contents [r, c].Rune; + string text = contents [r, c].Grapheme; - if (rune.DecodeSurrogatePair (out char []? sp)) - { - sb.Append (sp); - } - else - { - sb.Append ((char)rune.Value); - } + sb.Append (text); - if (rune.GetColumns () > 1) + if (text.GetColumns () > 1) { c++; } - - // See Issue #2616 - //foreach (var combMark in contents [r, c].CombiningMarks) { - // sb.Append ((char)combMark.Value); - //} } sb.AppendLine (); diff --git a/Terminal.Gui/Drivers/IDriver.cs b/Terminal.Gui/Drivers/IDriver.cs index 090c192a4..8616d8edf 100644 --- a/Terminal.Gui/Drivers/IDriver.cs +++ b/Terminal.Gui/Drivers/IDriver.cs @@ -124,8 +124,8 @@ public interface IDriver /// bool IsRuneSupported (Rune rune); - /// Tests whether the specified coordinate are valid for drawing the specified Rune. - /// Used to determine if one or two columns are required. + /// Tests whether the specified coordinate are valid for drawing the specified Text. + /// Used to determine if one or two columns are required. /// The column. /// The row. /// @@ -133,7 +133,7 @@ public interface IDriver /// . /// otherwise. /// - bool IsValidLocation (Rune rune, int col, int row); + bool IsValidLocation (string text, int col, int row); /// /// Updates and to the specified column and row in diff --git a/Terminal.Gui/Drivers/IOutputBuffer.cs b/Terminal.Gui/Drivers/IOutputBuffer.cs index 387fde46c..3344d0ba8 100644 --- a/Terminal.Gui/Drivers/IOutputBuffer.cs +++ b/Terminal.Gui/Drivers/IOutputBuffer.cs @@ -84,15 +84,15 @@ public interface IOutputBuffer void FillRect (Rectangle rect, char rune); /// - /// Tests whether the specified coordinate is valid for drawing the specified Rune. + /// Tests whether the specified coordinate is valid for drawing the specified Text. /// - /// Used to determine if one or two columns are required. + /// Used to determine if one or two columns are required. /// The column. /// The row. /// - /// True if the coordinate is valid for the Rune; false otherwise. + /// True if the coordinate is valid for the Text; false otherwise. /// - bool IsValidLocation (Rune rune, int col, int row); + bool IsValidLocation (string text, int col, int row); /// /// The first cell index on left of screen - basically always 0. diff --git a/Terminal.Gui/Drivers/OutputBase.cs b/Terminal.Gui/Drivers/OutputBase.cs index ca8b92c01..6c10fad0b 100644 --- a/Terminal.Gui/Drivers/OutputBase.cs +++ b/Terminal.Gui/Drivers/OutputBase.cs @@ -76,25 +76,6 @@ public abstract class OutputBase outputWidth++; - // Handle special cases that AppendCellAnsi doesn't cover - Rune rune = cell.Rune; - if (cell.CombiningMarks.Count > 0) - { - // AtlasEngine does not support NON-NORMALIZED combining marks in a way - // compatible with the driver architecture. Any CMs (except in the first col) - // are correctly combined with the base char, but are ALSO treated as 1 column - // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`. - // - // For now, we just ignore the list of CMs. - //foreach (var combMark in Contents [row, col].CombiningMarks) { - // output.Append (combMark); - } - else if (rune.IsSurrogatePair () && rune.GetColumns () < 2) - { - WriteToConsole (output, ref lastCol, row, ref outputWidth); - SetCursorPositionImpl (col - 1, row); - } - buffer.Contents [row, col].IsDirty = false; } } @@ -216,16 +197,12 @@ public abstract class OutputBase redrawTextStyle = attribute.Value.Style; } - // Add the character - const int MAX_CHARS_PER_RUNE = 2; - Span runeBuffer = stackalloc char [MAX_CHARS_PER_RUNE]; - Rune rune = cell.Rune; - int runeCharsWritten = rune.EncodeToUtf16 (runeBuffer); - ReadOnlySpan runeChars = runeBuffer [..runeCharsWritten]; - output.Append (runeChars); + // Add the grapheme + string grapheme = cell.Grapheme; + output.Append (grapheme); - // Handle wide characters - if (rune.GetColumns () > 1 && currentCol + 1 < maxCol) + // Handle wide grapheme + if (grapheme.GetColumns () > 1 && currentCol + 1 < maxCol) { currentCol++; // Skip next cell for wide character } diff --git a/Terminal.Gui/Drivers/OutputBufferImpl.cs b/Terminal.Gui/Drivers/OutputBufferImpl.cs index d12eb7991..30f112b4b 100644 --- a/Terminal.Gui/Drivers/OutputBufferImpl.cs +++ b/Terminal.Gui/Drivers/OutputBufferImpl.cs @@ -65,7 +65,9 @@ public class OutputBufferImpl : IOutputBuffer /// The topmost row in the terminal. public virtual int Top { get; set; } = 0; - /// + /// + /// Indicates which lines have been modified and need to be redrawn. + /// public bool [] DirtyLines { get; set; } = []; // QUESTION: When non-full screen apps are supported, will this represent the app size, or will that be in Application? @@ -112,178 +114,8 @@ public class OutputBufferImpl : IOutputBuffer /// will be added instead. /// /// - /// Rune to add. - public void AddRune (Rune rune) - { - int runeWidth = -1; - bool validLocation = IsValidLocation (rune, Col, Row); - - if (Contents is null) - { - return; - } - - Clip ??= new (Screen); - - Rectangle clipRect = Clip!.GetBounds (); - - if (validLocation) - { - rune = rune.MakePrintable (); - runeWidth = rune.GetColumns (); - - lock (Contents) - { - if (runeWidth == 0 && rune.IsCombiningMark ()) - { - // AtlasEngine does not support NON-NORMALIZED combining marks in a way - // compatible with the driver architecture. Any CMs (except in the first col) - // are correctly combined with the base char, but are ALSO treated as 1 column - // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`. - // - // Until this is addressed (see Issue #), we do our best by - // a) Attempting to normalize any CM with the base char to it's left - // b) Ignoring any CMs that don't normalize - if (Col > 0) - { - if (Contents [Row, Col - 1].CombiningMarks.Count > 0) - { - // Just add this mark to the list - Contents [Row, Col - 1].AddCombiningMark (rune); - - // Ignore. Don't move to next column (let the driver figure out what to do). - } - else - { - // Attempt to normalize the cell to our left combined with this mark - string combined = Contents [Row, Col - 1].Rune + rune.ToString (); - - // Normalize to Form C (Canonical Composition) - string normalized = combined.Normalize (NormalizationForm.FormC); - - if (normalized.Length == 1) - { - // It normalized! We can just set the Cell to the left with the - // normalized codepoint - Contents [Row, Col - 1].Rune = (Rune)normalized [0]; - - // Ignore. Don't move to next column because we're already there - } - else - { - // It didn't normalize. Add it to the Cell to left's CM list - Contents [Row, Col - 1].AddCombiningMark (rune); - - // Ignore. Don't move to next column (let the driver figure out what to do). - } - } - - Contents [Row, Col - 1].Attribute = CurrentAttribute; - Contents [Row, Col - 1].IsDirty = true; - } - else - { - // Most drivers will render a combining mark at col 0 as the mark - Contents [Row, Col].Rune = rune; - Contents [Row, Col].Attribute = CurrentAttribute; - Contents [Row, Col].IsDirty = true; - Col++; - } - } - else - { - Contents [Row, Col].Attribute = CurrentAttribute; - Contents [Row, Col].IsDirty = true; - - if (Col > 0) - { - // Check if cell to left has a wide glyph - if (Contents [Row, Col - 1].Rune.GetColumns () > 1) - { - // Invalidate cell to left - Contents [Row, Col - 1].Rune = Rune.ReplacementChar; - Contents [Row, Col - 1].IsDirty = true; - } - } - - if (runeWidth < 1) - { - Contents [Row, Col].Rune = Rune.ReplacementChar; - } - else if (runeWidth == 1) - { - Contents [Row, Col].Rune = rune; - - if (Col < clipRect.Right - 1) - { - Contents [Row, Col + 1].IsDirty = true; - } - } - else if (runeWidth == 2) - { - if (!Clip.Contains (Col + 1, Row)) - { - // We're at the right edge of the clip, so we can't display a wide character. - // TODO: Figure out if it is better to show a replacement character or ' ' - Contents [Row, Col].Rune = Rune.ReplacementChar; - } - else if (!Clip.Contains (Col, Row)) - { - // Our 1st column is outside the clip, so we can't display a wide character. - Contents [Row, Col + 1].Rune = Rune.ReplacementChar; - } - else - { - Contents [Row, Col].Rune = rune; - - if (Col < clipRect.Right - 1) - { - // Invalidate cell to right so that it doesn't get drawn - // TODO: Figure out if it is better to show a replacement character or ' ' - Contents [Row, Col + 1].Rune = Rune.ReplacementChar; - Contents [Row, Col + 1].IsDirty = true; - } - } - } - else - { - // This is a non-spacing character, so we don't need to do anything - Contents [Row, Col].Rune = (Rune)' '; - Contents [Row, Col].IsDirty = false; - } - - DirtyLines [Row] = true; - } - } - } - - if (runeWidth is < 0 or > 0) - { - Col++; - } - - if (runeWidth > 1) - { - Debug.Assert (runeWidth <= 2); - - if (validLocation && Col < clipRect.Right) - { - lock (Contents!) - { - // This is a double-width character, and we are not at the end of the line. - // Col now points to the second column of the character. Ensure it doesn't - // Get rendered. - Contents [Row, Col].IsDirty = false; - Contents [Row, Col].Attribute = CurrentAttribute; - - // TODO: Determine if we should wipe this out (for now now) - //Contents [Row, Col].Rune = (Rune)' '; - } - } - - Col++; - } - } + /// Text to add. + public void AddRune (Rune rune) { AddStr (rune.ToString ()); } /// /// Adds the specified to the display at the current cursor position. This method is a @@ -296,7 +128,7 @@ public class OutputBufferImpl : IOutputBuffer /// /// /// When the method returns, will be incremented by the number of columns - /// required, unless the new column value is outside of the or screen + /// required, unless the new column value is outside the or screen /// dimensions defined by . /// /// If requires more columns than are available, the output will be clipped. @@ -304,11 +136,112 @@ public class OutputBufferImpl : IOutputBuffer /// String. public void AddStr (string str) { - List runes = str.EnumerateRunes ().ToList (); - - for (var i = 0; i < runes.Count; i++) + foreach (string grapheme in GraphemeHelper.GetGraphemes (str)) { - AddRune (runes [i]); + string text = grapheme; + + int textWidth = -1; + bool validLocation = IsValidLocation (text, Col, Row); + + if (Contents is null) + { + return; + } + + Clip ??= new (Screen); + + Rectangle clipRect = Clip!.GetBounds (); + + if (validLocation) + { + text = text.MakePrintable (); + textWidth = text.GetColumns (); + + lock (Contents) + { + Contents [Row, Col].Attribute = CurrentAttribute; + Contents [Row, Col].IsDirty = true; + + if (Col > 0) + { + // Check if cell to left has a wide glyph + if (Contents [Row, Col - 1].Grapheme.GetColumns () > 1) + { + // Invalidate cell to left + Contents [Row, Col - 1].Grapheme = Rune.ReplacementChar.ToString (); + Contents [Row, Col - 1].IsDirty = true; + } + } + + if (textWidth is 0 or 1) + { + Contents [Row, Col].Grapheme = text; + + if (Col < clipRect.Right - 1) + { + Contents [Row, Col + 1].IsDirty = true; + } + } + else if (textWidth == 2) + { + if (!Clip.Contains (Col + 1, Row)) + { + // We're at the right edge of the clip, so we can't display a wide character. + // TODO: Figure out if it is better to show a replacement character or ' ' + Contents [Row, Col].Grapheme = Rune.ReplacementChar.ToString (); + } + else if (!Clip.Contains (Col, Row)) + { + // Our 1st column is outside the clip, so we can't display a wide character. + Contents [Row, Col + 1].Grapheme = Rune.ReplacementChar.ToString (); + } + else + { + Contents [Row, Col].Grapheme = text; + + if (Col < clipRect.Right - 1) + { + // Invalidate cell to right so that it doesn't get drawn + // TODO: Figure out if it is better to show a replacement character or ' ' + Contents [Row, Col + 1].Grapheme = Rune.ReplacementChar.ToString (); + Contents [Row, Col + 1].IsDirty = true; + } + } + } + else + { + // This is a non-spacing character, so we don't need to do anything + Contents [Row, Col].Grapheme = " "; + Contents [Row, Col].IsDirty = false; + } + + DirtyLines [Row] = true; + } + } + + Col++; + + if (textWidth > 1) + { + Debug.Assert (textWidth <= 2); + + if (validLocation && Col < clipRect.Right) + { + lock (Contents!) + { + // This is a double-width character, and we are not at the end of the line. + // Col now points to the second column of the character. Ensure it doesn't + // Get rendered. + Contents [Row, Col].IsDirty = false; + Contents [Row, Col].Attribute = CurrentAttribute; + + // TODO: Determine if we should wipe this out (for now now) + //Contents [Row, Col].Text = (Text)' '; + } + } + + Col++; + } } } @@ -331,7 +264,7 @@ public class OutputBufferImpl : IOutputBuffer { Contents [row, c] = new () { - Rune = (Rune)' ', + Grapheme = " ", Attribute = new Attribute (Color.White, Color.Black), IsDirty = true }; @@ -345,22 +278,19 @@ public class OutputBufferImpl : IOutputBuffer //ClearedContents?.Invoke (this, EventArgs.Empty); } - /// Tests whether the specified coordinate are valid for drawing the specified Rune. - /// Used to determine if one or two columns are required. + /// Tests whether the specified coordinate are valid for drawing the specified Text. + /// Used to determine if one or two columns are required. /// The column. /// The row. /// /// if the coordinate is outside the screen bounds or outside of . /// otherwise. /// - public bool IsValidLocation (Rune rune, int col, int row) + public bool IsValidLocation (string text, int col, int row) { - if (rune.GetColumns () < 2) - { - return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip!.Contains (col, row); - } + int textWidth = text.GetColumns (); - return Clip!.Contains (col, row) || Clip!.Contains (col + 1, row); + return col >= 0 && row >= 0 && col + textWidth <= Cols && row < Rows && Clip!.Contains (col, row); } /// @@ -383,14 +313,14 @@ public class OutputBufferImpl : IOutputBuffer { for (int c = rect.X; c < rect.X + rect.Width; c++) { - if (!IsValidLocation (rune, c, r)) + if (!IsValidLocation (rune.ToString (), c, r)) { continue; } Contents [r, c] = new () { - Rune = rune != default (Rune) ? rune : (Rune)' ', + Grapheme = rune != default (Rune) ? rune.ToString () : " ", Attribute = CurrentAttribute, IsDirty = true }; } diff --git a/Terminal.Gui/Text/StringExtensions.cs b/Terminal.Gui/Text/StringExtensions.cs index eb053a3d8..59f433b60 100644 --- a/Terminal.Gui/Text/StringExtensions.cs +++ b/Terminal.Gui/Text/StringExtensions.cs @@ -1,5 +1,4 @@ using System.Buffers; -using System.Globalization; namespace Terminal.Gui.Text; @@ -54,8 +53,9 @@ public static class StringExtensions /// Gets the number of columns the string occupies in the terminal. /// This is a Terminal.Gui extension method to to support TUI text manipulation. /// The string to measure. + /// Indicates whether to ignore values ​​less than zero, such as control keys. /// - public static int GetColumns (this string str) + public static int GetColumns (this string str, bool ignoreLessThanZero = true) { if (string.IsNullOrEmpty (str)) { @@ -63,17 +63,25 @@ public static class StringExtensions } var total = 0; - TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator (str); - while (enumerator.MoveNext ()) + foreach (string grapheme in GraphemeHelper.GetGraphemes (str)) { - string element = enumerator.GetTextElement (); - // Get the maximum rune width within this grapheme cluster - int width = element - .EnumerateRunes () - .Max (r => Math.Max (r.GetColumns (), 0)); - total += width; + int clusterWidth = grapheme.EnumerateRunes () + .Sum (r => + { + int w = r.GetColumns (); + + return ignoreLessThanZero && w < 0 ? 0 : w; + }); + + // Clamp to realistic max display width + if (clusterWidth > 2) + { + clusterWidth = 2; + } + + total += clusterWidth; } return total; @@ -94,7 +102,7 @@ public static class StringExtensions /// A indicating if all elements of the are ASCII digits ( /// ) or not ( /// - public static bool IsAllAsciiDigits (this ReadOnlySpan stringSpan) { return stringSpan.ToString ().All (char.IsAsciiDigit); } + public static bool IsAllAsciiDigits (this ReadOnlySpan stringSpan) { return !stringSpan.IsEmpty && stringSpan.ToString ().All (char.IsAsciiDigit); } /// /// Determines if this of is composed entirely of ASCII @@ -105,7 +113,7 @@ public static class StringExtensions /// A indicating if all elements of the are ASCII digits ( /// ) or not ( /// - public static bool IsAllAsciiHexDigits (this ReadOnlySpan stringSpan) { return stringSpan.ToString ().All (char.IsAsciiHexDigit); } + public static bool IsAllAsciiHexDigits (this ReadOnlySpan stringSpan) { return !stringSpan.IsEmpty && stringSpan.ToString ().All (char.IsAsciiHexDigit); } /// Repeats the string times. /// This is a Terminal.Gui extension method to to support TUI text manipulation. @@ -206,4 +214,60 @@ public static class StringExtensions return encoding.GetString (bytes.ToArray ()); } + + /// Converts a generic collection into a string. + /// The enumerable string to convert. + /// + public static string ToString (IEnumerable strings) { return string.Concat (strings); } + + /// Converts the string into a . + /// This is a Terminal.Gui extension method to to support TUI text manipulation. + /// The string to convert. + /// + public static List ToStringList (this string str) + { + List strings = []; + + foreach (string grapheme in GraphemeHelper.GetGraphemes (str)) + { + strings.Add (grapheme); + } + + return strings; + } + + /// Reports whether a string is a surrogate code point. + /// This is a Terminal.Gui extension method to to support TUI text manipulation. + /// The string to probe. + /// if the string is a surrogate code point; otherwise. + public static bool IsSurrogatePair (this string str) + { + if (str.Length != 2) + { + return false; + } + + Rune rune = Rune.GetRuneAt (str, 0); + + return rune.IsSurrogatePair (); + } + + /// + /// Ensures the text is not a control character and can be displayed by translating characters below 0x20 to + /// equivalent, printable, Unicode chars. + /// + /// This is a Terminal.Gui extension method to to support TUI text manipulation. + /// The text. + /// + public static string MakePrintable (this string str) + { + if (str.Length > 1) + { + return str; + } + + char ch = str [0]; + + return char.IsControl (ch) ? new ((char)(ch + 0x2400), 1) : str; + } } diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs index a11289a2b..ecd1be459 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -122,9 +122,10 @@ public class TextFormatter break; } - Rune [] runes = linesFormatted [line].ToRunes (); + string strings = linesFormatted [line]; + string[] graphemes = GraphemeHelper.GetGraphemes (strings).ToArray (); - // When text is justified, we lost left or right, so we use the direction to align. + // When text is justified, we lost left or right, so we use the direction to align. int x = 0, y = 0; @@ -139,7 +140,7 @@ public class TextFormatter } else { - int runesWidth = StringExtensions.ToString (runes).GetColumns (); + int runesWidth = strings.GetColumns (); x = screen.Right - runesWidth; CursorPosition = screen.Width - runesWidth + (_hotKeyPos > -1 ? _hotKeyPos : 0); } @@ -195,7 +196,7 @@ public class TextFormatter } else { - int runesWidth = StringExtensions.ToString (runes).GetColumns (); + int runesWidth = strings.GetColumns (); x = screen.Left + (screen.Width - runesWidth) / 2; CursorPosition = (screen.Width - runesWidth) / 2 + (_hotKeyPos > -1 ? _hotKeyPos : 0); @@ -213,7 +214,7 @@ public class TextFormatter { if (isVertical) { - y = screen.Bottom - runes.Length; + y = screen.Bottom - graphemes.Length; } else { @@ -249,7 +250,7 @@ public class TextFormatter { if (isVertical) { - int s = (screen.Height - runes.Length) / 2; + int s = (screen.Height - graphemes.Length) / 2; y = screen.Top + s; } else @@ -270,14 +271,14 @@ public class TextFormatter int size = isVertical ? screen.Height : screen.Width; int current = start + colOffset; List lastZeroWidthPos = null!; - Rune rune = default; - int zeroLengthCount = isVertical ? runes.Sum (r => r.GetColumns () == 0 ? 1 : 0) : 0; + string text = default; + int zeroLengthCount = isVertical ? strings.EnumerateRunes ().Sum (r => r.GetColumns () == 0 ? 1 : 0) : 0; for (int idx = (isVertical ? start - y : start - x) + colOffset; current < start + size + zeroLengthCount; idx++) { - Rune lastRuneUsed = rune; + string lastTextUsed = text; if (lastZeroWidthPos is null) { @@ -291,17 +292,17 @@ public class TextFormatter continue; } - if (!FillRemaining && idx > runes.Length - 1) + if (!FillRemaining && idx > graphemes.Length - 1) { break; } if ((!isVertical && (current - start > maxScreen.Left + maxScreen.Width - screen.X + colOffset - || (idx < runes.Length && runes [idx].GetColumns () > screen.Width))) + || (idx < graphemes.Length && graphemes [idx].GetColumns () > screen.Width))) || (isVertical && ((current > start + size + zeroLengthCount && idx > maxScreen.Top + maxScreen.Height - screen.Y) - || (idx < runes.Length && runes [idx].GetColumns () > screen.Width)))) + || (idx < graphemes.Length && graphemes [idx].GetColumns () > screen.Width)))) { break; } @@ -312,13 +313,13 @@ public class TextFormatter // break; - rune = (Rune)' '; + text = " "; if (isVertical) { - if (idx >= 0 && idx < runes.Length) + if (idx >= 0 && idx < graphemes.Length) { - rune = runes [idx]; + text = graphemes [idx]; } if (lastZeroWidthPos is null) @@ -334,7 +335,7 @@ public class TextFormatter if (foundIdx > -1) { - if (rune.IsCombiningMark ()) + if (Rune.GetRuneAt (text, 0).IsCombiningMark ()) { lastZeroWidthPos [foundIdx] = new Point ( @@ -347,7 +348,7 @@ public class TextFormatter current ); } - else if (!rune.IsCombiningMark () && lastRuneUsed.IsCombiningMark ()) + else if (!Rune.GetRuneAt (text, 0).IsCombiningMark () && Rune.GetRuneAt (lastTextUsed, 0).IsCombiningMark ()) { current++; driver?.Move (x, current); @@ -367,13 +368,13 @@ public class TextFormatter { driver?.Move (current, y); - if (idx >= 0 && idx < runes.Length) + if (idx >= 0 && idx < graphemes.Length) { - rune = runes [idx]; + text = graphemes [idx]; } } - int runeWidth = GetRuneWidth (rune, TabWidth); + int runeWidth = GetTextWidth (text, TabWidth); if (HotKeyPos > -1 && idx == HotKeyPos) { @@ -383,7 +384,7 @@ public class TextFormatter } driver?.SetAttribute (hotColor); - driver?.AddRune (rune); + driver?.AddStr (text); driver?.SetAttribute (normalColor); } else @@ -412,7 +413,7 @@ public class TextFormatter } } - driver?.AddRune (rune); + driver?.AddStr (text); } if (isVertical) @@ -427,11 +428,11 @@ public class TextFormatter current += runeWidth; } - int nextRuneWidth = idx + 1 > -1 && idx + 1 < runes.Length - ? runes [idx + 1].GetColumns () + int nextRuneWidth = idx + 1 > -1 && idx + 1 < graphemes.Length + ? graphemes [idx + 1].GetColumns () : 0; - if (!isVertical && idx + 1 < runes.Length && current + nextRuneWidth > start + size) + if (!isVertical && idx + 1 < graphemes.Length && current + nextRuneWidth > start + size) { break; } @@ -929,9 +930,10 @@ public class TextFormatter break; } - Rune [] runes = linesFormatted [line].ToRunes (); + string strings = linesFormatted [line]; + string [] graphemes = GraphemeHelper.GetGraphemes (strings).ToArray (); - // When text is justified, we lost left or right, so we use the direction to align. + // When text is justified, we lost left or right, so we use the direction to align. int x = 0, y = 0; switch (Alignment) @@ -946,17 +948,17 @@ public class TextFormatter } case Alignment.End: { - int runesWidth = StringExtensions.ToString (runes).GetColumns (); - x = screen.Right - runesWidth; + int stringsWidth = strings.GetColumns (); + x = screen.Right - stringsWidth; break; } case Alignment.Start when isVertical: { - int runesWidth = line > 0 - ? GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth) - : 0; - x = screen.Left + runesWidth; + int stringsWidth = line > 0 + ? GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth) + : 0; + x = screen.Left + stringsWidth; break; } @@ -966,7 +968,7 @@ public class TextFormatter break; case Alignment.Fill when isVertical: { - int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth); + int stringsWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth); int prevLineWidth = line > 0 ? GetColumnsRequiredForVerticalText (linesFormatted, line - 1, 1, TabWidth) : 0; int firstLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, 1, TabWidth); int lastLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, linesFormatted.Count - 1, 1, TabWidth); @@ -975,7 +977,7 @@ public class TextFormatter x = line == 0 ? screen.Left : line < linesFormatted.Count - 1 - ? screen.Width - runesWidth <= lastLineWidth ? screen.Left + prevLineWidth : screen.Left + line * interval + ? screen.Width - stringsWidth <= lastLineWidth ? screen.Left + prevLineWidth : screen.Left + line * interval : screen.Right - lastLineWidth; break; @@ -986,16 +988,16 @@ public class TextFormatter break; case Alignment.Center when isVertical: { - int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth); + int stringsWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth); int linesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth); - x = screen.Left + linesWidth + (screen.Width - runesWidth) / 2; + x = screen.Left + linesWidth + (screen.Width - stringsWidth) / 2; break; } case Alignment.Center: { - int runesWidth = StringExtensions.ToString (runes).GetColumns (); - x = screen.Left + (screen.Width - runesWidth) / 2; + int stringsWidth = strings.GetColumns (); + x = screen.Left + (screen.Width - stringsWidth) / 2; break; } @@ -1009,7 +1011,7 @@ public class TextFormatter { // Vertical Alignment case Alignment.End when isVertical: - y = screen.Bottom - runes.Length; + y = screen.Bottom - graphemes.Length; break; case Alignment.End: @@ -1039,7 +1041,7 @@ public class TextFormatter } case Alignment.Center when isVertical: { - int s = (screen.Height - runes.Length) / 2; + int s = (screen.Height - graphemes.Length) / 2; y = screen.Top + s; break; @@ -1061,7 +1063,7 @@ public class TextFormatter int start = isVertical ? screen.Top : screen.Left; int size = isVertical ? screen.Height : screen.Width; int current = start + colOffset; - int zeroLengthCount = isVertical ? runes.Sum (r => r.GetColumns () == 0 ? 1 : 0) : 0; + int zeroLengthCount = isVertical ? strings.EnumerateRunes ().Sum (r => r.GetColumns () == 0 ? 1 : 0) : 0; int lineX = x, lineY = y, lineWidth = 0, lineHeight = 1; @@ -1079,23 +1081,23 @@ public class TextFormatter continue; } - if (!FillRemaining && idx > runes.Length - 1) + if (!FillRemaining && idx > graphemes.Length - 1) { break; } if ((!isVertical && (current - start > maxScreen.Left + maxScreen.Width - screen.X + colOffset - || (idx < runes.Length && runes [idx].GetColumns () > screen.Width))) + || (idx < graphemes.Length && graphemes [idx].GetColumns () > screen.Width))) || (isVertical && ((current > start + size + zeroLengthCount && idx > maxScreen.Top + maxScreen.Height - screen.Y) - || (idx < runes.Length && runes [idx].GetColumns () > screen.Width)))) + || (idx < graphemes.Length && graphemes [idx].GetColumns () > screen.Width)))) { break; } - Rune rune = idx >= 0 && idx < runes.Length ? runes [idx] : (Rune)' '; - int runeWidth = GetRuneWidth (rune, TabWidth); + string text = idx >= 0 && idx < graphemes.Length ? graphemes [idx] : " "; + int runeWidth = GetStringWidth (text, TabWidth); if (isVertical) { @@ -1114,11 +1116,11 @@ public class TextFormatter current += isVertical && runeWidth > 0 ? 1 : runeWidth; - int nextRuneWidth = idx + 1 > -1 && idx + 1 < runes.Length - ? runes [idx + 1].GetColumns () + int nextStringWidth = idx + 1 > -1 && idx + 1 < graphemes.Length + ? graphemes [idx + 1].GetColumns () : 0; - if (!isVertical && idx + 1 < runes.Length && current + nextRuneWidth > start + size) + if (!isVertical && idx + 1 < graphemes.Length && current + nextStringWidth > start + size) { break; } @@ -1331,33 +1333,34 @@ public class TextFormatter /// A list of text without the newline characters. public static List SplitNewLine (string text) { - List runes = text.ToRuneList (); + List graphemes = GraphemeHelper.GetGraphemes (text).ToList (); List lines = new (); var start = 0; - for (var i = 0; i < runes.Count; i++) + for (var i = 0; i < graphemes.Count; i++) { int end = i; - switch (runes [i].Value) + switch (graphemes [i]) { - case '\n': - lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start))); + case "\n": + case "\r\n": + lines.Add (StringExtensions.ToString (graphemes.GetRange (start, end - start))); i++; start = i; break; - case '\r': - if (i + 1 < runes.Count && runes [i + 1].Value == '\n') + case "\r": + if (i + 1 < graphemes.Count && graphemes [i + 1] == "\n") { - lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start))); + lines.Add (StringExtensions.ToString (graphemes.GetRange (start, end - start))); i += 2; start = i; } else { - lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start))); + lines.Add (StringExtensions.ToString (graphemes.GetRange (start, end - start))); i++; start = i; } @@ -1366,14 +1369,14 @@ public class TextFormatter } } - switch (runes.Count) + switch (graphemes.Count) { case > 0 when lines.Count == 0: - lines.Add (StringExtensions.ToString (runes)); + lines.Add (StringExtensions.ToString (graphemes)); break; - case > 0 when start < runes.Count: - lines.Add (StringExtensions.ToString (runes.GetRange (start, runes.Count - start))); + case > 0 when start < graphemes.Count: + lines.Add (StringExtensions.ToString (graphemes.GetRange (start, graphemes.Count - start))); break; default: @@ -1401,16 +1404,19 @@ public class TextFormatter } // if value is not wide enough - if (text.EnumerateRunes ().Sum (c => c.GetColumns ()) < width) + string [] graphemes = GraphemeHelper.GetGraphemes (text).ToArray (); + int totalColumns = graphemes.Sum (s => s.GetColumns ()); + + if (totalColumns < width) { // pad it out with spaces to the given Alignment - int toPad = width - text.EnumerateRunes ().Sum (c => c.GetColumns ()); + int toPad = width - totalColumns; return text + new string (' ', toPad); } // value is too wide - return new (text.TakeWhile (c => (width -= ((Rune)c).GetColumns ()) >= 0).ToArray ()); + return string.Concat (graphemes.TakeWhile (t => (width -= t.GetColumns ()) >= 0)); } /// Formats the provided text to fit within the width provided using word wrapping. @@ -1451,18 +1457,18 @@ public class TextFormatter return lines; } - List runes = StripCRLF (text).ToRuneList (); + List graphemes = GraphemeHelper.GetGraphemes (StripCRLF (text)).ToList (); int start = Math.Max ( - !runes.Contains ((Rune)' ') && textFormatter is { VerticalAlignment: Alignment.End } && IsVerticalDirection (textDirection) - ? runes.Count - width + !graphemes.Contains (" ") && textFormatter is { VerticalAlignment: Alignment.End } && IsVerticalDirection (textDirection) + ? graphemes.Count - width : 0, 0); int end; if (preserveTrailingSpaces) { - while ((end = start) < runes.Count) + while (start < graphemes.Count) { end = GetNextWhiteSpace (start, width, out bool incomplete); @@ -1473,7 +1479,7 @@ public class TextFormatter break; } - lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start))); + lines.Add (StringExtensions.ToString (graphemes.GetRange (start, end - start))); start = end; if (incomplete) @@ -1490,14 +1496,14 @@ public class TextFormatter { while ((end = start + GetLengthThatFits ( - runes.GetRange (start, runes.Count - start), + string.Concat (graphemes.GetRange (start, graphemes.Count - start)), width, tabWidth, textDirection )) - < runes.Count) + < graphemes.Count) { - while (runes [end].Value != ' ' && end > start) + while (graphemes [end] != " " && end > start) { end--; } @@ -1506,22 +1512,22 @@ public class TextFormatter { end = start + GetLengthThatFits ( - runes.GetRange (end, runes.Count - end), + string.Concat (graphemes.GetRange (end, graphemes.Count - end)), width, tabWidth, textDirection ); } - var str = StringExtensions.ToString (runes.GetRange (start, end - start)); + var str = StringExtensions.ToString (graphemes.GetRange (start, end - start)); int zeroLength = text.EnumerateRunes ().Sum (r => r.GetColumns () == 0 ? 1 : 0); - if (end > start && GetRuneWidth (str, tabWidth, textDirection) <= width + zeroLength) + if (end > start && GetTextWidth (str, tabWidth, textDirection) <= width + zeroLength) { lines.Add (str); start = end; - if (runes [end].Value == ' ') + if (graphemes [end] == " ") { start++; } @@ -1535,9 +1541,9 @@ public class TextFormatter } else { - while ((end = start + width) < runes.Count) + while ((end = start + width) < graphemes.Count) { - while (runes [end].Value != ' ' && end > start) + while (graphemes [end] != " " && end > start) { end--; } @@ -1549,11 +1555,11 @@ public class TextFormatter var zeroLength = 0; - for (int i = end; i < runes.Count - start; i++) + for (int i = end; i < graphemes.Count - start; i++) { - Rune r = runes [i]; + string s = graphemes [i]; - if (r.GetColumns () == 0) + if (s.GetColumns () == 0) { zeroLength++; } @@ -1565,7 +1571,7 @@ public class TextFormatter lines.Add ( StringExtensions.ToString ( - runes.GetRange ( + graphemes.GetRange ( start, end - start + zeroLength ) @@ -1574,7 +1580,7 @@ public class TextFormatter end += zeroLength; start = end; - if (runes [end].Value == ' ') + if (graphemes [end] == " ") { start++; } @@ -1588,13 +1594,13 @@ public class TextFormatter int length = cLength; incomplete = false; - while (length < cWidth && to < runes.Count) + while (length < cWidth && to < graphemes.Count) { - Rune rune = runes [to]; + string grapheme = graphemes [to]; if (IsHorizontalDirection (textDirection)) { - length += rune.GetColumns (); + length += grapheme.GetColumns (false); } else { @@ -1603,7 +1609,7 @@ public class TextFormatter if (length > cWidth) { - if (to >= runes.Count || (length > 1 && cWidth <= 1)) + if (to >= graphemes.Count || (length > 1 && cWidth <= 1)) { incomplete = true; } @@ -1611,15 +1617,15 @@ public class TextFormatter return to; } - switch (rune.Value) + switch (grapheme) { - case ' ' when length == cWidth: + case " " when length == cWidth: return to + 1; - case ' ' when length > cWidth: + case " " when length > cWidth: return to; - case ' ': + case " ": return GetNextWhiteSpace (to + 1, cWidth, out incomplete, length); - case '\t': + case "\t": { length += tabWidth + 1; @@ -1644,8 +1650,8 @@ public class TextFormatter return cLength switch { - > 0 when to < runes.Count && runes [to].Value != ' ' && runes [to].Value != '\t' => from, - > 0 when to < runes.Count && (runes [to].Value == ' ' || runes [to].Value == '\t') => from, + > 0 when to < graphemes.Count && graphemes [to] != " " && graphemes [to] != "\t" => from, + > 0 when to < graphemes.Count && (graphemes [to] == " " || graphemes [to] == "\t") => from, _ => to }; } @@ -1653,7 +1659,7 @@ public class TextFormatter if (start < text.GetRuneCount ()) { string str = ReplaceTABWithSpaces ( - StringExtensions.ToString (runes.GetRange (start, runes.Count - start)), + StringExtensions.ToString (graphemes.GetRange (start, graphemes.Count - start)), tabWidth ); @@ -1717,42 +1723,42 @@ public class TextFormatter } text = ReplaceTABWithSpaces (text, tabWidth); - List runes = text.ToRuneList (); - int zeroLength = runes.Sum (r => r.GetColumns () == 0 ? 1 : 0); + List graphemes = GraphemeHelper.GetGraphemes (text).ToList (); + int zeroLength = graphemes.Sum (s => s.EnumerateRunes ().Sum (r => r.GetColumns() == 0 ? 1 : 0)); - if (runes.Count - zeroLength > width) + if (graphemes.Count - zeroLength > width) { if (IsHorizontalDirection (textDirection)) { if (textFormatter is { Alignment: Alignment.End }) { - return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection); + return GetRangeThatFits (graphemes, graphemes.Count - width, text, width, tabWidth, textDirection); } if (textFormatter is { Alignment: Alignment.Center }) { - return GetRangeThatFits (runes, Math.Max ((runes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection); + return GetRangeThatFits (graphemes, Math.Max ((graphemes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection); } - return GetRangeThatFits (runes, 0, text, width, tabWidth, textDirection); + return GetRangeThatFits (graphemes, 0, text, width, tabWidth, textDirection); } if (IsVerticalDirection (textDirection)) { if (textFormatter is { VerticalAlignment: Alignment.End }) { - return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection); + return GetRangeThatFits (graphemes, graphemes.Count - width, text, width, tabWidth, textDirection); } if (textFormatter is { VerticalAlignment: Alignment.Center }) { - return GetRangeThatFits (runes, Math.Max ((runes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection); + return GetRangeThatFits (graphemes, Math.Max ((graphemes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection); } - return GetRangeThatFits (runes, 0, text, width, tabWidth, textDirection); + return GetRangeThatFits (graphemes, 0, text, width, tabWidth, textDirection); } - return StringExtensions.ToString (runes.GetRange (0, width + zeroLength)); + return StringExtensions.ToString (graphemes.GetRange (0, width + zeroLength)); } if (justify) @@ -1764,18 +1770,18 @@ public class TextFormatter { if (textFormatter is { Alignment: Alignment.End }) { - if (GetRuneWidth (text, tabWidth, textDirection) > width) + if (GetTextWidth (text, tabWidth, textDirection) > width) { - return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection); + return GetRangeThatFits (graphemes, graphemes.Count - width, text, width, tabWidth, textDirection); } } else if (textFormatter is { Alignment: Alignment.Center }) { - return GetRangeThatFits (runes, Math.Max ((runes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection); + return GetRangeThatFits (graphemes, Math.Max ((graphemes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection); } - else if (GetRuneWidth (text, tabWidth, textDirection) > width) + else if (GetTextWidth (text, tabWidth, textDirection) > width) { - return GetRangeThatFits (runes, 0, text, width, tabWidth, textDirection); + return GetRangeThatFits (graphemes, 0, text, width, tabWidth, textDirection); } } @@ -1783,28 +1789,28 @@ public class TextFormatter { if (textFormatter is { VerticalAlignment: Alignment.End }) { - if (runes.Count - zeroLength > width) + if (graphemes.Count - zeroLength > width) { - return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection); + return GetRangeThatFits (graphemes, graphemes.Count - width, text, width, tabWidth, textDirection); } } else if (textFormatter is { VerticalAlignment: Alignment.Center }) { - return GetRangeThatFits (runes, Math.Max ((runes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection); + return GetRangeThatFits (graphemes, Math.Max ((graphemes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection); } - else if (runes.Count - zeroLength > width) + else if (graphemes.Count - zeroLength > width) { - return GetRangeThatFits (runes, 0, text, width, tabWidth, textDirection); + return GetRangeThatFits (graphemes, 0, text, width, tabWidth, textDirection); } } return text; } - private static string GetRangeThatFits (List runes, int index, string text, int width, int tabWidth, TextDirection textDirection) + private static string GetRangeThatFits (List strings, int index, string text, int width, int tabWidth, TextDirection textDirection) { return StringExtensions.ToString ( - runes.GetRange ( + strings.GetRange ( Math.Max (index, 0), GetLengthThatFits (text, width, tabWidth, textDirection) ) @@ -1842,7 +1848,7 @@ public class TextFormatter if (IsHorizontalDirection (textDirection)) { - textCount = words.Sum (arg => GetRuneWidth (arg, tabWidth, textDirection)); + textCount = words.Sum (arg => GetTextWidth (arg, tabWidth, textDirection)); } else { @@ -2137,11 +2143,11 @@ public class TextFormatter i < (linesCount == -1 ? lines.Count : startLine + linesCount); i++) { - string runes = lines [i]; + string strings = lines [i]; - if (runes.Length > 0) + if (strings.Length > 0) { - max += runes.EnumerateRunes ().Max (r => GetRuneWidth (r, tabWidth)); + max += strings.EnumerateRunes ().Max (r => GetRuneWidth (r, tabWidth)); } } @@ -2163,7 +2169,7 @@ public class TextFormatter { List result = SplitNewLine (text); - return result.Max (x => GetRuneWidth (x, tabWidth)); + return result.Max (x => GetTextWidth (x, tabWidth)); } /// @@ -2182,13 +2188,13 @@ public class TextFormatter public static int GetSumMaxCharWidth (string text, int startIndex = -1, int length = -1, int tabWidth = 0) { var max = 0; - Rune [] runes = text.ToRunes (); + string [] graphemes = GraphemeHelper.GetGraphemes (text).ToArray (); for (int i = startIndex == -1 ? 0 : startIndex; - i < (length == -1 ? runes.Length : startIndex + length); + i < (length == -1 ? graphemes.Length : startIndex + length); i++) { - max += GetRuneWidth (runes [i], tabWidth); + max += GetStringWidth (graphemes [i], tabWidth); } return max; @@ -2206,51 +2212,38 @@ public class TextFormatter /// The index of the text that fit the width. public static int GetLengthThatFits (string text, int width, int tabWidth = 0, TextDirection textDirection = TextDirection.LeftRight_TopBottom) { - return GetLengthThatFits (text?.ToRuneList () ?? [], width, tabWidth, textDirection); - } - - /// Gets the number of the Runes in a list of Runes that will fit in . - /// - /// This API will return incorrect results if the text includes glyphs whose width is dependent on surrounding - /// glyphs (e.g. Arabic). - /// - /// The list of runes. - /// The width. - /// The width used for a tab. - /// The text direction. - /// The index of the last Rune in that fit in . - public static int GetLengthThatFits (List runes, int width, int tabWidth = 0, TextDirection textDirection = TextDirection.LeftRight_TopBottom) - { - if (runes is null || runes.Count == 0) + if (string.IsNullOrEmpty (text)) { return 0; } - var runesLength = 0; - var runeIdx = 0; + var textLength = 0; + var stringIdx = 0; - for (; runeIdx < runes.Count; runeIdx++) + foreach (string grapheme in GraphemeHelper.GetGraphemes (text)) { - int runeWidth = GetRuneWidth (runes [runeIdx], tabWidth, textDirection); + int textWidth = GetStringWidth (grapheme, tabWidth, textDirection); - if (runesLength + runeWidth > width) + if (textLength + textWidth > width) { break; } - runesLength += runeWidth; + textLength += textWidth; + stringIdx++; } - return runeIdx; + return stringIdx; } - private static int GetRuneWidth (string str, int tabWidth, TextDirection textDirection = TextDirection.LeftRight_TopBottom) + private static int GetTextWidth (string str, int tabWidth, TextDirection textDirection = TextDirection.LeftRight_TopBottom) { int runesWidth = 0; - foreach (Rune rune in str.EnumerateRunes ()) + foreach (string grapheme in GraphemeHelper.GetGraphemes (str)) { - runesWidth += GetRuneWidth (rune, tabWidth, textDirection); + runesWidth += GetStringWidth (grapheme, tabWidth, textDirection); } + return runesWidth; } @@ -2271,6 +2264,23 @@ public class TextFormatter return runeWidth; } + private static int GetStringWidth (string str, int tabWidth, TextDirection textDirection = TextDirection.LeftRight_TopBottom) + { + int textWidth = IsHorizontalDirection (textDirection) ? str.GetColumns (false) : str.GetColumns () == 0 ? 0 : 1; + + if (str == "\t") + { + return tabWidth; + } + + if (textWidth is < 0 or > 0) + { + return Math.Max (textWidth, 1); + } + + return textWidth; + } + /// Gets the index position from the list based on the . /// /// This API will return incorrect results if the text includes glyphs whose width is dependent on surrounding @@ -2282,23 +2292,23 @@ public class TextFormatter /// The index of the list that fit the width. public static int GetMaxColsForWidth (List lines, int width, int tabWidth = 0) { - var runesLength = 0; + var textLength = 0; var lineIdx = 0; for (; lineIdx < lines.Count; lineIdx++) { - List runes = lines [lineIdx].ToRuneList (); + string [] graphemes = GraphemeHelper.GetGraphemes (lines [lineIdx]).ToArray (); - int maxRruneWidth = runes.Count > 0 - ? runes.Max (r => GetRuneWidth (r, tabWidth)) + int maxTextWidth = graphemes.Length > 0 + ? graphemes.Max (r => GetStringWidth (r, tabWidth)) : 1; - if (runesLength + maxRruneWidth > width) + if (textLength + maxTextWidth > width) { break; } - runesLength += maxRruneWidth; + textLength += maxTextWidth; } return lineIdx; diff --git a/Terminal.Gui/ViewBase/Adornment/ShadowView.cs b/Terminal.Gui/ViewBase/Adornment/ShadowView.cs index 9212e7271..cbb484fb7 100644 --- a/Terminal.Gui/ViewBase/Adornment/ShadowView.cs +++ b/Terminal.Gui/ViewBase/Adornment/ShadowView.cs @@ -100,7 +100,7 @@ internal class ShadowView : View if (c < ScreenContents?.GetLength (1) && r < ScreenContents?.GetLength (0)) { - AddRune (ScreenContents [r, c].Rune); + AddStr (ScreenContents [r, c].Grapheme); } } } @@ -134,7 +134,7 @@ internal class ShadowView : View if (ScreenContents is { } && screen.X < ScreenContents.GetLength (1) && r < ScreenContents.GetLength (0)) { - AddRune (ScreenContents [r, c].Rune); + AddStr (ScreenContents [r, c].Grapheme); } } } @@ -142,7 +142,7 @@ internal class ShadowView : View private Attribute GetAttributeUnderLocation (Point location) { - if (SuperView is not Adornment adornment + if (SuperView is not Adornment || location.X < 0 || location.X >= App?.Screen.Width || location.Y < 0 @@ -171,7 +171,7 @@ internal class ShadowView : View if (newAttribute.Background == Color.DarkGray) { List currentViewsUnderMouse = GetViewsUnderLocation (location, ViewportSettingsFlags.Transparent); - View? underView = currentViewsUnderMouse!.LastOrDefault (); + View? underView = currentViewsUnderMouse.LastOrDefault (); attr = underView?.GetAttributeForRole (VisualRole.Normal) ?? Attribute.Default; newAttribute = new ( diff --git a/Terminal.Gui/ViewBase/View.Drawing.Primitives.cs b/Terminal.Gui/ViewBase/View.Drawing.Primitives.cs index 6b169718b..c5971598c 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.Primitives.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.Primitives.cs @@ -32,7 +32,6 @@ public partial class View Driver?.AddRune (rune); } - /// /// Adds the specified to the display at the current cursor position. This method is a /// convenience method that calls with the constructor. @@ -72,6 +71,25 @@ public partial class View { Driver?.AddStr (str); } + + /// Draws the specified in the specified viewport-relative column and row of the View. + /// + /// If the provided coordinates are outside the visible content area, this method does nothing. + /// + /// + /// The top-left corner of the visible content area is ViewPort.Location. + /// + /// Column (viewport-relative). + /// Row (viewport-relative). + /// The Text. + public void AddStr (int col, int row, string str) + { + if (Move (col, row)) + { + Driver?.AddStr (str); + } + } + /// Utility function to draw strings that contain a hotkey. /// String to display, the hotkey specifier before a letter flags the next letter as the hotkey. /// Hot color. diff --git a/Terminal.Gui/ViewBase/View.Drawing.cs b/Terminal.Gui/ViewBase/View.Drawing.cs index ed300ee5d..70e915863 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.cs @@ -194,11 +194,11 @@ public partial class View // Drawing APIs else { // Set the clip to be just the thicknesses of the adornments - // TODO: Put this union logic in a method on View? + // TODO: Put this union logic in a method on View? Region? clipAdornments = Margin!.Thickness.AsRegion (Margin!.FrameToScreen ()); - clipAdornments?.Combine (Border!.Thickness.AsRegion (Border!.FrameToScreen ()), RegionOp.Union); - clipAdornments?.Combine (Padding!.Thickness.AsRegion (Padding!.FrameToScreen ()), RegionOp.Union); - clipAdornments?.Combine (originalClip, RegionOp.Intersect); + clipAdornments.Combine (Border!.Thickness.AsRegion (Border!.FrameToScreen ()), RegionOp.Union); + clipAdornments.Combine (Padding!.Thickness.AsRegion (Padding!.FrameToScreen ()), RegionOp.Union); + clipAdornments.Combine (originalClip, RegionOp.Intersect); SetClip (clipAdornments); } @@ -239,7 +239,7 @@ public partial class View // Drawing APIs { // We do not attempt to draw Margin. It is drawn in a separate pass. - // Each of these renders lines to this View's LineCanvas + // Each of these renders lines to this View's LineCanvas // Those lines will be finally rendered in OnRenderLineCanvas if (Border is { } && Border.Thickness != Thickness.Empty) { @@ -660,7 +660,7 @@ public partial class View // Drawing APIs Driver.Move (p.Key.X, p.Key.Y); // TODO: #2616 - Support combining sequences that don't normalize - AddRune (p.Value.Value.Rune); + AddStr (p.Value.Value.Grapheme); } } @@ -687,7 +687,7 @@ public partial class View // Drawing APIs context!.ClipDrawnRegion (ViewportToScreen (Viewport)); // Exclude the drawn region from the clip - ExcludeFromClip (context!.GetDrawnRegion ()); + ExcludeFromClip (context.GetDrawnRegion ()); // Exclude the Border and Padding from the clip ExcludeFromClip (Border?.Thickness.AsRegion (Border.FrameToScreen ())); diff --git a/Terminal.Gui/Views/Autocomplete/AutocompleteFilepathContext.cs b/Terminal.Gui/Views/Autocomplete/AutocompleteFilepathContext.cs index 80481a787..12384e45b 100644 --- a/Terminal.Gui/Views/Autocomplete/AutocompleteFilepathContext.cs +++ b/Terminal.Gui/Views/Autocomplete/AutocompleteFilepathContext.cs @@ -12,16 +12,16 @@ internal class AutocompleteFilepathContext (string currentLine, int cursorPositi internal class FilepathSuggestionGenerator : ISuggestionGenerator { - private FileDialogState state; + private FileDialogState _state; public IEnumerable GenerateSuggestions (AutocompleteContext context) { if (context is AutocompleteFilepathContext fileState) { - state = fileState.State; + _state = fileState.State; } - if (state is null) + if (_state is null) { return Enumerable.Empty (); } @@ -42,7 +42,7 @@ internal class FilepathSuggestionGenerator : ISuggestionGenerator return Enumerable.Empty (); } - if (term.Equals (state?.Directory?.Name)) + if (term.Equals (_state?.Directory?.Name)) { // Clear suggestions return Enumerable.Empty (); @@ -50,13 +50,13 @@ internal class FilepathSuggestionGenerator : ISuggestionGenerator bool isWindows = RuntimeInformation.IsOSPlatform (OSPlatform.Windows); - string [] suggestions = state.Children.Where (d => !d.IsParent) - .Select ( - e => e.FileSystemInfo is IDirectoryInfo d - ? d.Name + Path.DirectorySeparatorChar - : e.FileSystemInfo.Name - ) - .ToArray (); + string [] suggestions = _state!.Children.Where (d => !d.IsParent) + .Select ( + e => e.FileSystemInfo is IDirectoryInfo d + ? d.Name + Path.DirectorySeparatorChar + : e.FileSystemInfo.Name + ) + .ToArray (); string [] validSuggestions = suggestions .Where ( @@ -82,9 +82,9 @@ internal class FilepathSuggestionGenerator : ISuggestionGenerator .ToList (); } - public bool IsWordChar (Rune rune) + public bool IsWordChar (string text) { - if (rune.Value == '\n') + if (text == "\n") { return false; } diff --git a/Terminal.Gui/Views/Autocomplete/ISuggestionGenerator.cs b/Terminal.Gui/Views/Autocomplete/ISuggestionGenerator.cs index e8eedd2ee..79f62045e 100644 --- a/Terminal.Gui/Views/Autocomplete/ISuggestionGenerator.cs +++ b/Terminal.Gui/Views/Autocomplete/ISuggestionGenerator.cs @@ -8,9 +8,9 @@ public interface ISuggestionGenerator IEnumerable GenerateSuggestions (AutocompleteContext context); /// - /// Returns if is a character that would continue autocomplete + /// Returns if is a character that would continue autocomplete /// suggesting. Returns if it is a 'breaking' character (i.e. terminating current word /// boundary) /// - bool IsWordChar (Rune rune); + bool IsWordChar (string text); } diff --git a/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs b/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs index 87f39fc2b..8a7ca9d3e 100644 --- a/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs +++ b/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs @@ -188,7 +188,7 @@ public abstract partial class PopupAutocomplete : AutocompleteBase /// trueif the key can be handled falseotherwise. public override bool ProcessKey (Key key) { - if (SuggestionGenerator.IsWordChar ((Rune)key)) + if (SuggestionGenerator.IsWordChar (key.AsRune.ToString ())) { Visible = true; _closed = false; diff --git a/Terminal.Gui/Views/Autocomplete/SingleWordSuggestionGenerator.cs b/Terminal.Gui/Views/Autocomplete/SingleWordSuggestionGenerator.cs index 59bde624b..fc9504027 100644 --- a/Terminal.Gui/Views/Autocomplete/SingleWordSuggestionGenerator.cs +++ b/Terminal.Gui/Views/Autocomplete/SingleWordSuggestionGenerator.cs @@ -18,10 +18,10 @@ public class SingleWordSuggestionGenerator : ISuggestionGenerator // if there is nothing to pick from if (AllSuggestions.Count == 0) { - return Enumerable.Empty (); + return []; } - List line = context.CurrentLine.Select (c => c.Rune).ToList (); + List line = context.CurrentLine.Select (c => c.Grapheme).ToList (); string currentWord = IdxToWord (line, context.CursorPosition, out int startIdx); context.CursorPosition = startIdx < 1 ? startIdx : Math.Min (startIdx + 1, line.Count); @@ -44,9 +44,13 @@ public class SingleWordSuggestionGenerator : ISuggestionGenerator /// Return true if the given symbol should be considered part of a word and can be contained in matches. Base /// behavior is to use /// - /// The rune. + /// The text. /// - public virtual bool IsWordChar (Rune rune) { return char.IsLetterOrDigit ((char)rune.Value); } + public virtual bool IsWordChar (string text) + { + return !string.IsNullOrEmpty (text) + && Rune.IsLetterOrDigit (text.EnumerateRunes ().First ()); + } /// /// @@ -65,7 +69,7 @@ public class SingleWordSuggestionGenerator : ISuggestionGenerator /// The start index of the word. /// /// - protected virtual string IdxToWord (List line, int idx, out int startIdx, int columnOffset = 0) + protected virtual string IdxToWord (List line, int idx, out int startIdx, int columnOffset = 0) { var sb = new StringBuilder (); startIdx = idx; @@ -94,7 +98,7 @@ public class SingleWordSuggestionGenerator : ISuggestionGenerator { if (IsWordChar (line [startIdx])) { - sb.Insert (0, (char)line [startIdx].Value); + sb.Insert (0, line [startIdx]); } else { diff --git a/Terminal.Gui/Views/CharMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs index bf54252cf..370c50c6b 100644 --- a/Terminal.Gui/Views/CharMap/CharMap.cs +++ b/Terminal.Gui/Views/CharMap/CharMap.cs @@ -147,10 +147,7 @@ public class CharMap : View, IDesignable break; } - var rune = new Rune (cp); - Span utf16 = new char [2]; - rune.EncodeToUtf16 (utf16); - UnicodeCategory cat = CharUnicodeInfo.GetUnicodeCategory (utf16 [0]); + UnicodeCategory cat = CharUnicodeInfo.GetUnicodeCategory (cp); if (cat == ShowUnicodeCategory.Value) { anyVisible = true; @@ -684,7 +681,7 @@ public class CharMap : View, IDesignable // Don't render out-of-range scalars if (scalar > MAX_CODE_POINT) { - AddRune (' '); + AddStr (" "); if (visibleRow == selectedRowIndex && col == selectedCol) { SetAttributeForRole (VisualRole.Normal); @@ -692,22 +689,20 @@ public class CharMap : View, IDesignable continue; } - var rune = (Rune)'?'; + string grapheme = "?"; if (Rune.IsValid (scalar)) { - rune = new (scalar); + grapheme = new Rune (scalar).ToString (); } - int width = rune.GetColumns (); + int width = grapheme.GetColumns (); // Compute visibility based on ShowUnicodeCategory bool isVisible = Rune.IsValid (scalar); if (isVisible && ShowUnicodeCategory.HasValue) { - Span filterUtf16 = new char [2]; - rune.EncodeToUtf16 (filterUtf16); - UnicodeCategory cat = CharUnicodeInfo.GetUnicodeCategory (filterUtf16 [0]); + UnicodeCategory cat = CharUnicodeInfo.GetUnicodeCategory (scalar); isVisible = cat == ShowUnicodeCategory.Value; } @@ -716,11 +711,11 @@ public class CharMap : View, IDesignable // Glyph row if (isVisible) { - RenderRune (rune, width); + RenderGrapheme (grapheme, width, scalar); } else { - AddRune (' '); + AddStr (" "); } } else @@ -735,7 +730,7 @@ public class CharMap : View, IDesignable } else { - AddRune (' '); + AddStr (" "); } } @@ -749,21 +744,18 @@ public class CharMap : View, IDesignable return true; - void RenderRune (Rune rune, int width) + void RenderGrapheme (string grapheme, int width, int scalar) { // Get the UnicodeCategory - Span utf16 = new char [2]; - int charCount = rune.EncodeToUtf16 (utf16); - // Get the bidi class for the first code unit // For most bidi characters, the first code unit is sufficient - UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory (utf16 [0]); + UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory (scalar); switch (category) { case UnicodeCategory.OtherNotAssigned: SetAttributeForRole (VisualRole.Highlight); - AddRune (Rune.ReplacementChar); + AddStr (Rune.ReplacementChar.ToString ()); SetAttributeForRole (VisualRole.Normal); break; @@ -772,7 +764,7 @@ public class CharMap : View, IDesignable // These report width of 0 and don't render on their own. case UnicodeCategory.Format: SetAttributeForRole (VisualRole.Highlight); - AddRune ('F'); + AddStr ("F"); SetAttributeForRole (VisualRole.Normal); break; @@ -785,36 +777,7 @@ public class CharMap : View, IDesignable case UnicodeCategory.EnclosingMark: if (width > 0) { - AddRune (rune); - } - else - { - if (rune.IsCombiningMark ()) - { - // This is a hack to work around the fact that combining marks - // a) can't be rendered on their own - // b) that don't normalize are not properly supported in - // any known terminal (esp Windows/AtlasEngine). - // See Issue #2616 - var sb = new StringBuilder (); - sb.Append ('a'); - sb.Append (rune); - - // Try normalizing after combining with 'a'. If it normalizes, at least - // it'll show on the 'a'. If not, just show the replacement char. - string normal = sb.ToString ().Normalize (NormalizationForm.FormC); - - if (normal.Length == 1) - { - AddRune ((Rune)normal [0]); - } - else - { - SetAttributeForRole (VisualRole.Highlight); - AddRune ('M'); - SetAttributeForRole (VisualRole.Normal); - } - } + AddStr (grapheme); } break; @@ -824,20 +787,28 @@ public class CharMap : View, IDesignable case UnicodeCategory.LineSeparator: case UnicodeCategory.ParagraphSeparator: case UnicodeCategory.Surrogate: - AddRune (rune); + AddStr (grapheme); break; + case UnicodeCategory.OtherLetter: + AddStr (grapheme); + if (width == 0) + { + AddStr (" "); + } + + break; default: // Draw the rune if (width > 0) { - AddRune (rune); + AddStr (grapheme); } else { - throw new InvalidOperationException ($"The Rune \"{rune}\" (U+{rune.Value:x6}) has zero width and no special-case UnicodeCategory logic applies."); + throw new InvalidOperationException ($"The Rune \"{grapheme}\" (U+{Rune.GetRuneAt (grapheme, 0).Value:x6}) has zero width and no special-case UnicodeCategory logic applies."); } break; diff --git a/Terminal.Gui/Views/Slider/Slider.cs b/Terminal.Gui/Views/Slider/Slider.cs index 3f379790a..985f9d16a 100644 --- a/Terminal.Gui/Views/Slider/Slider.cs +++ b/Terminal.Gui/Views/Slider/Slider.cs @@ -74,13 +74,13 @@ public class Slider : View, IOrientation switch (_config._sliderOrientation) { case Orientation.Horizontal: - Style.SpaceChar = new () { Rune = Glyphs.HLine }; // '─' - Style.OptionChar = new () { Rune = Glyphs.BlackCircle }; // '┼●🗹□⏹' + Style.SpaceChar = new () { Grapheme = Glyphs.HLine.ToString () }; // '─' + Style.OptionChar = new () { Grapheme = Glyphs.BlackCircle.ToString () }; // '┼●🗹□⏹' break; case Orientation.Vertical: - Style.SpaceChar = new () { Rune = Glyphs.VLine }; - Style.OptionChar = new () { Rune = Glyphs.BlackCircle }; + Style.SpaceChar = new () { Grapheme = Glyphs.VLine.ToString () }; + Style.OptionChar = new () { Grapheme = Glyphs.BlackCircle.ToString () }; break; } @@ -105,12 +105,12 @@ public class Slider : View, IOrientation */ _config._legendsOrientation = _config._sliderOrientation; - Style.EmptyChar = new () { Rune = new (' ') }; - Style.SetChar = new () { Rune = Glyphs.ContinuousMeterSegment }; // ■ - Style.RangeChar = new () { Rune = Glyphs.Stipple }; // ░ ▒ ▓ // Medium shade not blinking on curses. - Style.StartRangeChar = new () { Rune = Glyphs.ContinuousMeterSegment }; - Style.EndRangeChar = new () { Rune = Glyphs.ContinuousMeterSegment }; - Style.DragChar = new () { Rune = Glyphs.Diamond }; + Style.EmptyChar = new () { Grapheme = " " }; + Style.SetChar = new () { Grapheme = Glyphs.ContinuousMeterSegment.ToString () }; // ■ + Style.RangeChar = new () { Grapheme = Glyphs.Stipple.ToString () }; // ░ ▒ ▓ // Medium shade not blinking on curses. + Style.StartRangeChar = new () { Grapheme = Glyphs.ContinuousMeterSegment.ToString () }; + Style.EndRangeChar = new () { Grapheme = Glyphs.ContinuousMeterSegment.ToString () }; + Style.DragChar = new () { Grapheme = Glyphs.Diamond.ToString () }; // TODO: Support left & right (top/bottom) // First = '├', @@ -256,11 +256,11 @@ public class Slider : View, IOrientation switch (_config._sliderOrientation) { case Orientation.Horizontal: - Style.SpaceChar = new () { Rune = Glyphs.HLine }; // '─' + Style.SpaceChar = new () { Grapheme = Glyphs.HLine.ToString () }; // '─' break; case Orientation.Vertical: - Style.SpaceChar = new () { Rune = Glyphs.VLine }; + Style.SpaceChar = new () { Grapheme = Glyphs.VLine.ToString () }; break; } @@ -799,7 +799,7 @@ public class Slider : View, IOrientation if (_dragPosition.HasValue && _moveRenderPosition.HasValue) { - AddRune (_moveRenderPosition.Value.X, _moveRenderPosition.Value.Y, Style.DragChar.Rune); + AddStr (_moveRenderPosition.Value.X, _moveRenderPosition.Value.Y, Style.DragChar.Grapheme); } return true; @@ -875,11 +875,11 @@ public class Slider : View, IOrientation ? Style.RangeChar.Attribute ?? normalAttr : Style.SpaceChar.Attribute ?? normalAttr ); - Rune rune = isSet && _config._type == SliderType.LeftRange ? Style.RangeChar.Rune : Style.SpaceChar.Rune; + string text = isSet && _config._type == SliderType.LeftRange ? Style.RangeChar.Grapheme : Style.SpaceChar.Grapheme; for (var i = 0; i < _config._startSpacing; i++) { - MoveAndAdd (x, y, rune); + MoveAndAdd (x, y, text); if (isVertical) { @@ -897,7 +897,7 @@ public class Slider : View, IOrientation for (var i = 0; i < _config._startSpacing; i++) { - MoveAndAdd (x, y, Style.EmptyChar.Rune); + MoveAndAdd (x, y, Style.EmptyChar.Grapheme); if (isVertical) { @@ -951,25 +951,25 @@ public class Slider : View, IOrientation drawRange ? Style.RangeChar.Attribute ?? setAttr : Style.OptionChar.Attribute ?? normalAttr ); - Rune rune = drawRange ? Style.RangeChar.Rune : Style.OptionChar.Rune; + string text = drawRange ? Style.RangeChar.Grapheme : Style.OptionChar.Grapheme; if (isSet) { if (_setOptions [0] == i) { - rune = Style.StartRangeChar.Rune; + text = Style.StartRangeChar.Grapheme; } else if (_setOptions.Count > 1 && _setOptions [1] == i) { - rune = Style.EndRangeChar.Rune; + text = Style.EndRangeChar.Grapheme; } else if (_setOptions.Contains (i)) { - rune = Style.SetChar.Rune; + text = Style.SetChar.Grapheme; } } - MoveAndAdd (x, y, rune); + MoveAndAdd (x, y, text); if (isVertical) { @@ -992,7 +992,7 @@ public class Slider : View, IOrientation for (var s = 0; s < _config._cachedInnerSpacing; s++) { - MoveAndAdd (x, y, drawRange && isSet ? Style.RangeChar.Rune : Style.SpaceChar.Rune); + MoveAndAdd (x, y, drawRange && isSet ? Style.RangeChar.Grapheme : Style.SpaceChar.Grapheme); if (isVertical) { @@ -1017,11 +1017,11 @@ public class Slider : View, IOrientation ? Style.RangeChar.Attribute ?? normalAttr : Style.SpaceChar.Attribute ?? normalAttr ); - Rune rune = isSet && _config._type == SliderType.RightRange ? Style.RangeChar.Rune : Style.SpaceChar.Rune; + string text = isSet && _config._type == SliderType.RightRange ? Style.RangeChar.Grapheme : Style.SpaceChar.Grapheme; for (var i = 0; i < remaining; i++) { - MoveAndAdd (x, y, rune); + MoveAndAdd (x, y, text); if (isVertical) { @@ -1039,7 +1039,7 @@ public class Slider : View, IOrientation for (var i = 0; i < remaining; i++) { - MoveAndAdd (x, y, Style.EmptyChar.Rune); + MoveAndAdd (x, y, Style.EmptyChar.Grapheme); if (isVertical) { diff --git a/Terminal.Gui/Views/TableView/TreeTableSource.cs b/Terminal.Gui/Views/TableView/TreeTableSource.cs index 8c6925399..61e15f78a 100644 --- a/Terminal.Gui/Views/TableView/TreeTableSource.cs +++ b/Terminal.Gui/Views/TableView/TreeTableSource.cs @@ -88,14 +88,14 @@ public class TreeTableSource : IEnumerableTableSource, IDisposable where T { Branch branch = RowToBranch (row); - // Everything on line before the expansion run and branch text - Rune [] prefix = branch.GetLinePrefix ().ToArray (); - Rune expansion = branch.GetExpandableSymbol (); + // Everything on the line before the expansion run and branch text + string [] prefix = branch.GetLinePrefix ().ToArray (); + string expansion = branch.GetExpandableSymbol (); string lineBody = _tree.AspectGetter (branch.Model) ?? ""; var sb = new StringBuilder (); - foreach (Rune p in prefix) + foreach (string p in prefix) { sb.Append (p); } diff --git a/Terminal.Gui/Views/TextInput/TextField.cs b/Terminal.Gui/Views/TextInput/TextField.cs index 37a92062b..38d123c19 100644 --- a/Terminal.Gui/Views/TextInput/TextField.cs +++ b/Terminal.Gui/Views/TextInput/TextField.cs @@ -17,7 +17,7 @@ public class TextField : View, IDesignable private int _selectedStart; // -1 represents there is no text selection. private string _selectedText; private int _start; - private List _text; + private List _text; /// /// Initializes a new instance of the class. @@ -541,7 +541,7 @@ public class TextField : View, IDesignable ClearAllSelection (); // Note we use NewValue here; TextChanging subscribers may have changed it - _text = args.Result.EnumerateRunes ().ToList (); + _text = args.Result.ToStringList (); if (!Secret && !_historyText.IsFromHistory) { @@ -629,7 +629,7 @@ public class TextField : View, IDesignable } Clipboard.Contents = SelectedText; - List newText = DeleteSelectedText (); + List newText = DeleteSelectedText (); Text = StringExtensions.ToString (newText); Adjust (); } @@ -700,7 +700,7 @@ public class TextField : View, IDesignable } else { - List newText = DeleteSelectedText (); + List newText = DeleteSelectedText (); Text = StringExtensions.ToString (newText); Adjust (); } @@ -734,7 +734,7 @@ public class TextField : View, IDesignable } else { - List newText = DeleteSelectedText (); + List newText = DeleteSelectedText (); Text = StringExtensions.ToString (newText); Adjust (); } @@ -943,8 +943,8 @@ public class TextField : View, IDesignable for (int idx = p; idx < tcount; idx++) { - Rune rune = _text [idx]; - int cols = rune.GetColumns (); + string text = _text [idx]; + int cols = text.GetColumns (); if (!Enabled) { @@ -980,7 +980,7 @@ public class TextField : View, IDesignable if (col + cols <= width) { - AddRune (Secret ? Glyphs.Dot : rune); + AddStr (Secret ? Glyphs.Dot.ToString () : text); } if (!TextModel.SetCol (ref col, width, cols)) @@ -1254,7 +1254,7 @@ public class TextField : View, IDesignable private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.Replace (e.OldKey.KeyCode, e.NewKey.KeyCode); } - private List DeleteSelectedText () + private List DeleteSelectedText () { SetSelectedStartSelectedLength (); int selStart = SelectedStart > -1 ? _start : _cursorPosition; @@ -1270,7 +1270,7 @@ public class TextField : View, IDesignable ClearAllSelection (); _cursorPosition = selStart >= newText.GetRuneCount () ? newText.GetRuneCount () : selStart; - return newText.ToRuneList (); + return newText.ToStringList (); } private void GenerateSuggestions () @@ -1318,7 +1318,7 @@ public class TextField : View, IDesignable new (_cursorPosition, 0) ); - List newText = _text; + List newText = _text; if (SelectedLength > 0) { @@ -1339,7 +1339,7 @@ public class TextField : View, IDesignable if (_cursorPosition == newText.Count + 1) { - SetText (newText.Concat (kbstr).ToList ()); + SetText (newText.Concat (kbstr.Select (r => r.ToString ())).ToList ()); } else { @@ -1350,7 +1350,7 @@ public class TextField : View, IDesignable SetText ( newText.GetRange (0, _preTextChangedCursorPos) - .Concat (kbstr) + .Concat (kbstr.Select (r => r.ToString ())) .Concat ( newText.GetRange ( _preTextChangedCursorPos, @@ -1367,7 +1367,7 @@ public class TextField : View, IDesignable { SetText ( newText.GetRange (0, _preTextChangedCursorPos) - .Concat (kbstr) + .Concat (kbstr.Select (r => r.ToString ())) .Concat ( newText.GetRange ( Math.Min (_preTextChangedCursorPos + 1, newText.Count), @@ -1729,7 +1729,7 @@ public class TextField : View, IDesignable TitleTextFormatter.Draw (driver: Driver, screen: ViewportToScreen (new Rectangle (0, 0, Viewport.Width, 1)), normalColor: captionAttribute, hotColor: hotKeyAttribute); } - private void SetClipboard (IEnumerable text) + private void SetClipboard (IEnumerable text) { if (!Secret) { @@ -1755,8 +1755,8 @@ public class TextField : View, IDesignable } } - private void SetText (List newText) { Text = StringExtensions.ToString (newText); } - private void SetText (IEnumerable newText) { SetText (newText.ToList ()); } + private void SetText (List newText) { Text = StringExtensions.ToString (newText); } + private void SetText (IEnumerable newText) { SetText (newText.ToList ()); } private void ShowContextMenu (bool keyboard) { diff --git a/Terminal.Gui/Views/TextInput/TextModel.cs b/Terminal.Gui/Views/TextInput/TextModel.cs index 81d07848a..296c39579 100644 --- a/Terminal.Gui/Views/TextInput/TextModel.cs +++ b/Terminal.Gui/Views/TextInput/TextModel.cs @@ -71,7 +71,7 @@ internal class TextModel for (int i = first; i < last; i++) { List line = GetLine (i); - int tabSum = line.Sum (c => c.Rune.Value == '\t' ? Math.Max (tabWidth - 1, 0) : 0); + int tabSum = line.Sum (c => c.Grapheme == "\t" ? Math.Max (tabWidth - 1, 0) : 0); int l = line.Count + tabSum; if (l > maxLength) @@ -222,7 +222,7 @@ internal class TextModel if (cell is { }) { - rune = cell.Value.Rune; + rune = Rune.GetRuneAt (cell.Value.Grapheme, 0); } else { @@ -299,10 +299,11 @@ internal class TextModel } List line = GetLine (nRow); + Rune firstRune = Rune.GetRuneAt (line [0].Grapheme, 0); if (nCol == 0 && nRow == fromRow - && (Rune.IsLetterOrDigit (line [0].Rune) || Rune.IsPunctuation (line [0].Rune) || Rune.IsSymbol (line [0].Rune))) + && (Rune.IsLetterOrDigit (firstRune) || Rune.IsPunctuation (firstRune) || Rune.IsSymbol (firstRune))) { return; } @@ -366,7 +367,7 @@ internal class TextModel try { - Rune rune = _lines [row].Count > 0 ? RuneAt (col, row)!.Value.Rune : default (Rune); + Rune rune = _lines [row].Count > 0 ? Rune.GetRuneAt (RuneAt (col, row)!.Value.Grapheme, 0) : default (Rune); RuneType runeType = GetRuneType (rune); int lastValidCol = IsSameRuneType (rune, runeType, useSameRuneType) && (Rune.IsLetterOrDigit (rune) || Rune.IsPunctuation (rune) || Rune.IsSymbol (rune)) @@ -425,10 +426,11 @@ internal class TextModel } List line = GetLine (nRow); + Rune firstRune = Rune.GetRuneAt (line [0].Grapheme, 0); if (nCol == line.Count && nRow == fromRow - && (Rune.IsLetterOrDigit (line [0].Rune) || Rune.IsPunctuation (line [0].Rune) || Rune.IsSymbol (line [0].Rune))) + && (Rune.IsLetterOrDigit (firstRune) || Rune.IsPunctuation (firstRune) || Rune.IsSymbol (firstRune))) { return; } @@ -475,10 +477,10 @@ internal class TextModel } if (startCol > 0 - && StringExtensions.ToString (line.GetRange (startCol, col - startCol).Select (c => c.Rune).ToList ()).Trim () == "" - && (col - startCol > 1 || (col - startCol > 0 && line [startCol - 1].Rune == (Rune)' '))) + && StringExtensions.ToString (line.GetRange (startCol, col - startCol).Select (c => c.Grapheme).ToList ()).Trim () == "" + && (col - startCol > 1 || (col - startCol > 0 && line [startCol - 1].Grapheme == " "))) { - while (startCol > 0 && line [startCol - 1].Rune == (Rune)' ') + while (startCol > 0 && line [startCol - 1].Grapheme == " ") { startCol--; } @@ -495,13 +497,13 @@ internal class TextModel if (selectWordOnly) { - List selRunes = line.GetRange (startCol, col - startCol).Select (c => c.Rune).ToList (); + List selText = line.GetRange (startCol, col - startCol).Select (c => c.Grapheme).ToList (); - if (StringExtensions.ToString (selRunes).Trim () != "") + if (StringExtensions.ToString (selText).Trim () != "") { - for (int i = selRunes.Count - 1; i > -1; i--) + for (int i = selText.Count - 1; i > -1; i--) { - if (selRunes [i] == (Rune)' ') + if (selText [i] == " ") { col--; } @@ -519,18 +521,18 @@ internal class TextModel internal static int CalculateLeftColumn (List t, int start, int end, int width, int tabWidth = 0) { - List runes = new (); + List strings = new (); foreach (Cell cell in t) { - runes.Add (cell.Rune); + strings.Add (cell.Grapheme); } - return CalculateLeftColumn (runes, start, end, width, tabWidth); + return CalculateLeftColumn (strings, start, end, width, tabWidth); } // Returns the left column in a range of the string. - internal static int CalculateLeftColumn (List t, int start, int end, int width, int tabWidth = 0) + internal static int CalculateLeftColumn (List t, int start, int end, int width, int tabWidth = 0) { if (t is null || t.Count == 0) { @@ -538,15 +540,15 @@ internal class TextModel } var size = 0; - int tcount = end > t.Count - 1 ? t.Count - 1 : end; + int tCount = end > t.Count - 1 ? t.Count - 1 : end; var col = 0; - for (int i = tcount; i >= 0; i--) + for (int i = tCount; i >= 0; i--) { - Rune rune = t [i]; - size += rune.GetColumns (); + string text = t [i]; + size += text.GetColumns (false); - if (rune.Value == '\t') + if (text == "\t") { size += tabWidth + 1; } @@ -576,23 +578,23 @@ internal class TextModel List t, int start = -1, int end = -1, - bool checkNextRune = true, + bool checkNextText = true, int tabWidth = 0 ) { - List runes = new (); + List strings = new (); foreach (Cell cell in t) { - runes.Add (cell.Rune); + strings.Add (cell.Grapheme); } - return DisplaySize (runes, start, end, checkNextRune, tabWidth); + return DisplaySize (strings, start, end, checkNextText, tabWidth); } // Returns the size and length in a range of the string. internal static (int size, int length) DisplaySize ( - List t, + List t, int start = -1, int end = -1, bool checkNextRune = true, @@ -607,35 +609,35 @@ internal class TextModel var size = 0; var len = 0; - int tcount = end == -1 ? t.Count : + int tCount = end == -1 ? t.Count : end > t.Count ? t.Count : end; int i = start == -1 ? 0 : start; - for (; i < tcount; i++) + for (; i < tCount; i++) { - Rune rune = t [i]; - size += rune.GetColumns (); - len += rune.GetEncodingLength (Encoding.Unicode); + string text = t [i]; + size += text.GetColumns (false); + len += text.Length; - if (rune.Value == '\t') + if (text == "\t") { size += tabWidth + 1; len += tabWidth - 1; } - if (checkNextRune && i == tcount - 1 && t.Count > tcount && IsWideRune (t [i + 1], tabWidth, out int s, out int l)) + if (checkNextRune && i == tCount - 1 && t.Count > tCount && IsWideText (t [i + 1], tabWidth, out int s, out int l)) { size += s; len += l; } } - bool IsWideRune (Rune r, int tWidth, out int s, out int l) + bool IsWideText (string s1, int tWidth, out int s, out int l) { - s = r.GetColumns (); - l = r.GetEncodingLength (); + s = s1.GetColumns (); + l = Encoding.Unicode.GetByteCount (s1); - if (r.Value == '\t') + if (s1 == "\t") { s += tWidth + 1; l += tWidth - 1; @@ -744,17 +746,17 @@ internal class TextModel internal static int GetColFromX (List t, int start, int x, int tabWidth = 0) { - List runes = new (); + List strings = new (); foreach (Cell cell in t) { - runes.Add (cell.Rune); + strings.Add (cell.Grapheme); } - return GetColFromX (runes, start, x, tabWidth); + return GetColFromX (strings, start, x, tabWidth); } - internal static int GetColFromX (List t, int start, int x, int tabWidth = 0) + internal static int GetColFromX (List t, int start, int x, int tabWidth = 0) { if (x < 0) { @@ -766,10 +768,10 @@ internal class TextModel for (int i = start; i < t.Count; i++) { - Rune r = t [i]; - size += r.GetColumns (); + string s = t [i]; + size += s.GetColumns (); - if (r.Value == '\t') + if (s == "\t") { size += tabWidth + 1; } @@ -1055,18 +1057,21 @@ internal class TextModel if (col + 1 < line.Count) { col++; - rune = line [col].Rune; + rune = Rune.GetRuneAt (line [col].Grapheme, 0); + Rune prevRune = Rune.GetRuneAt (line [col - 1].Grapheme, 0); if (col + 1 == line.Count && !Rune.IsLetterOrDigit (rune) - && !Rune.IsWhiteSpace (line [col - 1].Rune) - && IsSameRuneType (line [col - 1].Rune, GetRuneType (rune), useSameRuneType)) + && !Rune.IsWhiteSpace (prevRune) + && IsSameRuneType (prevRune, GetRuneType (rune), useSameRuneType)) { col++; } + prevRune = Rune.GetRuneAt (line [col - 1].Grapheme, 0); + if (!Rune.IsWhiteSpace (rune) - && (Rune.IsWhiteSpace (line [col - 1].Rune) || !IsSameRuneType (line [col - 1].Rune, GetRuneType (rune), useSameRuneType))) + && (Rune.IsWhiteSpace (prevRune) || !IsSameRuneType (prevRune, GetRuneType (rune), useSameRuneType))) { return false; } @@ -1097,12 +1102,13 @@ internal class TextModel if (col > 0) { col--; - rune = line [col].Rune; + rune = Rune.GetRuneAt (line [col].Grapheme, 0); + Rune nextRune = Rune.GetRuneAt (line [col + 1].Grapheme, 0); if ((!Rune.IsWhiteSpace (rune) - && !Rune.IsWhiteSpace (line [col + 1].Rune) - && !IsSameRuneType (line [col + 1].Rune, GetRuneType (rune), useSameRuneType)) - || (Rune.IsWhiteSpace (rune) && !Rune.IsWhiteSpace (line [col + 1].Rune))) + && !Rune.IsWhiteSpace (nextRune) + && !IsSameRuneType (nextRune, GetRuneType (rune), useSameRuneType)) + || (Rune.IsWhiteSpace (rune) && !Rune.IsWhiteSpace (nextRune))) { return false; } diff --git a/Terminal.Gui/Views/TextInput/TextView.cs b/Terminal.Gui/Views/TextInput/TextView.cs index 8b5da5ef3..c2ad7d463 100644 --- a/Terminal.Gui/Views/TextInput/TextView.cs +++ b/Terminal.Gui/Views/TextInput/TextView.cs @@ -1481,7 +1481,7 @@ public class TextView : View, IDesignable } /// Loads the contents of the list into the . - /// Rune cells list to load the contents from. + /// Text cells list to load the contents from. public void Load (List cells) { SetWrapModel (); @@ -1801,8 +1801,8 @@ public class TextView : View, IDesignable for (int idxCol = _leftColumn; idxCol < lineRuneCount; idxCol++) { - Rune rune = idxCol >= lineRuneCount ? (Rune)' ' : line [idxCol].Rune; - int cols = rune.GetColumns (); + string text = idxCol >= lineRuneCount ? " " : line [idxCol].Grapheme; + int cols = text.GetColumns (false); if (idxCol < line.Count && IsSelecting && PointInSelection (idxCol, idxRow)) { @@ -1821,7 +1821,7 @@ public class TextView : View, IDesignable OnDrawNormalColor (line, idxCol, idxRow); } - if (rune.Value == '\t') + if (text == "\t") { cols += TabWidth + 1; @@ -1840,7 +1840,7 @@ public class TextView : View, IDesignable } else { - AddRune (col, row, rune); + AddStr (col, row, text); // Ensures that cols less than 0 to be 1 because it will be converted to a printable rune cols = Math.Max (cols, 1); @@ -1851,7 +1851,7 @@ public class TextView : View, IDesignable break; } - if (idxCol + 1 < lineRuneCount && col + line [idxCol + 1].Rune.GetColumns () > right) + if (idxCol + 1 < lineRuneCount && col + line [idxCol + 1].Grapheme.GetColumns () > right) { break; } @@ -2047,9 +2047,9 @@ public class TextView : View, IDesignable break; } - int cols = line [idx].Rune.GetColumns (); + int cols = line [idx].Grapheme.GetColumns (); - if (line [idx].Rune.Value == '\t') + if (line [idx].Grapheme == "\t") { cols += TabWidth + 1; } @@ -2806,12 +2806,12 @@ public class TextView : View, IDesignable cells = line.GetRange (startCol, endCol - startCol); cellsList.Add (cells); - return StringFromRunes (cells); + return StringFromCells (cells); } cells = line.GetRange (startCol, line.Count - startCol); cellsList.Add (cells); - string res = StringFromRunes (cells); + string res = StringFromCells (cells); for (int row = startRow + 1; row < maxRow; row++) { @@ -2821,14 +2821,14 @@ public class TextView : View, IDesignable res = res + Environment.NewLine - + StringFromRunes (cells); + + StringFromCells (cells); } line = model is null ? _model.GetLine (maxRow) : model.GetLine (maxRow); cellsList.AddRange ([]); cells = line.GetRange (0, endCol); cellsList.Add (cells); - res = res + Environment.NewLine + StringFromRunes (cells); + res = res + Environment.NewLine + StringFromCells (cells); return res; } @@ -3108,7 +3108,7 @@ public class TextView : View, IDesignable { if (Used) { - Insert (new () { Rune = a.AsRune, Attribute = attribute }); + Insert (new () { Grapheme = a.AsRune.ToString (), Attribute = attribute }); CurrentColumn++; if (CurrentColumn >= _leftColumn + Viewport.Width) @@ -3119,7 +3119,7 @@ public class TextView : View, IDesignable } else { - Insert (new () { Rune = a.AsRune, Attribute = attribute }); + Insert (new () { Grapheme = a.AsRune.ToString (), Attribute = attribute }); CurrentColumn++; } } @@ -3207,7 +3207,7 @@ public class TextView : View, IDesignable int restCount = currentLine.Count - CurrentColumn; List rest = currentLine.GetRange (CurrentColumn, restCount); var val = string.Empty; - val += StringFromRunes (rest); + val += StringFromCells (rest); if (_lastWasKill) { @@ -3313,7 +3313,7 @@ public class TextView : View, IDesignable int restCount = CurrentColumn; List rest = currentLine.GetRange (0, restCount); var val = string.Empty; - val += StringFromRunes (rest); + val += StringFromCells (rest); if (_lastWasKill) { @@ -3842,7 +3842,7 @@ public class TextView : View, IDesignable List currentLine = GetCurrentLine (); - if (currentLine.Count > 0 && currentLine [CurrentColumn - 1].Rune.Value == '\t') + if (currentLine.Count > 0 && currentLine[CurrentColumn - 1].Grapheme == "\t") { _historyText.Add (new () { new (currentLine) }, CursorPosition); @@ -4601,29 +4601,28 @@ public class TextView : View, IDesignable _isButtonShift = false; } - private string StringFromRunes (List cells) + private string StringFromCells (List cells) { - if (cells is null) - { - throw new ArgumentNullException (nameof (cells)); - } + ArgumentNullException.ThrowIfNull (cells); var size = 0; - foreach (Cell cell in cells) { - size += cell.Rune.GetEncodingLength (); + string t = cell.Grapheme; + size += Encoding.Unicode.GetByteCount (t); } - var encoded = new byte [size]; + byte [] encoded = new byte [size]; var offset = 0; - foreach (Cell cell in cells) { - offset += cell.Rune.Encode (encoded, offset); + string t = cell.Grapheme; + int bytesWritten = Encoding.Unicode.GetBytes (t, 0, t.Length, encoded, offset); + offset += bytesWritten; } - return StringExtensions.ToString (encoded); + // decode using the same encoding and the bytes actually written + return Encoding.Unicode.GetString (encoded, 0, offset); } private void TextView_SuperViewChanged (object sender, SuperViewChangedEventArgs e) diff --git a/Terminal.Gui/Views/TreeView/Branch.cs b/Terminal.Gui/Views/TreeView/Branch.cs index 9c48a0a0d..481adafac 100644 --- a/Terminal.Gui/Views/TreeView/Branch.cs +++ b/Terminal.Gui/Views/TreeView/Branch.cs @@ -87,9 +87,9 @@ internal class Branch where T : class isSelected ? _tree.HasFocus ? _tree.GetAttributeForRole (VisualRole.Focus) : _tree.GetAttributeForRole (VisualRole.HotNormal) : _tree.GetAttributeForRole (VisualRole.Normal); Attribute symbolColor = _tree.Style.HighlightModelTextOnly ? _tree.GetAttributeForRole (VisualRole.Normal) : textColor; - // Everything on line before the expansion run and branch text - Rune [] prefix = GetLinePrefix ().ToArray (); - Rune expansion = GetExpandableSymbol (); + // Everything on the line before the expansion run and branch text + string [] prefix = GetLinePrefix ().ToArray (); + string expansion = GetExpandableSymbol (); string lineBody = _tree.AspectGetter (Model) ?? ""; _tree.Move (0, y); @@ -99,7 +99,7 @@ internal class Branch where T : class Attribute attr = symbolColor; // Draw the line prefix (all parallel lanes or whitespace and an expand/collapse/leaf symbol) - foreach (Rune r in prefix) + foreach (string s in prefix) { if (toSkip > 0) { @@ -107,8 +107,8 @@ internal class Branch where T : class } else { - cells.Add (NewCell (attr, r)); - availableWidth -= r.GetColumns (); + cells.Add (NewCell (attr, s)); + availableWidth -= s.GetColumns (); } } @@ -212,7 +212,7 @@ internal class Branch where T : class } attr = modelColor; - cells.AddRange (lineBody.Select (r => NewCell (attr, new (r)))); + cells.AddRange (lineBody.Select (c => NewCell (attr, c.ToString ()))); if (availableWidth > 0) { @@ -220,7 +220,7 @@ internal class Branch where T : class cells.AddRange ( Enumerable.Repeat ( - NewCell (attr, new (' ')), + NewCell (attr, " "), availableWidth ) ); @@ -243,7 +243,7 @@ internal class Branch where T : class foreach (Cell cell in cells) { _tree.SetAttribute ((Attribute)cell.Attribute!); - _tree.AddRune (cell.Rune); + _tree.AddStr (cell.Grapheme); } } @@ -288,21 +288,21 @@ internal class Branch where T : class /// object to indicate whether it or not (or it is a leaf). /// /// - public Rune GetExpandableSymbol () + public string GetExpandableSymbol () { Rune leafSymbol = _tree.Style.ShowBranchLines ? Glyphs.HLine : (Rune)' '; if (IsExpanded) { - return _tree.Style.CollapseableSymbol ?? leafSymbol; + return _tree.Style.CollapseableSymbol.ToString () ?? leafSymbol.ToString (); } if (CanExpand ()) { - return _tree.Style.ExpandableSymbol ?? leafSymbol; + return _tree.Style.ExpandableSymbol.ToString () ?? leafSymbol.ToString (); } - return leafSymbol; + return leafSymbol.ToString (); } /// @@ -409,14 +409,14 @@ internal class Branch where T : class /// any tree branches (if enabled). /// /// - internal IEnumerable GetLinePrefix () + internal IEnumerable GetLinePrefix () { // If not showing line branches or this is a root object. if (!_tree.Style.ShowBranchLines) { for (var i = 0; i < Depth; i++) { - yield return new (' '); + yield return new (" "); } yield break; @@ -427,23 +427,23 @@ internal class Branch where T : class { if (cur.IsLast ()) { - yield return new (' '); + yield return new (" "); } else { - yield return Glyphs.VLine; + yield return Glyphs.VLine.ToString (); } - yield return new (' '); + yield return new (" "); } if (IsLast ()) { - yield return Glyphs.LLCorner; + yield return Glyphs.LLCorner.ToString (); } else { - yield return Glyphs.LeftTee; + yield return Glyphs.LeftTee.ToString (); } } @@ -531,5 +531,5 @@ internal class Branch where T : class return Parent.ChildBranches.LastOrDefault () == this; } - private static Cell NewCell (Attribute attr, Rune r) { return new () { Rune = r, Attribute = new (attr) }; } + private static Cell NewCell (Attribute attr, string s) { return new () { Grapheme = s, Attribute = new (attr) }; } } diff --git a/Tests/UnitTests/DriverAssert.cs b/Tests/UnitTests/DriverAssert.cs index 4e7795b13..7bac03f05 100644 --- a/Tests/UnitTests/DriverAssert.cs +++ b/Tests/UnitTests/DriverAssert.cs @@ -198,7 +198,7 @@ internal partial class DriverAssert IDriver? driver = null ) { - List> lines = []; + List> lines = []; var sb = new StringBuilder (); driver ??= Application.Driver!; @@ -211,13 +211,13 @@ internal partial class DriverAssert for (var rowIndex = 0; rowIndex < driver.Rows; rowIndex++) { - List runes = []; + List strings = []; for (var colIndex = 0; colIndex < driver.Cols; colIndex++) { - Rune runeAtCurrentLocation = contents! [rowIndex, colIndex].Rune; + string textAtCurrentLocation = contents! [rowIndex, colIndex].Grapheme; - if (runeAtCurrentLocation != _spaceRune) + if (textAtCurrentLocation != _spaceRune.ToString ()) { if (x == -1) { @@ -226,11 +226,11 @@ internal partial class DriverAssert for (var i = 0; i < colIndex; i++) { - runes.InsertRange (i, [_spaceRune]); + strings.InsertRange (i, [_spaceRune.ToString ()]); } } - if (runeAtCurrentLocation.GetColumns () > 1) + if (textAtCurrentLocation.GetColumns () > 1) { colIndex++; } @@ -245,18 +245,13 @@ internal partial class DriverAssert if (x > -1) { - runes.Add (runeAtCurrentLocation); + strings.Add (textAtCurrentLocation); } - - // See Issue #2616 - //foreach (var combMark in contents [r, c].CombiningMarks) { - // runes.Add (combMark); - //} } - if (runes.Count > 0) + if (strings.Count > 0) { - lines.Add (runes); + lines.Add (strings); } } @@ -270,13 +265,13 @@ internal partial class DriverAssert } // Remove trailing whitespace on each line - foreach (List row in lines) + foreach (List row in lines) { for (int c = row.Count - 1; c >= 0; c--) { - Rune rune = row [c]; + string text = row [c]; - if (rune != (Rune)' ' || row.Sum (x => x.GetColumns ()) == w) + if (text != " " || row.Sum (x => x.GetColumns ()) == w) { break; } @@ -285,7 +280,7 @@ internal partial class DriverAssert } } - // Convert Rune list to string + // Convert Text list to string for (var r = 0; r < lines.Count; r++) { var line = StringExtensions.ToString (lines [r]); diff --git a/Tests/UnitTests/Drivers/ClipRegionTests.cs b/Tests/UnitTests/Drivers/ClipRegionTests.cs index 013cdbe7a..def6bdde0 100644 --- a/Tests/UnitTests/Drivers/ClipRegionTests.cs +++ b/Tests/UnitTests/Drivers/ClipRegionTests.cs @@ -16,24 +16,24 @@ public class ClipRegionTests (ITestOutputHelper output) Application.Driver!.Move (0, 0); Application.Driver!.AddRune ('x'); - Assert.Equal ((Rune)'x', Application.Driver!.Contents! [0, 0].Rune); + Assert.Equal ("x", Application.Driver!.Contents! [0, 0].Grapheme); Application.Driver?.Move (5, 5); Application.Driver?.AddRune ('x'); - Assert.Equal ((Rune)'x', Application.Driver!.Contents [5, 5].Rune); + Assert.Equal ("x", Application.Driver!.Contents [5, 5].Grapheme); // Clear the contents Application.Driver?.FillRect (new Rectangle (0, 0, Application.Driver.Rows, Application.Driver.Cols), ' '); - Assert.Equal ((Rune)' ', Application.Driver?.Contents [0, 0].Rune); + Assert.Equal (" ", Application.Driver?.Contents [0, 0].Grapheme); // Setup the region with a single rectangle, fill screen with 'x' Application.Driver!.Clip = new (new Rectangle (5, 5, 5, 5)); Application.Driver.FillRect (new Rectangle (0, 0, Application.Driver.Rows, Application.Driver.Cols), 'x'); - Assert.Equal ((Rune)' ', Application.Driver?.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', Application.Driver?.Contents [4, 9].Rune); - Assert.Equal ((Rune)'x', Application.Driver?.Contents [5, 5].Rune); - Assert.Equal ((Rune)'x', Application.Driver?.Contents [9, 9].Rune); - Assert.Equal ((Rune)' ', Application.Driver?.Contents [10, 10].Rune); + Assert.Equal (" ", Application.Driver?.Contents [0, 0].Grapheme); + Assert.Equal (" ", Application.Driver?.Contents [4, 9].Grapheme); + Assert.Equal ("x", Application.Driver?.Contents [5, 5].Grapheme); + Assert.Equal ("x", Application.Driver?.Contents [9, 9].Grapheme); + Assert.Equal (" ", Application.Driver?.Contents [10, 10].Grapheme); Application.Shutdown (); } diff --git a/Tests/UnitTests/View/Draw/ClipTests.cs b/Tests/UnitTests/View/Draw/ClipTests.cs index 4e3f62e60..1efec5d43 100644 --- a/Tests/UnitTests/View/Draw/ClipTests.cs +++ b/Tests/UnitTests/View/Draw/ClipTests.cs @@ -49,17 +49,17 @@ public class ClipTests (ITestOutputHelper _output) view.Draw (); // Only valid location w/in Viewport is 0, 0 (view) - 2, 2 (screen) - Assert.Equal ((Rune)' ', Application.Driver?.Contents! [2, 2].Rune); + Assert.Equal (" ", Application.Driver?.Contents! [2, 2].Grapheme); // When we exit Draw, the view is excluded from the clip. So drawing at 0,0, is not valid and is clipped. view.AddRune (0, 0, Rune.ReplacementChar); - Assert.Equal ((Rune)' ', Application.Driver?.Contents! [2, 2].Rune); + Assert.Equal (" ", Application.Driver?.Contents! [2, 2].Grapheme); view.AddRune (-1, -1, Rune.ReplacementChar); - Assert.Equal ((Rune)'P', Application.Driver?.Contents! [1, 1].Rune); + Assert.Equal ("P", Application.Driver?.Contents! [1, 1].Grapheme); view.AddRune (1, 1, Rune.ReplacementChar); - Assert.Equal ((Rune)'P', Application.Driver?.Contents! [3, 3].Rune); + Assert.Equal ("P", Application.Driver?.Contents! [3, 3].Grapheme); } [Theory] @@ -233,7 +233,7 @@ public class ClipTests (ITestOutputHelper _output) // 01 2345678901234 56 78 90 12 34 56 // │� |0123456989│� ン ラ イ ン で す 。 expectedOutput = """ - │�│0123456789│�ンラインです。 + │�│0123456789│ ンラインです。 """; DriverAssert.AssertDriverContentsWithFrameAre (expectedOutput, _output); diff --git a/Tests/UnitTests/View/Draw/DrawTests.cs b/Tests/UnitTests/View/Draw/DrawTests.cs index 0e84371d5..6a8a13325 100644 --- a/Tests/UnitTests/View/Draw/DrawTests.cs +++ b/Tests/UnitTests/View/Draw/DrawTests.cs @@ -14,19 +14,19 @@ public class DrawTests (ITestOutputHelper output) [Trait ("Category", "Unicode")] public void CJK_Compatibility_Ideographs_ConsoleWidth_ColumnWidth_Equal_Two () { - const string us = "\U0000f900"; + const string s = "\U0000f900"; var r = (Rune)0xf900; - Assert.Equal ("豈", us); + Assert.Equal ("豈", s); Assert.Equal ("豈", r.ToString ()); - Assert.Equal (us, r.ToString ()); + Assert.Equal (s, r.ToString ()); - Assert.Equal (2, us.GetColumns ()); + Assert.Equal (2, s.GetColumns ()); Assert.Equal (2, r.GetColumns ()); - var win = new Window { Title = us }; + var win = new Window { Title = s }; var view = new View { Text = r.ToString (), Height = Dim.Fill (), Width = Dim.Fill () }; - var tf = new TextField { Text = us, Y = 1, Width = 3 }; + var tf = new TextField { Text = s, Y = 1, Width = 3 }; win.Add (view, tf); Toplevel top = new (); top.Add (win); @@ -36,9 +36,9 @@ public class DrawTests (ITestOutputHelper output) const string expectedOutput = """ - ┌┤豈├────┐ - │豈 │ - │豈 │ + ┌┤豈├────┐ + │豈 │ + │豈 │ └────────┘ """; DriverAssert.AssertDriverContentsWithFrameAre (expectedOutput, output); diff --git a/Tests/UnitTests/View/TextTests.cs b/Tests/UnitTests/View/TextTests.cs index 625d0ce49..dba01c151 100644 --- a/Tests/UnitTests/View/TextTests.cs +++ b/Tests/UnitTests/View/TextTests.cs @@ -1,4 +1,5 @@ -using UnitTests; +using System.Text; +using UnitTests; using Xunit.Abstractions; namespace UnitTests.ViewTests; @@ -699,14 +700,14 @@ w "; string GetContents () { - var text = ""; + var sb = new StringBuilder (); for (var i = 0; i < 4; i++) { - text += Application.Driver?.Contents [0, i].Rune; + sb.Append (Application.Driver?.Contents! [0, i].Grapheme); } - return text; + return sb.ToString (); } Application.End (rs); diff --git a/Tests/UnitTests/Views/LabelTests.cs b/Tests/UnitTests/Views/LabelTests.cs index 7b949c459..bdda95498 100644 --- a/Tests/UnitTests/Views/LabelTests.cs +++ b/Tests/UnitTests/Views/LabelTests.cs @@ -171,7 +171,7 @@ This TextFormatter (tf2) is rewritten. ", [AutoInitShutdown] public void Label_Draw_Horizontal_Simple_Runes () { - var label = new Label { Text = "Demo Simple Rune" }; + var label = new Label { Text = "Demo Simple Text" }; var top = new Toplevel (); top.Add (label); Application.Begin (top); @@ -180,7 +180,7 @@ This TextFormatter (tf2) is rewritten. ", Assert.Equal (new (0, 0, 16, 1), label.Frame); var expected = @" -Demo Simple Rune +Demo Simple Text "; Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); @@ -190,9 +190,9 @@ Demo Simple Rune [Fact] [AutoInitShutdown] - public void Label_Draw_Vertical_Simple_Runes () + public void Label_Draw_Vertical_Simple_Text () { - var label = new Label { TextDirection = TextDirection.TopBottom_LeftRight, Text = "Demo Simple Rune" }; + var label = new Label { TextDirection = TextDirection.TopBottom_LeftRight, Text = "Demo Simple Text" }; var top = new Toplevel (); top.Add (label); Application.Begin (top); @@ -213,10 +213,10 @@ p l e -R -u -n +T e +x +t "; Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); diff --git a/Tests/UnitTests/Views/ProgressBarTests.cs b/Tests/UnitTests/Views/ProgressBarTests.cs index b7bb6aaa2..7f4e7b1b7 100644 --- a/Tests/UnitTests/Views/ProgressBarTests.cs +++ b/Tests/UnitTests/Views/ProgressBarTests.cs @@ -46,57 +46,57 @@ public class ProgressBarTests if (i == 0) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); } else if (i == 1) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); } else if (i == 2) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); } else if (i == 3) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); } else if (i == 4) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); } else if (i == 5) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); } } } @@ -185,687 +185,687 @@ public class ProgressBarTests if (i == 0) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 1) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 2) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 3) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 4) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 5) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 6) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 7) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 8) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 9) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 10) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 11) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 12) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 13) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 14) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } else if (i == 15) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } else if (i == 16) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } else if (i == 17) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } else if (i == 18) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } else if (i == 19) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 20) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 21) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 22) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 23) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 24) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 25) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 26) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 27) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 28) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 29) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 30) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 31) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 32) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 33) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } else if (i == 34) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } else if (i == 35) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } else if (i == 36) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } else if (i == 37) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } } } @@ -894,687 +894,687 @@ public class ProgressBarTests if (i == 0) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 1) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 2) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 3) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 4) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 5) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 6) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 7) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 8) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 9) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 10) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 11) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 12) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 13) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 14) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } else if (i == 15) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } else if (i == 16) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } else if (i == 17) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } else if (i == 18) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } else if (i == 19) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } else if (i == 20) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } else if (i == 21) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } else if (i == 22) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 14].Grapheme); } else if (i == 23) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 24) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 25) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 26) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 27) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 28) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 29) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 30) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 31) { - Assert.Equal ((Rune)' ', driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (" ", driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 32) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 33) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 34) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 35) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 36) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } else if (i == 37) { - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 0].Rune); - Assert.Equal (Glyphs.BlocksMeterSegment, driver.Contents [0, 1].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 2].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 3].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 4].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 5].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 6].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 7].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 8].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 9].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 10].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 11].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 12].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 13].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 14].Rune); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (Glyphs.BlocksMeterSegment.ToString (), driver.Contents [0, 1].Grapheme); + Assert.Equal (" ", driver.Contents [0, 2].Grapheme); + Assert.Equal (" ", driver.Contents [0, 3].Grapheme); + Assert.Equal (" ", driver.Contents [0, 4].Grapheme); + Assert.Equal (" ", driver.Contents [0, 5].Grapheme); + Assert.Equal (" ", driver.Contents [0, 6].Grapheme); + Assert.Equal (" ", driver.Contents [0, 7].Grapheme); + Assert.Equal (" ", driver.Contents [0, 8].Grapheme); + Assert.Equal (" ", driver.Contents [0, 9].Grapheme); + Assert.Equal (" ", driver.Contents [0, 10].Grapheme); + Assert.Equal (" ", driver.Contents [0, 11].Grapheme); + Assert.Equal (" ", driver.Contents [0, 12].Grapheme); + Assert.Equal (" ", driver.Contents [0, 13].Grapheme); + Assert.Equal (" ", driver.Contents [0, 14].Grapheme); } } } diff --git a/Tests/UnitTests/Views/TextFieldTests.cs b/Tests/UnitTests/Views/TextFieldTests.cs index 922178409..d41de3152 100644 --- a/Tests/UnitTests/Views/TextFieldTests.cs +++ b/Tests/UnitTests/Views/TextFieldTests.cs @@ -95,6 +95,7 @@ public class TextFieldTests (ITestOutputHelper output) Assert.Equal (11, caption.Length); Assert.Equal (10, caption.EnumerateRunes ().Sum (c => c.GetColumns ())); + Assert.Equal (10, caption.GetColumns ()); TextField tf = GetTextFieldsInView (); diff --git a/Tests/UnitTests/Views/TextViewTests.cs b/Tests/UnitTests/Views/TextViewTests.cs index 5d03983eb..80f185fb4 100644 --- a/Tests/UnitTests/Views/TextViewTests.cs +++ b/Tests/UnitTests/Views/TextViewTests.cs @@ -6922,7 +6922,7 @@ line. { string [] lines = _textView.Text.Split (Environment.NewLine); - if (lines == null || lines.Length == 0) + if (lines is { Length: 0 }) { return 0; } @@ -7034,11 +7034,11 @@ line. List> text = [ Cell.ToCells ( - "This is the first line.".ToRunes () + "This is the first line.".ToStringList () ), Cell.ToCells ( - "This is the second line.".ToRunes () + "This is the second line.".ToStringList () ) ]; TextView tv = CreateTextView (); @@ -7101,12 +7101,9 @@ line. ", { string csName = color.Key; - foreach (Rune rune in csName.EnumerateRunes ()) - { - cells.Add (new () { Rune = rune, Attribute = color.Value.Normal }); - } + cells.AddRange (Cell.ToCellList (csName, color.Value.Normal)); - cells.Add (new () { Rune = (Rune)'\n', Attribute = color.Value.Focus }); + cells.Add (new () { Grapheme = "\n", Attribute = color.Value.Focus }); } TextView tv = CreateTextView (); diff --git a/Tests/UnitTests/Views/TreeViewTests.cs b/Tests/UnitTests/Views/TreeViewTests.cs index f57330054..0e115751b 100644 --- a/Tests/UnitTests/Views/TreeViewTests.cs +++ b/Tests/UnitTests/Views/TreeViewTests.cs @@ -979,10 +979,10 @@ public class TreeViewTests (ITestOutputHelper output) Assert.All (eventArgs, ea => Assert.Equal (ea.Tree, tv)); Assert.All (eventArgs, ea => Assert.False (ea.Handled)); - Assert.Equal ("├-root one", eventArgs [0].Cells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); - Assert.Equal ("│ ├─leaf 1", eventArgs [1].Cells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); - Assert.Equal ("│ └─leaf 2", eventArgs [2].Cells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); - Assert.Equal ("└─root two", eventArgs [3].Cells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); + Assert.Equal ("├-root one", eventArgs [0].Cells.Aggregate ("", (s, n) => s += n.Grapheme).TrimEnd ()); + Assert.Equal ("│ ├─leaf 1", eventArgs [1].Cells.Aggregate ("", (s, n) => s += n.Grapheme).TrimEnd ()); + Assert.Equal ("│ └─leaf 2", eventArgs [2].Cells.Aggregate ("", (s, n) => s += n.Grapheme).TrimEnd ()); + Assert.Equal ("└─root two", eventArgs [3].Cells.Aggregate ("", (s, n) => s += n.Grapheme).TrimEnd ()); Assert.Equal (1, eventArgs [0].IndexOfExpandCollapseSymbol); Assert.Equal (3, eventArgs [1].IndexOfExpandCollapseSymbol); @@ -1092,9 +1092,9 @@ oot two Assert.All (eventArgs, ea => Assert.Equal (ea.Tree, tv)); Assert.All (eventArgs, ea => Assert.False (ea.Handled)); - Assert.Equal ("─leaf 1", eventArgs [0].Cells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); - Assert.Equal ("─leaf 2", eventArgs [1].Cells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); - Assert.Equal ("oot two", eventArgs [2].Cells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); + Assert.Equal ("─leaf 1", eventArgs [0].Cells.Aggregate ("", (s, n) => s += n.Grapheme).TrimEnd ()); + Assert.Equal ("─leaf 2", eventArgs [1].Cells.Aggregate ("", (s, n) => s += n.Grapheme).TrimEnd ()); + Assert.Equal ("oot two", eventArgs [2].Cells.Aggregate ("", (s, n) => s += n.Grapheme).TrimEnd ()); Assert.Equal (0, eventArgs [0].IndexOfExpandCollapseSymbol); Assert.Equal (0, eventArgs [1].IndexOfExpandCollapseSymbol); diff --git a/Tests/UnitTestsParallelizable/Drawing/CellTests.cs b/Tests/UnitTestsParallelizable/Drawing/CellTests.cs index 13a088a13..b51ab37a9 100644 --- a/Tests/UnitTestsParallelizable/Drawing/CellTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/CellTests.cs @@ -1,17 +1,39 @@ using System.Text; -using Xunit.Abstractions; namespace UnitTests_Parallelizable.DrawingTests; -public class CellTests () +public class CellTests { [Fact] public void Constructor_Defaults () { var c = new Cell (); Assert.True (c is { }); - Assert.Equal (0, c.Rune.Value); + Assert.Empty (c.Runes); Assert.Null (c.Attribute); + Assert.False (c.IsDirty); + Assert.Null (c.Grapheme); + } + + [Theory] + [InlineData (null, new uint [] { })] + [InlineData ("", new uint [] { })] + [InlineData ("a", new uint [] { 0x0061 })] + [InlineData ("👩‍❤️‍💋‍👨", new uint [] { 0x1F469, 0x200D, 0x2764, 0xFE0F, 0x200D, 0x1F48B, 0x200D, 0x1F468 })] + [InlineData ("æ", new uint [] { 0x00E6 })] + [InlineData ("a︠", new uint [] { 0x0061, 0xFE20 })] + [InlineData ("e︡", new uint [] { 0x0065, 0xFE21 })] + public void Runes_From_Grapheme (string grapheme, uint [] expected) + { + // Arrange + var c = new Cell { Grapheme = grapheme }; + + // Act + Rune [] runes = expected.Select (u => new Rune (u)).ToArray (); + + // Assert + Assert.Equal (grapheme, c.Grapheme); + Assert.Equal (runes, c.Runes); } [Fact] @@ -21,32 +43,138 @@ public class CellTests () var c2 = new Cell { - Rune = new ('a'), Attribute = new (Color.Red) + Grapheme = "a", Attribute = new (Color.Red) }; Assert.False (c1.Equals (c2)); Assert.False (c2.Equals (c1)); - c1.Rune = new ('a'); + c1.Grapheme = "a"; c1.Attribute = new (); - Assert.Equal (c1.Rune, c2.Rune); + Assert.Equal (c1.Grapheme, c2.Grapheme); Assert.False (c1.Equals (c2)); Assert.False (c2.Equals (c1)); } [Fact] - public void ToString_Override () + public void Set_Text_With_Invalid_Grapheme_Throws () { - var c1 = new Cell (); - - var c2 = new Cell - { - Rune = new ('a'), Attribute = new (Color.Red) - }; - Assert.Equal ("['\0':]", c1.ToString ()); - - Assert.Equal ( - "['a':[Red,Red,None]]", - c2.ToString () - ); + Assert.Throws (() => new Cell { Grapheme = "ab" }); + Assert.Throws (() => new Cell { Grapheme = "\u0061\u0062" }); // ab } + + [Theory] + [MemberData (nameof (ToStringTestData))] + public void ToString_Override (string text, Attribute? attribute, string expected) + { + var c = new Cell (attribute, true, text); + string result = c.ToString (); + + Assert.Equal (expected, result); + } + + public static IEnumerable ToStringTestData () + { + yield return ["", null, "[\"\":]"]; + yield return ["a", null, "[\"a\":]"]; + yield return ["\t", null, "[\"\\t\":]"]; + yield return ["\r", null, "[\"\\r\":]"]; + yield return ["\n", null, "[\"\\n\":]"]; + yield return ["\r\n", null, "[\"\\r\\n\":]"]; + yield return ["\f", null, "[\"\\f\":]"]; + yield return ["\v", null, "[\"\\v\":]"]; + yield return ["\x1B", null, "[\"\\u001B\":]"]; + yield return ["\\", new Attribute (Color.Blue), "[\"\\\":[Blue,Blue,None]]"]; + yield return ["😀", null, "[\"😀\":]"]; + yield return ["👨‍👩‍👦‍👦", null, "[\"👨‍👩‍👦‍👦\":]"]; + yield return ["A", new Attribute (Color.Red) { Style = TextStyle.Blink }, "[\"A\":[Red,Red,Blink]]"]; + yield return ["\U0001F469\u200D\u2764\uFE0F\u200D\U0001F48B\u200D\U0001F468", null, "[\"👩‍❤️‍💋‍👨\":]"]; + } + + [Fact] + public void Graphemes_Decomposed_Normalize () + { + Cell c1 = new () + { + // 'e' + '◌́' COMBINING ACUTE ACCENT (U+0301) + Grapheme = "e\u0301" // visually "é" + }; + + Cell c2 = new () + { + // NFC single code point (U+00E9) + Grapheme = "é" + }; + + // Validation + Assert.Equal ("é", c1.Grapheme); // Proper normalized grapheme + Assert.Equal (c1.Grapheme, c2.Grapheme); + Assert.Equal (c1.Runes.Count, c2.Runes.Count); + Assert.Equal (new (0x00E9), c2.Runes [0]); + } + + [Fact] + public void Cell_IsDirty_Flag_Works () + { + var c = new Cell (); + Assert.False (c.IsDirty); + c.IsDirty = true; + Assert.True (c.IsDirty); + c.IsDirty = false; + Assert.False (c.IsDirty); + } + + [Theory] + [InlineData ("\uFDD0", false)] + [InlineData ("\uFDEF", false)] + [InlineData ("\uFFFE", true)] + [InlineData ("\uFFFF", false)] + [InlineData ("\U0001FFFE", false)] + [InlineData ("\U0001FFFF", false)] + [InlineData ("\U0010FFFE", false)] + [InlineData ("\U0010FFFF", false)] + public void IsNormalized_ArgumentException (string text, bool throws) + { + try + { + bool normalized = text.IsNormalized (NormalizationForm.FormC); + + Assert.True (normalized); + Assert.False (throws); + } + catch (ArgumentException) + { + Assert.True (throws); + } + + Assert.Null (Record.Exception (() => new Cell { Grapheme = text })); + } + + [Fact] + public void Surrogate_Normalize_Throws_And_Cell_Setter_Throws () + { + // Create the lone high surrogate at runtime (safe) + string s = new string ((char)0xD800, 1); + + // Confirm the runtime string actually contains the surrogate + Assert.Equal (0xD800, s [0]); + + // Normalize should throw + Assert.Throws (() => s.Normalize (NormalizationForm.FormC)); + + // And if your Grapheme setter normalizes, assignment should throw as well + Assert.Throws (() => new Cell () { Grapheme = s }); + + // Create the lone low surrogate at runtime (safe) + s = new string ((char)0xDC00, 1); + + // Confirm the runtime string actually contains the surrogate + Assert.Equal (0xDC00, s [0]); + + // Normalize should throw + Assert.Throws (() => s.Normalize (NormalizationForm.FormC)); + + // And if your Grapheme setter normalizes, assignment should throw as well + Assert.Throws (() => new Cell () { Grapheme = s }); + } + } diff --git a/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs b/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs index 2dd2fb769..4b4205791 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs @@ -19,7 +19,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase driver.Rows = 25; driver.Cols = 80; driver.AddRune (new Rune ('a')); - Assert.Equal ((Rune)'a', driver.Contents [0, 0].Rune); + Assert.Equal ("a", driver.Contents [0, 0].Grapheme); driver.End (); } @@ -29,28 +29,30 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase { IDriver driver = CreateFakeDriver (); - var expected = new Rune ('ắ'); + var expected = "ắ"; var text = "\u1eaf"; driver.AddStr (text); - Assert.Equal (expected, driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); + Assert.Equal (expected, driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); driver.ClearContents (); driver.Move (0, 0); + expected = "ắ"; text = "\u0103\u0301"; driver.AddStr (text); - Assert.Equal (expected, driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); + Assert.Equal (expected, driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); driver.ClearContents (); driver.Move (0, 0); + expected = "ắ"; text = "\u0061\u0306\u0301"; driver.AddStr (text); - Assert.Equal (expected, driver.Contents [0, 0].Rune); - Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); + Assert.Equal (expected, driver.Contents [0, 0].Grapheme); + Assert.Equal (" ", driver.Contents [0, 1].Grapheme); // var s = "a\u0301\u0300\u0306"; @@ -86,7 +88,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase { for (var row = 0; row < driver.Rows; row++) { - Assert.Equal ((Rune)' ', driver.Contents [row, col].Rune); + Assert.Equal (" ", driver.Contents [row, col].Grapheme); } } @@ -99,12 +101,12 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase IDriver driver = CreateFakeDriver (); driver.AddRune ('a'); - Assert.Equal ((Rune)'a', driver.Contents [0, 0].Rune); + Assert.Equal ("a", driver.Contents [0, 0].Grapheme); Assert.Equal (0, driver.Row); Assert.Equal (1, driver.Col); driver.AddRune ('b'); - Assert.Equal ((Rune)'b', driver.Contents [0, 1].Rune); + Assert.Equal ("b", driver.Contents [0, 1].Grapheme); Assert.Equal (0, driver.Row); Assert.Equal (2, driver.Col); @@ -116,7 +118,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase // Add a rune to the last column of the first row; should increment the row or col even though it's now invalid driver.AddRune ('c'); - Assert.Equal ((Rune)'c', driver.Contents [0, lastCol].Rune); + Assert.Equal ("c", driver.Contents [0, lastCol].Grapheme); Assert.Equal (lastCol + 1, driver.Col); // Add a rune; should succeed but do nothing as it's outside of Contents @@ -127,7 +129,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase { for (var row = 0; row < driver.Rows; row++) { - Assert.NotEqual ((Rune)'d', driver.Contents [row, col].Rune); + Assert.NotEqual ("d", driver.Contents [row, col].Grapheme); } } @@ -146,12 +148,12 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase Assert.Equal (2, rune.GetColumns ()); driver.AddRune (rune); - Assert.Equal (rune, driver.Contents [0, 0].Rune); + Assert.Equal (rune.ToString (), driver.Contents [0, 0].Grapheme); Assert.Equal (0, driver.Row); Assert.Equal (2, driver.Col); //driver.AddRune ('b'); - //Assert.Equal ((Rune)'b', driver.Contents [0, 1].Rune); + //Assert.Equal ((Text)'b', driver.Contents [0, 1].Text); //Assert.Equal (0, driver.Row); //Assert.Equal (2, driver.Col); @@ -163,7 +165,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase //// Add a rune to the last column of the first row; should increment the row or col even though it's now invalid //driver.AddRune ('c'); - //Assert.Equal ((Rune)'c', driver.Contents [0, lastCol].Rune); + //Assert.Equal ((Text)'c', driver.Contents [0, lastCol].Text); //Assert.Equal (lastCol + 1, driver.Col); //// Add a rune; should succeed but do nothing as it's outside of Contents @@ -171,7 +173,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase //Assert.Equal (lastCol + 2, driver.Col); //for (var col = 0; col < driver.Cols; col++) { // for (var row = 0; row < driver.Rows; row++) { - // Assert.NotEqual ((Rune)'d', driver.Contents [row, col].Rune); + // Assert.NotEqual ((Text)'d', driver.Contents [row, col].Text); // } //} diff --git a/Tests/UnitTestsParallelizable/Drivers/ContentsTests.cs b/Tests/UnitTestsParallelizable/Drivers/ContentsTests.cs index ceaccc042..b900ad206 100644 --- a/Tests/UnitTestsParallelizable/Drivers/ContentsTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/ContentsTests.cs @@ -36,7 +36,7 @@ public class ContentsTests (ITestOutputHelper output) : FakeDriverBase // a + ogonek + acute = ( ą́ ) var oGonek = new Rune (0x0328); // Combining ogonek (a small hook or comma shape) combined = "a" + oGonek + acuteAccent; - expected = ("a" + oGonek).Normalize (NormalizationForm.FormC); // See Issue #2616 + expected = ("a" + oGonek + acuteAccent).Normalize (NormalizationForm.FormC); // See Issue #2616 driver.Move (0, 0); driver.AddStr (combined); @@ -44,7 +44,7 @@ public class ContentsTests (ITestOutputHelper output) : FakeDriverBase // e + ogonek + acute = ( ę́́ ) combined = "e" + oGonek + acuteAccent; - expected = ("e" + oGonek).Normalize (NormalizationForm.FormC); // See Issue #2616 + expected = ("e" + oGonek + acuteAccent).Normalize (NormalizationForm.FormC); // See Issue #2616 driver.Move (0, 0); driver.AddStr (combined); @@ -52,7 +52,7 @@ public class ContentsTests (ITestOutputHelper output) : FakeDriverBase // i + ogonek + acute = ( į́́́ ) combined = "i" + oGonek + acuteAccent; - expected = ("i" + oGonek).Normalize (NormalizationForm.FormC); // See Issue #2616 + expected = ("i" + oGonek + acuteAccent).Normalize (NormalizationForm.FormC); // See Issue #2616 driver.Move (0, 0); driver.AddStr (combined); @@ -60,7 +60,7 @@ public class ContentsTests (ITestOutputHelper output) : FakeDriverBase // u + ogonek + acute = ( ų́́́́ ) combined = "u" + oGonek + acuteAccent; - expected = ("u" + oGonek).Normalize (NormalizationForm.FormC); // See Issue #2616 + expected = ("u" + oGonek + acuteAccent).Normalize (NormalizationForm.FormC); // See Issue #2616 driver.Move (0, 0); driver.AddStr (combined); diff --git a/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs b/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs index 9f87d80d0..511c7e12f 100644 --- a/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs @@ -1,6 +1,52 @@ using UnitTests; +using Xunit.Abstractions; namespace UnitTests_Parallelizable.DriverTests; public class DriverTests : FakeDriverBase -{ } +{ + [Theory] + [InlineData (null, true)] + [InlineData ("", true)] + [InlineData ("a", true)] + [InlineData ("👩‍❤️‍💋‍👨", false)] + public void IsValidLocation (string text, bool positive) + { + IDriver driver = CreateFakeDriver (); + driver.SetScreenSize (10, 10); + + // positive + Assert.True (driver.IsValidLocation (text, 0, 0)); + Assert.True (driver.IsValidLocation (text, 1, 1)); + Assert.Equal (positive, driver.IsValidLocation (text, driver.Cols - 1, driver.Rows - 1)); + + // negative + Assert.False (driver.IsValidLocation (text, -1, 0)); + Assert.False (driver.IsValidLocation (text, 0, -1)); + Assert.False (driver.IsValidLocation (text, -1, -1)); + Assert.False (driver.IsValidLocation (text, driver.Cols, driver.Rows - 1)); + Assert.False (driver.IsValidLocation (text, driver.Cols, driver.Rows - 1)); + Assert.False (driver.IsValidLocation (text, driver.Cols, driver.Rows)); + + // Define a clip rectangle + driver.Clip = new (new Rectangle (5, 5, 5, 5)); + + // positive + Assert.True (driver.IsValidLocation (text, 5, 5)); + Assert.Equal (positive, driver.IsValidLocation (text, 9, 9)); + + // negative + Assert.False (driver.IsValidLocation (text, 4, 5)); + Assert.False (driver.IsValidLocation (text, 5, 4)); + Assert.False (driver.IsValidLocation (text, 10, 9)); + Assert.False (driver.IsValidLocation (text, 9, 10)); + Assert.False (driver.IsValidLocation (text, -1, 0)); + Assert.False (driver.IsValidLocation (text, 0, -1)); + Assert.False (driver.IsValidLocation (text, -1, -1)); + Assert.False (driver.IsValidLocation (text, driver.Cols, driver.Rows - 1)); + Assert.False (driver.IsValidLocation (text, driver.Cols, driver.Rows - 1)); + Assert.False (driver.IsValidLocation (text, driver.Cols, driver.Rows)); + + driver.End (); + } +} diff --git a/Tests/UnitTestsParallelizable/Drivers/FakeDriverTests.cs b/Tests/UnitTestsParallelizable/Drivers/FakeDriverTests.cs index 251a26344..30ba27be9 100644 --- a/Tests/UnitTestsParallelizable/Drivers/FakeDriverTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/FakeDriverTests.cs @@ -155,7 +155,7 @@ public class FakeDriverTests (ITestOutputHelper output) : FakeDriverBase { for (int col = rect.X; col < rect.X + rect.Width; col++) { - Assert.Equal ((Rune)'X', driver.Contents [row, col].Rune); + Assert.Equal ("X", driver.Contents [row, col].Grapheme); } } } diff --git a/Tests/UnitTestsParallelizable/Text/RuneTests.cs b/Tests/UnitTestsParallelizable/Text/RuneTests.cs index 34214d6ef..4e03e8048 100644 --- a/Tests/UnitTestsParallelizable/Text/RuneTests.cs +++ b/Tests/UnitTestsParallelizable/Text/RuneTests.cs @@ -88,7 +88,7 @@ public class RuneTests 1 )] // the letters 법 join to form the Korean word for "rice:" U+BC95 법 (read from top left to bottom right) [InlineData ("\U0001F468\u200D\U0001F469\u200D\U0001F467", "👨‍👩‍👧", 8, 2, 8)] // Man, Woman and Girl emoji. - [InlineData ("\u0915\u093f", "कि", 2, 1, 2)] // Hindi कि with DEVANAGARI LETTER KA and DEVANAGARI VOWEL SIGN I + //[InlineData ("\u0915\u093f", "कि", 2, 2, 2)] // Hindi कि with DEVANAGARI LETTER KA and DEVANAGARI VOWEL SIGN I [InlineData ( "\u0e4d\u0e32", "ํา", @@ -213,7 +213,7 @@ public class RuneTests [InlineData ( '\u1161', "ᅡ", - 1, + 0, 1, 3 )] // ᅡ Hangul Jungseong A - Unicode Hangul Jamo for join with column width equal to 0 alone. @@ -231,7 +231,7 @@ public class RuneTests )] // ䷀Hexagram For The Creative Heaven - U+4dc0 - https://github.com/microsoft/terminal/blob/main/src/types/unicode_width_overrides.xml // See https://github.com/microsoft/terminal/issues/19389 - [InlineData ('\ud7b0', "ힰ", 1, 1, 3)] // ힰ ┤Hangul Jungseong O-Yeo - ힰ U+d7b0')] + [InlineData ('\ud7b0', "ힰ", 0, 1, 3)] // ힰ ┤Hangul Jungseong O-Yeo - ힰ U+d7b0')] [InlineData ('\uf61e', "", 1, 1, 3)] // Private Use Area [InlineData ('\u23f0', "⏰", 2, 1, 3)] // Alarm Clock - ⏰ U+23f0 [InlineData ('\u1100', "ᄀ", 2, 1, 3)] // ᄀ Hangul Choseong Kiyeok @@ -365,6 +365,42 @@ public class RuneTests [InlineData ('\ud801')] public void Rune_Exceptions_Integers (int code) { Assert.Throws (() => new Rune (code)); } + [Theory] + // Control characters (should be mapped to Control Pictures) + [InlineData ('\u0000', 0x2400)] // NULL → ␀ + [InlineData ('\u0009', 0x2409)] // TAB → ␉ + [InlineData ('\u000A', 0x240A)] // LF → ␊ + [InlineData ('\u000D', 0x240D)] // CR → ␍ + + // Printable characters (should remain unchanged) + [InlineData ('A', 'A')] + [InlineData (' ', ' ')] + [InlineData ('~', '~')] + public void MakePrintable_ReturnsExpected (char inputChar, int expectedCodePoint) + { + // Arrange + Rune input = new Rune (inputChar); + + // Act + Rune result = input.MakePrintable (); + + // Assert + Assert.Equal (expectedCodePoint, result.Value); + } + + [Fact] + public void MakePrintable_SupplementaryRune_RemainsUnchanged () + { + // Arrange: supplementary character outside BMP (not a control) + Rune input = new Rune (0x1F600); // 😀 grinning face emoji + + // Act + Rune result = input.MakePrintable (); + + // Assert + Assert.Equal (input.Value, result.Value); + } + [Theory] [InlineData (new [] { '\ud799', '\udc21' })] public void Rune_Exceptions_Utf16_Encode (char [] code) @@ -954,11 +990,9 @@ public class RuneTests Assert.Equal (runeCount, us.GetRuneCount ()); Assert.Equal (stringCount, s.Length); - TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator (s); - var textElementCount = 0; - while (enumerator.MoveNext ()) + foreach (string _ in GraphemeHelper.GetGraphemes (s)) { textElementCount++; // For versions prior to Net5.0 the StringInfo class might handle some grapheme clusters incorrectly. } @@ -1064,4 +1098,95 @@ public class RuneTests return true; } + + [Theory] + [InlineData (0x0041, new byte [] { 0x41 })] // 'A', ASCII + [InlineData (0x00E9, new byte [] { 0xC3, 0xA9 })] // 'é', 2-byte UTF-8 + [InlineData (0x20AC, new byte [] { 0xE2, 0x82, 0xAC })] // '€', 3-byte UTF-8 + [InlineData (0x1F600, new byte [] { 0xF0, 0x9F, 0x98, 0x80 })] // 😀 emoji, 4-byte UTF-8 + public void Encode_WritesExpectedBytes (int codePoint, byte [] expectedBytes) + { + // Arrange + Rune rune = new Rune (codePoint); + byte [] buffer = new byte [10]; // extra space + for (int i = 0; i < buffer.Length; i++) + { + buffer [i] = 0xFF; + } + + // Act + int written = rune.Encode (buffer); + + // Assert + Assert.Equal (expectedBytes.Length, written); + for (int i = 0; i < written; i++) + { + Assert.Equal (expectedBytes [i], buffer [i]); + } + } + + [Fact] + public void Encode_WithStartAndCount_WritesPartialBytes () + { + // Arrange: U+1F600 😀 (4 bytes) + Rune rune = new Rune (0x1F600); + byte [] buffer = new byte [10]; + for (int i = 0; i < buffer.Length; i++) + { + buffer [i] = 0xFF; + } + + // Act: write starting at index 2, limit count to 2 bytes + int written = rune.Encode (buffer, start: 2, count: 2); + + // Assert + Assert.Equal (2, written); + // Original UTF-8 bytes: F0 9F 98 80 + Assert.Equal (0xF0, buffer [2]); + Assert.Equal (0x9F, buffer [3]); + // Remaining buffer untouched + Assert.Equal (0xFF, buffer [0]); + Assert.Equal (0xFF, buffer [1]); + Assert.Equal (0xFF, buffer [4]); + } + + [Fact] + public void Encode_WithCountGreaterThanRuneBytes_WritesAllBytes () + { + // Arrange: é → C3 A9 + Rune rune = new Rune ('é'); + byte [] buffer = new byte [10]; + for (int i = 0; i < buffer.Length; i++) + { + buffer [i] = 0xFF; + } + + // Act: count larger than needed + int written = rune.Encode (buffer, start: 1, count: 10); + + // Assert + Assert.Equal (2, written); + Assert.Equal (0xC3, buffer [1]); + Assert.Equal (0xA9, buffer [2]); + Assert.Equal (0xFF, buffer [3]); // next byte untouched + } + + [Fact] + public void Encode_ZeroCount_WritesNothing () + { + Rune rune = new Rune ('A'); + byte [] buffer = new byte [5]; + for (int i = 0; i < buffer.Length; i++) + { + buffer [i] = 0xFF; + } + + int written = rune.Encode (buffer, start: 0, count: 0); + + Assert.Equal (0, written); + foreach (var b in buffer) + { + Assert.Equal (0xFF, b); // buffer untouched + } + } } diff --git a/Tests/UnitTestsParallelizable/Text/StringTests.cs b/Tests/UnitTestsParallelizable/Text/StringTests.cs index 80c52b96e..a3c4e52ba 100644 --- a/Tests/UnitTestsParallelizable/Text/StringTests.cs +++ b/Tests/UnitTestsParallelizable/Text/StringTests.cs @@ -4,6 +4,13 @@ public class StringTests { + [Fact] + public void TestGetColumns_Null () + { + string? str = null; + Assert.Equal (0, str!.GetColumns ()); + } + [Fact] public void TestGetColumns_Empty () { @@ -11,6 +18,20 @@ public class StringTests Assert.Equal (0, str.GetColumns ()); } + [Fact] + public void TestGetColumns_SingleRune () + { + var str = "a"; + Assert.Equal (1, str.GetColumns ()); + } + + [Fact] + public void TestGetColumns_Zero_Width () + { + var str = "\u200D"; + Assert.Equal (0, str.GetColumns ()); + } + [Theory] [InlineData ("a", 1)] [InlineData ("á", 1)] @@ -30,39 +51,37 @@ public class StringTests // Test known wide codepoints [Theory] - [InlineData ("🙂", 2)] - [InlineData ("a🙂", 3)] - [InlineData ("🙂a", 3)] - [InlineData ("👨‍👩‍👦‍👦", 2)] - [InlineData ("👨‍👩‍👦‍👦🙂", 4)] - [InlineData ("👨‍👩‍👦‍👦🙂a", 5)] - [InlineData ("👨‍👩‍👦‍👦a🙂", 5)] - [InlineData ("👨‍👩‍👦‍👦👨‍👩‍👦‍👦", 4)] - [InlineData ("山", 2)] // The character for "mountain" in Chinese/Japanese/Korean (山), Unicode U+5C71 - [InlineData ("山🙂", 4)] // The character for "mountain" in Chinese/Japanese/Korean (山), Unicode U+5C71 - //[InlineData ("\ufe20\ufe21", 2)] // Combining Ligature Left Half ︠ - U+fe20 -https://github.com/microsoft/terminal/blob/main/src/types/unicode_width_overrides.xml - // // Combining Ligature Right Half - U+fe21 -https://github.com/microsoft/terminal/blob/main/src/types/unicode_width_overrides.xml - public void TestGetColumns_MultiRune_WideBMP (string str, int expected) { Assert.Equal (expected, str.GetColumns ()); } - - [Fact] - public void TestGetColumns_Null () + [InlineData ("🙂", 2, 1, 2)] + [InlineData ("a🙂", 3, 2, 3)] + [InlineData ("🙂a", 3, 2, 3)] + [InlineData ("👨‍👩‍👦‍👦", 8, 1, 2)] + [InlineData ("👨‍👩‍👦‍👦🙂", 10, 2, 4)] + [InlineData ("👨‍👩‍👦‍👦🙂a", 11, 3, 5)] + [InlineData ("👨‍👩‍👦‍👦a🙂", 11, 3, 5)] + [InlineData ("👨‍👩‍👦‍👦👨‍👩‍👦‍👦", 16, 2, 4)] + [InlineData ("าำ", 2, 1, 2)] // า U+0E32 - THAI CHARACTER SARA AA with ำ U+0E33 - THAI CHARACTER SARA AM + [InlineData ("山", 2, 1, 2)] // The character for "mountain" in Chinese/Japanese/Korean (山), Unicode U+5C71 + [InlineData ("山🙂", 4, 2, 4)] // The character for "mountain" in Chinese/Japanese/Korean (山), Unicode U+5C71 + [InlineData ("a\ufe20e\ufe21", 2, 2, 2)] // Combining Ligature Left Half ︠ - U+fe20 -https://github.com/microsoft/terminal/blob/main/src/types/unicode_width_overrides.xml + // Combining Ligature Right Half - U+fe21 -https://github.com/microsoft/terminal/blob/main/src/types/unicode_width_overrides.xml + //[InlineData ("क", 1, 1, 1)] // क U+0915 Devanagari Letter Ka + //[InlineData ("ि", 1, 1, 1)] // U+093F Devanagari Vowel Sign I ि (i-kar). + //[InlineData ("कि", 2, 1, 2)] // "कि" is U+0915 for the base consonant "क" with U+093F for the vowel sign "ि" (i-kar). + [InlineData ("ᄀ", 2, 1, 2)] // ᄀ U+1100 HANGUL CHOSEONG KIYEOK (consonant) + [InlineData ("ᅡ", 0, 1, 0)] // ᅡ U+1161 HANGUL JUNGSEONG A (vowel) + [InlineData ("가", 2, 1, 2)] // ᄀ U+1100 HANGUL CHOSEONG KIYEOK (consonant) with ᅡ U+1161 HANGUL JUNGSEONG A (vowel) + [InlineData ("ᄒ", 2, 1, 2)] // ᄒ U+1112 Hangul Choseong Hieuh + [InlineData ("ᅵ", 0, 1, 0)] // ᅵ U+1175 Hangul Jungseong I + [InlineData ("ᇂ", 0, 1, 0)] // ᇂ U+11C2 Hangul Jongseong Hieuh + [InlineData ("힣", 2, 1, 2)] // ᄒ (choseong h) + ᅵ (jungseong i) + ᇂ (jongseong h) + [InlineData ("ힰ", 0, 1, 0)] // U+D7B0 ힰ Hangul Jungseong O-Yeo + [InlineData ("ᄀힰ", 2, 1, 2)] // ᄀ U+1100 HANGUL CHOSEONG KIYEOK (consonant) with U+D7B0 ힰ Hangul Jungseong O-Yeo + //[InlineData ("षि", 2, 1, 2)] // U+0937 ष DEVANAGARI LETTER SSA with U+093F ि COMBINING DEVANAGARI VOWEL SIGN I + public void TestGetColumns_MultiRune_WideBMP_Graphemes (string str, int expectedRunesWidth, int expectedGraphemesCount, int expectedWidth) { - string? str = null; - Assert.Equal (0, str!.GetColumns ()); - } - - [Fact] - public void TestGetColumns_SingleRune () - { - var str = "a"; - Assert.Equal (1, str.GetColumns ()); - } - - [Fact] - public void TestGetColumns_Zero_Width () - { - var str = "\u200D"; - Assert.Equal (0, str.GetColumns ()); + Assert.Equal (expectedRunesWidth, str.EnumerateRunes ().Sum (r => r.GetColumns ())); + Assert.Equal (expectedGraphemesCount, GraphemeHelper.GetGraphemes (str).ToArray ().Length); + Assert.Equal (expectedWidth, str.GetColumns ()); } [Theory] @@ -70,13 +89,124 @@ public class StringTests [InlineData ("")] public void TestGetColumns_Does_Not_Throws_With_Null_And_Empty_String (string? text) { - if (text is null) + // ReSharper disable once InvokeAsExtensionMethod + Assert.Equal (0, StringExtensions.GetColumns (text!)); + } + + public class ReadOnlySpanExtensionsTests + { + [Theory] + [InlineData ("12345", true)] // all ASCII digits + [InlineData ("0", true)] // single ASCII digit + [InlineData ("", false)] // empty span + [InlineData ("12a45", false)] // contains a letter + [InlineData ("123", false)] // full-width Unicode digits (not ASCII) + [InlineData ("12 34", false)] // contains space + [InlineData ("١٢٣", false)] // Arabic-Indic digits + public void IsAllAsciiDigits_WorksAsExpected (string input, bool expected) { - Assert.Equal (0, StringExtensions.GetColumns (text!)); - } - else - { - Assert.Equal (0, text.GetColumns ()); + // Arrange + ReadOnlySpan span = input.AsSpan (); + + // Act + bool result = span.IsAllAsciiDigits (); + + // Assert + Assert.Equal (expected, result); } } + + [Theory] + [InlineData ("0", true)] + [InlineData ("9", true)] + [InlineData ("A", true)] + [InlineData ("F", true)] + [InlineData ("a", true)] + [InlineData ("f", true)] + [InlineData ("123ABC", true)] + [InlineData ("abcdef", true)] + [InlineData ("G", false)] // 'G' not hex + [InlineData ("Z9", false)] // 'Z' not hex + [InlineData ("12 34", false)] // space not hex + [InlineData ("", false)] // empty string + [InlineData ("123", false)] // full-width digits, not ASCII + [InlineData ("0xFF", false)] // includes 'x' + public void IsAllAsciiHexDigits_ReturnsExpected (string input, bool expected) + { + // Arrange + ReadOnlySpan span = input.AsSpan (); + + // Act + bool result = span.IsAllAsciiHexDigits (); + + // Assert + Assert.Equal (expected, result); + } + + [Theory] + [MemberData (nameof (GetStringConcatCases))] + public void ToString_ReturnsExpected (IEnumerable input, string expected) + { + // Act + string result = StringExtensions.ToString (input); + + // Assert + Assert.Equal (expected, result); + } + + public static IEnumerable GetStringConcatCases () + { + yield return [new string [] { }, string.Empty]; // Empty sequence + yield return [new [] { "" }, string.Empty]; // Single empty string + yield return [new [] { "A" }, "A"]; // Single element + yield return [new [] { "A", "B" }, "AB"]; // Simple concatenation + yield return [new [] { "Hello", " ", "World" }, "Hello World"]; // Multiple parts + yield return [new [] { "123", "456", "789" }, "123456789"]; // Numeric strings + yield return [new [] { "👩‍", "🧒" }, "👩‍🧒"]; // Grapheme sequence + yield return [new [] { "α", "β", "γ" }, "αβγ"]; // Unicode letters + yield return [new [] { "A", null, "B" }, "AB"]; // Null ignored by string.Concat + } + + [Theory] + [InlineData ("", false)] // Empty string + [InlineData ("A", false)] // Single BMP character + [InlineData ("AB", false)] // Two BMP chars, not a surrogate pair + [InlineData ("👩", true)] // Single emoji surrogate pair (U+1F469) + [InlineData ("🧒", true)] // Another emoji surrogate pair (U+1F9D2) + [InlineData ("𐍈", true)] // Gothic letter hwair (U+10348) + [InlineData ("A👩", false)] // One BMP + one surrogate half + [InlineData ("👩‍", false)] // Surrogate pair + ZWJ (length != 2) + public void IsSurrogatePair_ReturnsExpected (string input, bool expected) + { + // Act + bool result = input.IsSurrogatePair (); + + // Assert + Assert.Equal (expected, result); + } + + [Theory] + // Control characters (should be replaced with the "Control Pictures" block) + [InlineData ("\u0000", "\u2400")] // NULL → ␀ + [InlineData ("\u0009", "\u2409")] // TAB → ␉ + [InlineData ("\u000A", "\u240A")] // LF → ␊ + [InlineData ("\u000D", "\u240D")] // CR → ␍ + + // Printable characters (should remain unchanged) + [InlineData ("A", "A")] + [InlineData (" ", " ")] + [InlineData ("~", "~")] + + // Multi-character string (should return unchanged) + [InlineData ("AB", "AB")] + [InlineData ("Hello", "Hello")] + [InlineData ("\u0009A", "\u0009A")] // includes a control char, but length > 1 + public void MakePrintable_ReturnsExpected (string input, string expected) + { + // Act + string result = input.MakePrintable (); + + // Assert + Assert.Equal (expected, result); + } } diff --git a/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs b/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs index da433ffc0..fb15c0f6b 100644 --- a/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs +++ b/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs @@ -658,5 +658,32 @@ Nice Work")] DriverAssert.AssertDriverContentsWithFrameAre (expectedDraw, output, driver); } + [Theory] + [InlineData ("\U0001F468\u200D\U0001F469\u200D\U0001F467\u200D\U0001F466", 2, 1, TextDirection.LeftRight_TopBottom, "👨‍👩‍👧‍👦")] + [InlineData ("\U0001F468\u200D\U0001F469\u200D\U0001F467\u200D\U0001F466", 2, 1, TextDirection.TopBottom_LeftRight, "👨‍👩‍👧‍👦")] + public void Draw_Emojis_With_Zero_Width_Joiner ( + string text, + int width, + int height, + TextDirection direction, + string expectedDraw + ) + { + IDriver driver = CreateFakeDriver (); + + TextFormatter tf = new () + { + Direction = direction, + ConstrainToSize = new (width, height), + Text = text, + WordWrap = false + }; + Assert.Equal (width, text.GetColumns ()); + + tf.Draw (driver, new (0, 0, width, height), Attribute.Default, Attribute.Default); + + DriverAssert.AssertDriverContentsWithFrameAre (expectedDraw, output, driver); + } + #endregion } diff --git a/Tests/UnitTestsParallelizable/Text/TextFormatterTests.cs b/Tests/UnitTestsParallelizable/Text/TextFormatterTests.cs index 8fc71180c..0ec012292 100644 --- a/Tests/UnitTestsParallelizable/Text/TextFormatterTests.cs +++ b/Tests/UnitTestsParallelizable/Text/TextFormatterTests.cs @@ -792,19 +792,16 @@ public class TextFormatterTests (ITestOutputHelper output) : FakeDriverBase [MemberData (nameof (CMGlyphs))] public void GetLengthThatFits_List_Simple_And_Wide_Runes (string text, int columns, int expectedLength) { - List runes = text.ToRuneList (); - Assert.Equal (expectedLength, TextFormatter.GetLengthThatFits (runes, columns)); + Assert.Equal (expectedLength, TextFormatter.GetLengthThatFits (text, columns)); } [Theory] [InlineData ("test", 3, 3)] [InlineData ("test", 4, 4)] [InlineData ("test", 10, 4)] - public void GetLengthThatFits_Runelist (string text, int columns, int expectedLength) + public void GetLengthThatFits_For_String (string text, int columns, int expectedLength) { - List runes = text.ToRuneList (); - - Assert.Equal (expectedLength, TextFormatter.GetLengthThatFits (runes, columns)); + Assert.Equal (expectedLength, TextFormatter.GetLengthThatFits (text, columns)); } [Theory] @@ -833,7 +830,8 @@ public class TextFormatterTests (ITestOutputHelper output) : FakeDriverBase public void GetLengthThatFits_With_Combining_Runes () { var text = "Les Mise\u0328\u0301rables"; - Assert.Equal (16, TextFormatter.GetLengthThatFits (text, 14)); + Assert.Equal (14, TextFormatter.GetLengthThatFits (text, 14)); + Assert.Equal ("Les Misę́rables", text); } [Fact] @@ -841,14 +839,18 @@ public class TextFormatterTests (ITestOutputHelper output) : FakeDriverBase { List text = new () { "Les Mis", "e\u0328\u0301", "rables" }; Assert.Equal (1, TextFormatter.GetMaxColsForWidth (text, 1)); + Assert.Equal ("Les Mis", text [0]); + Assert.Equal ("ę́", text [1]); + Assert.Equal ("rables", text [^1]); } - //[Fact] - //public void GetWidestLineLength_With_Combining_Runes () - //{ - // var text = "Les Mise\u0328\u0301rables"; - // Assert.Equal (1, TextFormatter.GetWidestLineLength (text, 1, 1)); - //} + [Fact] + public void GetWidestLineLength_With_Combining_Runes () + { + var text = "Les Mise\u0328\u0301rables"; + Assert.Equal (14, TextFormatter.GetWidestLineLength (text, 1)); + Assert.Equal ("Les Misę́rables", text); + } [Fact] public void Internal_Tests () @@ -2451,6 +2453,7 @@ public class TextFormatterTests (ITestOutputHelper output) : FakeDriverBase Assert.Equal (expected, breakLines); // Double space Complex example - this is how VS 2022 does it + // which I think is not correct. //text = "A sentence has words. "; //breakLines = ""; //wrappedLines = TextFormatter.WordWrapText (text, width, preserveTrailingSpaces: true); @@ -2762,8 +2765,7 @@ public class TextFormatterTests (ITestOutputHelper output) : FakeDriverBase "ฮ", "ฯ", "ะั", - "า", - "ำ" + "าำ" } )] public void WordWrap_Unicode_SingleWordLine ( @@ -2798,7 +2800,17 @@ public class TextFormatterTests (ITestOutputHelper output) : FakeDriverBase Assert.True ( expectedClippedWidth >= (wrappedLines.Count > 0 ? wrappedLines.Max (l => l.GetColumns ()) : 0) ); - Assert.Equal (resultLines, wrappedLines); + + if (maxWidth == 1) + { + List newResultLines = resultLines.ToList (); + newResultLines [^1] = ""; + Assert.Equal (newResultLines, wrappedLines); + } + else + { + Assert.Equal (resultLines, wrappedLines); + } } /// WordWrap strips CRLF @@ -3075,8 +3087,8 @@ public class TextFormatterTests (ITestOutputHelper output) : FakeDriverBase } [Theory] - [InlineData (14, 1, TextDirection.LeftRight_TopBottom, "Les Misęrables")] - [InlineData (1, 14, TextDirection.TopBottom_LeftRight, "L\ne\ns\n \nM\ni\ns\nę\nr\na\nb\nl\ne\ns")] + [InlineData (14, 1, TextDirection.LeftRight_TopBottom, "Les Misę́rables")] + [InlineData (1, 14, TextDirection.TopBottom_LeftRight, "L\ne\ns\n \nM\ni\ns\nę́\nr\na\nb\nl\ne\ns")] [InlineData ( 4, 4, @@ -3085,7 +3097,7 @@ public class TextFormatterTests (ITestOutputHelper output) : FakeDriverBase LMre eias ssb - ęl " + ę́l " )] public void Draw_With_Combining_Runes (int width, int height, TextDirection textDirection, string expected) { @@ -3111,7 +3123,6 @@ ssb driver.End (); } - [Theory] [InlineData (17, 1, TextDirection.LeftRight_TopBottom, 4, "This is a Tab")] [InlineData (1, 17, TextDirection.TopBottom_LeftRight, 4, "T\nh\ni\ns\n \ni\ns\n \na\n \n \n \n \n \nT\na\nb")] @@ -3187,7 +3198,6 @@ ssb driver.End (); } - [Theory] [InlineData (17, 1, TextDirection.LeftRight_TopBottom, 4, "This is a Tab")] [InlineData (1, 17, TextDirection.TopBottom_LeftRight, 4, "T\nh\ni\ns\n \ni\ns\n \na\n \n \n \n \n \nT\na\nb")] @@ -3224,5 +3234,4 @@ ssb driver.End (); } - } diff --git a/Tests/UnitTestsParallelizable/View/Draw/ViewClearViewportTests.cs b/Tests/UnitTestsParallelizable/View/Draw/ViewClearViewportTests.cs index 1ef96e203..58ad1bf9c 100644 --- a/Tests/UnitTestsParallelizable/View/Draw/ViewClearViewportTests.cs +++ b/Tests/UnitTestsParallelizable/View/Draw/ViewClearViewportTests.cs @@ -36,7 +36,7 @@ public class ViewClearViewportTests () : FakeDriverBase { for (int x = viewportScreen.X; x < viewportScreen.X + viewportScreen.Width; x++) { - Assert.Equal (new Rune (' '), driver.Contents [y, x].Rune); + Assert.Equal (" ", driver.Contents [y, x].Grapheme); } } } @@ -75,7 +75,7 @@ public class ViewClearViewportTests () : FakeDriverBase { for (int x = toClear.X; x < toClear.X + toClear.Width; x++) { - Assert.Equal (new Rune (' '), driver.Contents [y, x].Rune); + Assert.Equal (" ", driver.Contents [y, x].Grapheme); } } } @@ -154,7 +154,7 @@ public class ViewClearViewportTests () : FakeDriverBase { for (int x = viewportScreen.X; x < viewportScreen.X + viewportScreen.Width; x++) { - Assert.Equal (new Rune ('X'), driver.Contents [y, x].Rune); + Assert.Equal ("X", driver.Contents [y, x].Grapheme); } } } @@ -309,7 +309,7 @@ public class ViewClearViewportTests () : FakeDriverBase { for (int x = viewportScreen.X; x < viewportScreen.X + viewportScreen.Width; x++) { - Assert.Equal (new Rune (' '), driver.Contents [y, x].Rune); + Assert.Equal (" ", driver.Contents [y, x].Grapheme); } } } @@ -353,7 +353,7 @@ public class ViewClearViewportTests () : FakeDriverBase { for (int x = toClear.X; x < toClear.X + toClear.Width; x++) { - Assert.Equal (new Rune (' '), driver.Contents [y, x].Rune); + Assert.Equal (" ", driver.Contents[y, x].Grapheme); } } } diff --git a/Tests/UnitTestsParallelizable/View/Draw/ViewDrawTextAndLineCanvasTests.cs b/Tests/UnitTestsParallelizable/View/Draw/ViewDrawTextAndLineCanvasTests.cs index 5f2af2055..6a4c330e2 100644 --- a/Tests/UnitTestsParallelizable/View/Draw/ViewDrawTextAndLineCanvasTests.cs +++ b/Tests/UnitTestsParallelizable/View/Draw/ViewDrawTextAndLineCanvasTests.cs @@ -80,10 +80,10 @@ public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase // Text should appear at the content location Point screenPos = view.ContentToScreen (Point.Empty); - Assert.Equal ('T', (char)driver.Contents [screenPos.Y, screenPos.X].Rune.Value); - Assert.Equal ('e', (char)driver.Contents [screenPos.Y, screenPos.X + 1].Rune.Value); - Assert.Equal ('s', (char)driver.Contents [screenPos.Y, screenPos.X + 2].Rune.Value); - Assert.Equal ('t', (char)driver.Contents [screenPos.Y, screenPos.X + 3].Rune.Value); + Assert.Equal ("T", driver.Contents [screenPos.Y, screenPos.X].Grapheme); + Assert.Equal ("e", driver.Contents [screenPos.Y, screenPos.X + 1].Grapheme); + Assert.Equal ("s", driver.Contents [screenPos.Y, screenPos.X + 2].Grapheme); + Assert.Equal ("t", driver.Contents [screenPos.Y, screenPos.X + 3].Grapheme); } [Fact] @@ -273,7 +273,7 @@ public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase // Verify the line was drawn (check for horizontal line character) for (int i = 0; i < 5; i++) { - Assert.NotEqual (new Rune (' '), driver.Contents [screenPos.Y, screenPos.X + i].Rune); + Assert.NotEqual (" ", driver.Contents [screenPos.Y, screenPos.X + i].Grapheme); } } @@ -409,7 +409,7 @@ public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase bool lineRendered = true; for (int i = 0; i < 5; i++) { - if (driver.Contents [screenPos.Y, screenPos.X + i].Rune.Value == ' ') + if (driver.Contents [screenPos.Y, screenPos.X + i].Grapheme == " ") { lineRendered = false; break; diff --git a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs index 65a529a48..47f6a0234 100644 --- a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.Text; using Moq; using Terminal.Gui; using UnitTests; @@ -1278,14 +1279,14 @@ Item 6", string GetContents (int line) { - var item = ""; + var sb = new StringBuilder (); for (var i = 0; i < 7; i++) { - item += (app.Driver?.Contents!) [line, i].Rune; + sb.Append ((app?.Driver?.Contents!) [line, i].Grapheme); } - return item; + return sb.ToString (); } top.Dispose (); diff --git a/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs b/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs index a5362f46f..eff2c41b2 100644 --- a/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs @@ -622,14 +622,14 @@ public class TextFieldTests (ITestOutputHelper output) : FakeDriverBase string GetContents () { - var item = ""; + var sb = new StringBuilder (); for (var i = 0; i < 16; i++) { - item += driver.Contents [0, i]!.Rune; + sb.Append (driver.Contents! [0, i]!.Grapheme); } - return item; + return sb.ToString (); } } } diff --git a/Tests/UnitTestsParallelizable/Views/TextViewTests.cs b/Tests/UnitTestsParallelizable/Views/TextViewTests.cs index 8ea0df071..a9b940bea 100644 --- a/Tests/UnitTestsParallelizable/Views/TextViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TextViewTests.cs @@ -1444,23 +1444,23 @@ public class TextViewTests public void Internal_Tests () { var txt = "This is a text."; - List txtRunes = Cell.StringToCells (txt); - Assert.Equal (txt.Length, txtRunes.Count); - Assert.Equal ('T', txtRunes [0].Rune.Value); - Assert.Equal ('h', txtRunes [1].Rune.Value); - Assert.Equal ('i', txtRunes [2].Rune.Value); - Assert.Equal ('s', txtRunes [3].Rune.Value); - Assert.Equal (' ', txtRunes [4].Rune.Value); - Assert.Equal ('i', txtRunes [5].Rune.Value); - Assert.Equal ('s', txtRunes [6].Rune.Value); - Assert.Equal (' ', txtRunes [7].Rune.Value); - Assert.Equal ('a', txtRunes [8].Rune.Value); - Assert.Equal (' ', txtRunes [9].Rune.Value); - Assert.Equal ('t', txtRunes [10].Rune.Value); - Assert.Equal ('e', txtRunes [11].Rune.Value); - Assert.Equal ('x', txtRunes [12].Rune.Value); - Assert.Equal ('t', txtRunes [13].Rune.Value); - Assert.Equal ('.', txtRunes [^1].Rune.Value); + List txtStrings = Cell.StringToCells (txt); + Assert.Equal (txt.Length, txtStrings.Count); + Assert.Equal ("T", txtStrings [0].Grapheme); + Assert.Equal ("h", txtStrings [1].Grapheme); + Assert.Equal ("i", txtStrings [2].Grapheme); + Assert.Equal ("s", txtStrings [3].Grapheme); + Assert.Equal (" ", txtStrings [4].Grapheme); + Assert.Equal ("i", txtStrings [5].Grapheme); + Assert.Equal ("s", txtStrings [6].Grapheme); + Assert.Equal (" ", txtStrings [7].Grapheme); + Assert.Equal ("a", txtStrings [8].Grapheme); + Assert.Equal (" ", txtStrings [9].Grapheme); + Assert.Equal ("t", txtStrings [10].Grapheme); + Assert.Equal ("e", txtStrings [11].Grapheme); + Assert.Equal ("x", txtStrings [12].Grapheme); + Assert.Equal ("t", txtStrings [13].Grapheme); + Assert.Equal (".", txtStrings [^1].Grapheme); var col = 0; Assert.True (TextModel.SetCol (ref col, 80, 79)); @@ -1469,19 +1469,19 @@ public class TextViewTests var start = 0; var x = 8; - Assert.Equal (8, TextModel.GetColFromX (txtRunes, start, x)); - Assert.Equal ('a', txtRunes [start + x].Rune.Value); + Assert.Equal (8, TextModel.GetColFromX (txtStrings, start, x)); + Assert.Equal ("a", txtStrings [start + x].Grapheme); start = 1; x = 7; - Assert.Equal (7, TextModel.GetColFromX (txtRunes, start, x)); - Assert.Equal ('a', txtRunes [start + x].Rune.Value); + Assert.Equal (7, TextModel.GetColFromX (txtStrings, start, x)); + Assert.Equal ("a", txtStrings [start + x].Grapheme); - Assert.Equal ((15, 15), TextModel.DisplaySize (txtRunes)); - Assert.Equal ((6, 6), TextModel.DisplaySize (txtRunes, 1, 7)); + Assert.Equal ((15, 15), TextModel.DisplaySize (txtStrings)); + Assert.Equal ((6, 6), TextModel.DisplaySize (txtStrings, 1, 7)); - Assert.Equal (0, TextModel.CalculateLeftColumn (txtRunes, 0, 7, 8)); - Assert.Equal (1, TextModel.CalculateLeftColumn (txtRunes, 0, 8, 8)); - Assert.Equal (2, TextModel.CalculateLeftColumn (txtRunes, 0, 9, 8)); + Assert.Equal (0, TextModel.CalculateLeftColumn (txtStrings, 0, 7, 8)); + Assert.Equal (1, TextModel.CalculateLeftColumn (txtStrings, 0, 8, 8)); + Assert.Equal (2, TextModel.CalculateLeftColumn (txtStrings, 0, 9, 8)); var tm = new TextModel (); tm.AddLine (0, Cell.StringToCells ("This is first line.")); @@ -2050,9 +2050,9 @@ public class TextViewTests Assert.True (c1.Equals (c2)); Assert.True (c2.Equals (c1)); - c1.Rune = new ('a'); + c1.Grapheme = new ("a"); c1.Attribute = new (); - c2.Rune = new ('a'); + c2.Grapheme = new ("a"); c2.Attribute = new (); Assert.True (c1.Equals (c2)); Assert.True (c2.Equals (c1)); @@ -2063,10 +2063,10 @@ public class TextViewTests { List cells = new () { - new () { Rune = new ('T') }, - new () { Rune = new ('e') }, - new () { Rune = new ('s') }, - new () { Rune = new ('t') } + new () { Grapheme = new ("T") }, + new () { Grapheme = new ("e") }, + new () { Grapheme = new ("s") }, + new () { Grapheme = new ("t") } }; TextView tv = CreateTextView (); var top = new Toplevel (); From 16b42e86fdf2b52797c526a05de2d3d8ef496899 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 19:12:09 +0000 Subject: [PATCH 06/16] Initial plan From 4b975fd5b7f4ae594768f4193139221d01ac5e6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 19:34:48 +0000 Subject: [PATCH 07/16] Rename IApplication.Current to TopRunnable Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Examples/CommunityToolkitExample/Program.cs | 2 +- Examples/ReactiveExample/Program.cs | 2 +- Examples/UICatalog/Scenario.cs | 4 +- .../UICatalog/Scenarios/AllViewsTester.cs | 2 +- Examples/UICatalog/Scenarios/Bars.cs | 16 +- .../UICatalog/Scenarios/CombiningMarks.cs | 2 +- .../Scenarios/ConfigurationEditor.cs | 2 +- Examples/UICatalog/Scenarios/CsvEditor.cs | 2 +- Examples/UICatalog/Scenarios/Mazing.cs | 6 +- Examples/UICatalog/Scenarios/Shortcuts.cs | 54 ++--- .../Scenarios/SingleBackgroundWorker.cs | 4 +- Examples/UICatalog/Scenarios/Themes.cs | 2 +- Examples/UICatalog/Scenarios/TreeUseCases.cs | 12 +- .../Scenarios/WindowsAndFrameViews.cs | 2 +- Examples/UICatalog/UICatalog.cs | 4 +- Examples/UICatalog/UICatalogTop.cs | 2 +- Terminal.Gui/App/Application.Current.cs | 10 +- Terminal.Gui/App/ApplicationImpl.Lifecycle.cs | 10 +- Terminal.Gui/App/ApplicationImpl.Run.cs | 64 +++--- Terminal.Gui/App/ApplicationImpl.cs | 12 +- Terminal.Gui/App/ApplicationNavigation.cs | 2 +- Terminal.Gui/App/ApplicationPopover.cs | 8 +- Terminal.Gui/App/IApplication.cs | 11 +- Terminal.Gui/App/IPopover.cs | 2 +- Terminal.Gui/App/Keyboard/KeyboardImpl.cs | 4 +- .../App/MainLoop/ApplicationMainLoop.cs | 8 +- Terminal.Gui/App/Mouse/MouseImpl.cs | 6 +- Terminal.Gui/App/PopoverBaseImpl.cs | 2 +- .../App/Toplevel/ToplevelTransitionManager.cs | 6 +- .../ViewBase/Adornment/Border.Arrangment.cs | 2 +- Terminal.Gui/ViewBase/View.Hierarchy.cs | 4 +- Terminal.Gui/ViewBase/View.Layout.cs | 26 +-- Terminal.Gui/ViewBase/View.Navigation.cs | 12 +- Terminal.Gui/Views/Dialog.cs | 2 +- Terminal.Gui/Views/Menuv1/Menu.cs | 12 +- Terminal.Gui/Views/Menuv1/MenuBar.cs | 8 +- Terminal.Gui/Views/Toplevel.cs | 6 +- Terminal.Gui/Views/Wizard/Wizard.cs | 2 +- .../GuiTestContextKeyEventTests.cs | 8 +- .../FluentTests/GuiTestContextTests.cs | 8 +- .../FluentTests/MenuBarv2Tests.cs | 46 ++--- .../FluentTests/NavigationTests.cs | 4 +- .../FluentTests/PopverMenuTests.cs | 32 +-- Tests/StressTests/ApplicationStressTests.cs | 4 +- Tests/StressTests/ScenariosStressTests.cs | 4 +- .../FakeDriver/FakeApplicationLifecycle.cs | 2 +- .../GuiTestContext.Navigation.cs | 4 +- .../GuiTestContext.ViewBase.cs | 8 +- .../Application.NavigationTests.cs | 24 +-- .../ApplicationImplBeginEndTests.cs | 28 +-- .../Application/ApplicationImplTests.cs | 62 +++--- .../Application/ApplicationPopoverTests.cs | 14 +- .../Application/ApplicationScreenTests.cs | 10 +- .../UnitTests/Application/ApplicationTests.cs | 110 +++++----- .../Mouse/ApplicationMouseEnterLeaveTests.cs | 78 +++---- .../Mouse/ApplicationMouseTests.cs | 16 +- .../Application/SessionTokenTests.cs | 2 +- .../Application/SynchronizatonContextTests.cs | 4 +- Tests/UnitTests/Dialogs/DialogTests.cs | 30 +-- Tests/UnitTests/Dialogs/MessageBoxTests.cs | 14 +- .../View/Adornment/AdornmentSubViewTests.cs | 26 +-- Tests/UnitTests/View/Adornment/MarginTests.cs | 32 +-- .../View/Keyboard/KeyBindingsTests.cs | 6 +- .../View/Layout/GetViewsUnderLocationTests.cs | 194 +++++++++--------- .../UnitTests/View/Layout/Pos.CombineTests.cs | 24 +-- Tests/UnitTests/View/Layout/SetLayoutTests.cs | 16 +- .../View/Navigation/CanFocusTests.cs | 8 +- .../View/Navigation/NavigationTests.cs | 10 +- Tests/UnitTests/View/TextTests.cs | 2 +- Tests/UnitTests/View/ViewCommandTests.cs | 8 +- .../ViewportSettings.TransparentMouseTests.cs | 6 +- .../Views/AppendAutocompleteTests.cs | 24 +-- Tests/UnitTests/Views/ButtonTests.cs | 2 +- Tests/UnitTests/Views/CheckBoxTests.cs | 8 +- Tests/UnitTests/Views/ColorPickerTests.cs | 10 +- Tests/UnitTests/Views/ComboBoxTests.cs | 4 +- Tests/UnitTests/Views/HexViewTests.cs | 58 +++--- Tests/UnitTests/Views/LabelTests.cs | 38 ++-- .../UnitTests/Views/Menuv1/MenuBarv1Tests.cs | 56 ++--- Tests/UnitTests/Views/ShortcutTests.cs | 76 +++---- Tests/UnitTests/Views/SpinnerViewTests.cs | 6 +- Tests/UnitTests/Views/TableViewTests.cs | 54 ++--- Tests/UnitTests/Views/TextFieldTests.cs | 16 +- Tests/UnitTests/Views/TextViewTests.cs | 2 +- Tests/UnitTests/Views/ToplevelTests.cs | 68 +++--- Tests/UnitTestsParallelizable/TestSetup.cs | 2 +- docfx/docs/application.md | 24 +-- docfx/docs/migratingfromv1.md | 8 +- docfx/docs/navigation.md | 4 +- docfx/docs/runnable-architecture-proposal.md | 95 +++++++++ 90 files changed, 916 insertions(+), 820 deletions(-) create mode 100644 docfx/docs/runnable-architecture-proposal.md diff --git a/Examples/CommunityToolkitExample/Program.cs b/Examples/CommunityToolkitExample/Program.cs index 74e45ce06..ab1357224 100644 --- a/Examples/CommunityToolkitExample/Program.cs +++ b/Examples/CommunityToolkitExample/Program.cs @@ -16,7 +16,7 @@ public static class Program Services = ConfigureServices (); Application.Init (); Application.Run (Services.GetRequiredService ()); - Application.Current?.Dispose (); + Application.TopRunnable?.Dispose (); Application.Shutdown (); } diff --git a/Examples/ReactiveExample/Program.cs b/Examples/ReactiveExample/Program.cs index f73aa807d..2dcb27b90 100644 --- a/Examples/ReactiveExample/Program.cs +++ b/Examples/ReactiveExample/Program.cs @@ -16,7 +16,7 @@ public static class Program RxApp.MainThreadScheduler = TerminalScheduler.Default; RxApp.TaskpoolScheduler = TaskPoolScheduler.Default; Application.Run (new LoginView (new LoginViewModel ())); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.Shutdown (); } } diff --git a/Examples/UICatalog/Scenario.cs b/Examples/UICatalog/Scenario.cs index 0531d06c9..0fa13e6db 100644 --- a/Examples/UICatalog/Scenario.cs +++ b/Examples/UICatalog/Scenario.cs @@ -221,7 +221,7 @@ public class Scenario : IDisposable private void OnApplicationSessionBegun (object? sender, SessionTokenEventArgs e) { - SubscribeAllSubViews (Application.Current!); + SubscribeAllSubViews (Application.TopRunnable!); _demoKeys = GetDemoKeyStrokes (); @@ -241,7 +241,7 @@ public class Scenario : IDisposable return; - // Get a list of all subviews under Application.Current (and their subviews, etc.) + // Get a list of all subviews under Application.TopRunnable (and their subviews, etc.) // and subscribe to their DrawComplete event void SubscribeAllSubViews (View view) { diff --git a/Examples/UICatalog/Scenarios/AllViewsTester.cs b/Examples/UICatalog/Scenarios/AllViewsTester.cs index 5ac97bea9..aae301c3a 100644 --- a/Examples/UICatalog/Scenarios/AllViewsTester.cs +++ b/Examples/UICatalog/Scenarios/AllViewsTester.cs @@ -28,7 +28,7 @@ public class AllViewsTester : Scenario public override void Main () { - // Don't create a sub-win (Scenario.Win); just use Application.Current + // Don't create a sub-win (Scenario.Win); just use Application.TopRunnable Application.Init (); var app = new Window diff --git a/Examples/UICatalog/Scenarios/Bars.cs b/Examples/UICatalog/Scenarios/Bars.cs index 03f908611..730ddf969 100644 --- a/Examples/UICatalog/Scenarios/Bars.cs +++ b/Examples/UICatalog/Scenarios/Bars.cs @@ -28,7 +28,7 @@ public class Bars : Scenario // QuitKey and it only sticks if changed after init private void App_Loaded (object sender, EventArgs e) { - Application.Current!.Title = GetQuitKeyAndName (); + Application.TopRunnable!.Title = GetQuitKeyAndName (); ObservableCollection eventSource = new (); ListView eventLog = new ListView () @@ -41,7 +41,7 @@ public class Bars : Scenario Source = new ListWrapper (eventSource) }; eventLog.Border!.Thickness = new (0, 1, 0, 0); - Application.Current.Add (eventLog); + Application.TopRunnable.Add (eventLog); FrameView menuBarLikeExamples = new () { @@ -51,7 +51,7 @@ public class Bars : Scenario Width = Dim.Fill () - Dim.Width (eventLog), Height = Dim.Percent(33), }; - Application.Current.Add (menuBarLikeExamples); + Application.TopRunnable.Add (menuBarLikeExamples); Label label = new Label () { @@ -98,7 +98,7 @@ public class Bars : Scenario Width = Dim.Fill () - Dim.Width (eventLog), Height = Dim.Percent (33), }; - Application.Current.Add (menuLikeExamples); + Application.TopRunnable.Add (menuLikeExamples); label = new Label () { @@ -212,7 +212,7 @@ public class Bars : Scenario Width = Dim.Width (menuLikeExamples), Height = Dim.Percent (33), }; - Application.Current.Add (statusBarLikeExamples); + Application.TopRunnable.Add (statusBarLikeExamples); label = new Label () { @@ -249,7 +249,7 @@ public class Bars : Scenario ConfigStatusBar (bar); statusBarLikeExamples.Add (bar); - foreach (FrameView frameView in Application.Current.SubViews.Where (f => f is FrameView)!) + foreach (FrameView frameView in Application.TopRunnable.SubViews.Where (f => f is FrameView)!) { foreach (Bar barView in frameView.SubViews.Where (b => b is Bar)!) { @@ -269,8 +269,8 @@ public class Bars : Scenario //private void SetupContentMenu () //{ - // Application.Current.Add (new Label { Text = "Right Click for Context Menu", X = Pos.Center (), Y = 4 }); - // Application.Current.MouseClick += ShowContextMenu; + // Application.TopRunnable.Add (new Label { Text = "Right Click for Context Menu", X = Pos.Center (), Y = 4 }); + // Application.TopRunnable.MouseClick += ShowContextMenu; //} //private void ShowContextMenu (object s, MouseEventEventArgs e) diff --git a/Examples/UICatalog/Scenarios/CombiningMarks.cs b/Examples/UICatalog/Scenarios/CombiningMarks.cs index 62181c500..5a729760e 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.Current!.SetNeedsDraw (); + Application.TopRunnable!.SetNeedsDraw (); var i = -1; top.Move (0, ++i); diff --git a/Examples/UICatalog/Scenarios/ConfigurationEditor.cs b/Examples/UICatalog/Scenarios/ConfigurationEditor.cs index 36011a78c..c0bfcb03d 100644 --- a/Examples/UICatalog/Scenarios/ConfigurationEditor.cs +++ b/Examples/UICatalog/Scenarios/ConfigurationEditor.cs @@ -75,7 +75,7 @@ public class ConfigurationEditor : Scenario void ConfigurationManagerOnApplied (object? sender, ConfigurationManagerEventArgs e) { - Application.Current?.SetNeedsDraw (); + Application.TopRunnable?.SetNeedsDraw (); } } public void Save () diff --git a/Examples/UICatalog/Scenarios/CsvEditor.cs b/Examples/UICatalog/Scenarios/CsvEditor.cs index aca10fdac..f61d03b14 100644 --- a/Examples/UICatalog/Scenarios/CsvEditor.cs +++ b/Examples/UICatalog/Scenarios/CsvEditor.cs @@ -502,7 +502,7 @@ public class CsvEditor : Scenario // Only set the current filename if we successfully loaded the entire file _currentFile = filename; _selectedCellTextField.SuperView.Enabled = true; - Application.Current.Title = $"{GetName ()} - {Path.GetFileName (_currentFile)}"; + Application.TopRunnable.Title = $"{GetName ()} - {Path.GetFileName (_currentFile)}"; } catch (Exception ex) { diff --git a/Examples/UICatalog/Scenarios/Mazing.cs b/Examples/UICatalog/Scenarios/Mazing.cs index 935c72ae5..04cca789d 100644 --- a/Examples/UICatalog/Scenarios/Mazing.cs +++ b/Examples/UICatalog/Scenarios/Mazing.cs @@ -171,7 +171,7 @@ public class Mazing : Scenario if (_m.PlayerHp <= 0) { _message = "You died!"; - Application.Current!.SetNeedsDraw (); // trigger redraw + Application.TopRunnable!.SetNeedsDraw (); // trigger redraw _dead = true; return; // Stop further action if dead @@ -190,7 +190,7 @@ public class Mazing : Scenario _message = string.Empty; } - Application.Current!.SetNeedsDraw (); // trigger redraw + Application.TopRunnable!.SetNeedsDraw (); // trigger redraw } // Optional win condition: @@ -200,7 +200,7 @@ public class Mazing : Scenario _m = new (); // Generate a new maze _m.PlayerHp = hp; GenerateNpcs (); - Application.Current!.SetNeedsDraw (); // trigger redraw + Application.TopRunnable!.SetNeedsDraw (); // trigger redraw } } } diff --git a/Examples/UICatalog/Scenarios/Shortcuts.cs b/Examples/UICatalog/Scenarios/Shortcuts.cs index 331ed0337..573834aa3 100644 --- a/Examples/UICatalog/Scenarios/Shortcuts.cs +++ b/Examples/UICatalog/Scenarios/Shortcuts.cs @@ -28,7 +28,7 @@ public class Shortcuts : Scenario private void App_Loaded (object? sender, EventArgs e) { Application.QuitKey = Key.F4.WithCtrl; - Application.Current!.Title = GetQuitKeyAndName (); + Application.TopRunnable!.Title = GetQuitKeyAndName (); ObservableCollection eventSource = new (); @@ -46,14 +46,14 @@ public class Shortcuts : Scenario eventLog.Width = Dim.Func ( _ => Math.Min ( - Application.Current.Viewport.Width / 2, + Application.TopRunnable.Viewport.Width / 2, eventLog?.MaxLength + eventLog!.GetAdornmentsThickness ().Horizontal ?? 0)); eventLog.Width = Dim.Func ( _ => Math.Min ( eventLog.SuperView!.Viewport.Width / 2, eventLog?.MaxLength + eventLog!.GetAdornmentsThickness ().Horizontal ?? 0)); - Application.Current.Add (eventLog); + Application.TopRunnable.Add (eventLog); var alignKeysShortcut = new Shortcut { @@ -86,7 +86,7 @@ public class Shortcuts : Scenario }; - Application.Current.Add (alignKeysShortcut); + Application.TopRunnable.Add (alignKeysShortcut); var commandFirstShortcut = new Shortcut { @@ -115,7 +115,7 @@ public class Shortcuts : Scenario $"{commandFirstShortcut.Id}.CommandView.CheckedStateChanging: {cb.Text}"); eventLog.MoveDown (); - IEnumerable toAlign = Application.Current.SubViews.OfType (); + IEnumerable toAlign = Application.TopRunnable.SubViews.OfType (); IEnumerable enumerable = toAlign as View [] ?? toAlign.ToArray (); foreach (View view in enumerable) @@ -134,7 +134,7 @@ public class Shortcuts : Scenario } }; - Application.Current.Add (commandFirstShortcut); + Application.TopRunnable.Add (commandFirstShortcut); var canFocusShortcut = new Shortcut { @@ -159,7 +159,7 @@ public class Shortcuts : Scenario SetCanFocus (e.Result == CheckState.Checked); } }; - Application.Current.Add (canFocusShortcut); + Application.TopRunnable.Add (canFocusShortcut); var appShortcut = new Shortcut { @@ -173,7 +173,7 @@ public class Shortcuts : Scenario BindKeyToApplication = true }; - Application.Current.Add (appShortcut); + Application.TopRunnable.Add (appShortcut); var buttonShortcut = new Shortcut { @@ -193,7 +193,7 @@ public class Shortcuts : Scenario var button = (Button)buttonShortcut.CommandView; buttonShortcut.Accepting += Button_Clicked; - Application.Current.Add (buttonShortcut); + Application.TopRunnable.Add (buttonShortcut); var optionSelectorShortcut = new Shortcut { @@ -221,7 +221,7 @@ public class Shortcuts : Scenario } }; - Application.Current.Add (optionSelectorShortcut); + Application.TopRunnable.Add (optionSelectorShortcut); var sliderShortcut = new Shortcut { @@ -248,7 +248,7 @@ public class Shortcuts : Scenario eventLog.MoveDown (); }; - Application.Current.Add (sliderShortcut); + Application.TopRunnable.Add (sliderShortcut); ListView listView = new ListView () { @@ -270,7 +270,7 @@ public class Shortcuts : Scenario Key = Key.F5.WithCtrl, }; - Application.Current.Add (listViewShortcut); + Application.TopRunnable.Add (listViewShortcut); var noCommandShortcut = new Shortcut { @@ -282,7 +282,7 @@ public class Shortcuts : Scenario Key = Key.D0 }; - Application.Current.Add (noCommandShortcut); + Application.TopRunnable.Add (noCommandShortcut); var noKeyShortcut = new Shortcut { @@ -295,7 +295,7 @@ public class Shortcuts : Scenario HelpText = "Keyless" }; - Application.Current.Add (noKeyShortcut); + Application.TopRunnable.Add (noKeyShortcut); var noHelpShortcut = new Shortcut { @@ -308,7 +308,7 @@ public class Shortcuts : Scenario HelpText = "" }; - Application.Current.Add (noHelpShortcut); + Application.TopRunnable.Add (noHelpShortcut); noHelpShortcut.SetFocus (); var framedShortcut = new Shortcut @@ -340,7 +340,7 @@ public class Shortcuts : Scenario } framedShortcut.SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Toplevel); - Application.Current.Add (framedShortcut); + Application.TopRunnable.Add (framedShortcut); // Horizontal var progressShortcut = new Shortcut @@ -387,7 +387,7 @@ public class Shortcuts : Scenario }; timer.Start (); - Application.Current.Add (progressShortcut); + Application.TopRunnable.Add (progressShortcut); var textField = new TextField { @@ -408,7 +408,7 @@ public class Shortcuts : Scenario }; textField.CanFocus = true; - Application.Current.Add (textFieldShortcut); + Application.TopRunnable.Add (textFieldShortcut); var bgColorShortcut = new Shortcut { @@ -450,19 +450,19 @@ public class Shortcuts : Scenario eventSource.Add ($"ColorChanged: {o.GetType ().Name} - {args.Result}"); eventLog.MoveDown (); - Application.Current.SetScheme ( - new (Application.Current.GetScheme ()) + Application.TopRunnable.SetScheme ( + new (Application.TopRunnable.GetScheme ()) { Normal = new ( - Application.Current!.GetAttributeForRole (VisualRole.Normal).Foreground, + Application.TopRunnable!.GetAttributeForRole (VisualRole.Normal).Foreground, args.Result, - Application.Current!.GetAttributeForRole (VisualRole.Normal).Style) + Application.TopRunnable!.GetAttributeForRole (VisualRole.Normal).Style) }); } }; bgColorShortcut.CommandView = bgColor; - Application.Current.Add (bgColorShortcut); + Application.TopRunnable.Add (bgColorShortcut); var appQuitShortcut = new Shortcut { @@ -476,9 +476,9 @@ public class Shortcuts : Scenario }; appQuitShortcut.Accepting += (o, args) => { Application.RequestStop (); }; - Application.Current.Add (appQuitShortcut); + Application.TopRunnable.Add (appQuitShortcut); - foreach (Shortcut shortcut in Application.Current.SubViews.OfType ()) + foreach (Shortcut shortcut in Application.TopRunnable.SubViews.OfType ()) { shortcut.Selecting += (o, args) => { @@ -529,7 +529,7 @@ public class Shortcuts : Scenario void SetCanFocus (bool canFocus) { - foreach (Shortcut peer in Application.Current!.SubViews.OfType ()) + foreach (Shortcut peer in Application.TopRunnable!.SubViews.OfType ()) { if (peer.CanFocus) { @@ -542,7 +542,7 @@ public class Shortcuts : Scenario { var max = 0; - IEnumerable toAlign = Application.Current!.SubViews.OfType ().Where(s => !s.Y.Has(out _)).Cast(); + IEnumerable toAlign = Application.TopRunnable!.SubViews.OfType ().Where(s => !s.Y.Has(out _)).Cast(); IEnumerable enumerable = toAlign as Shortcut [] ?? toAlign.ToArray (); if (align) diff --git a/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs b/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs index 571597f8c..92e126b84 100644 --- a/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs +++ b/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs @@ -179,9 +179,9 @@ public class SingleBackgroundWorker : Scenario var builderUI = new StagingUIController (_startStaging, e.Result as ObservableCollection); - Toplevel top = Application.Current; + Toplevel top = Application.TopRunnable; top.Visible = false; - Application.Current.Visible = false; + Application.TopRunnable.Visible = false; builderUI.Load (); builderUI.Dispose (); top.Visible = true; diff --git a/Examples/UICatalog/Scenarios/Themes.cs b/Examples/UICatalog/Scenarios/Themes.cs index d515b5ee0..68d73ed40 100644 --- a/Examples/UICatalog/Scenarios/Themes.cs +++ b/Examples/UICatalog/Scenarios/Themes.cs @@ -129,7 +129,7 @@ public sealed class Themes : Scenario { if (_view is { }) { - Application.Current!.SchemeName = args.NewValue; + Application.TopRunnable!.SchemeName = args.NewValue; if (_view.HasScheme) { diff --git a/Examples/UICatalog/Scenarios/TreeUseCases.cs b/Examples/UICatalog/Scenarios/TreeUseCases.cs index d9ee48d15..021b5b65b 100644 --- a/Examples/UICatalog/Scenarios/TreeUseCases.cs +++ b/Examples/UICatalog/Scenarios/TreeUseCases.cs @@ -77,7 +77,7 @@ public class TreeUseCases : Scenario if (_currentTree != null) { - Application.Current.Remove (_currentTree); + Application.TopRunnable.Remove (_currentTree); _currentTree.Dispose (); } @@ -97,7 +97,7 @@ public class TreeUseCases : Scenario tree.TreeBuilder = new GameObjectTreeBuilder (); } - Application.Current.Add (tree); + Application.TopRunnable.Add (tree); tree.AddObject (army1); @@ -117,13 +117,13 @@ public class TreeUseCases : Scenario if (_currentTree != null) { - Application.Current.Remove (_currentTree); + Application.TopRunnable.Remove (_currentTree); _currentTree.Dispose (); } var tree = new TreeView { X = 0, Y = 1, Width = Dim.Fill(), Height = Dim.Fill (1) }; - Application.Current.Add (tree); + Application.TopRunnable.Add (tree); tree.AddObject (myHouse); @@ -134,13 +134,13 @@ public class TreeUseCases : Scenario { if (_currentTree != null) { - Application.Current.Remove (_currentTree); + Application.TopRunnable.Remove (_currentTree); _currentTree.Dispose (); } var tree = new TreeView { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1) }; - Application.Current.Add (tree); + Application.TopRunnable.Add (tree); var root1 = new TreeNode ("Root1"); root1.Children.Add (new TreeNode ("Child1.1")); diff --git a/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs b/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs index afe039f1c..d2b58d847 100644 --- a/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs +++ b/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs @@ -69,7 +69,7 @@ public class WindowsAndFrameViews : Scenario // add it to our list listWin.Add (win); - // create 3 more Windows in a loop, adding them Application.Current + // create 3 more Windows in a loop, adding them Application.TopRunnable // Each with a // button // sub Window with diff --git a/Examples/UICatalog/UICatalog.cs b/Examples/UICatalog/UICatalog.cs index 8b8eeca71..884e65fd1 100644 --- a/Examples/UICatalog/UICatalog.cs +++ b/Examples/UICatalog/UICatalog.cs @@ -246,7 +246,7 @@ public class UICatalog /// /// Shows the UI Catalog selection UI. When the user selects a Scenario to run, the UI Catalog main app UI is - /// killed and the Scenario is run as though it were Application.Current. When the Scenario exits, this function exits. + /// killed and the Scenario is run as though it were Application.TopRunnable. When the Scenario exits, this function exits. /// /// private static Scenario RunUICatalogTopLevel () @@ -347,7 +347,7 @@ public class UICatalog private static void ConfigFileChanged (object sender, FileSystemEventArgs e) { - if (Application.Current == null) + if (Application.TopRunnable == null) { return; } diff --git a/Examples/UICatalog/UICatalogTop.cs b/Examples/UICatalog/UICatalogTop.cs index 63c3eb528..304876a87 100644 --- a/Examples/UICatalog/UICatalogTop.cs +++ b/Examples/UICatalog/UICatalogTop.cs @@ -700,7 +700,7 @@ public class UICatalogTop : Toplevel _disableMouseCb!.CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked; _force16ColorsShortcutCb!.CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked; - Application.Current?.SetNeedsDraw (); + Application.TopRunnable?.SetNeedsDraw (); } private void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a) { ConfigApplied (); } diff --git a/Terminal.Gui/App/Application.Current.cs b/Terminal.Gui/App/Application.Current.cs index e94d074e7..1b91a45ff 100644 --- a/Terminal.Gui/App/Application.Current.cs +++ b/Terminal.Gui/App/Application.Current.cs @@ -7,12 +7,12 @@ public static partial class Application // Current handling /// [Obsolete ("The legacy static Application object is going away.")] public static ConcurrentStack SessionStack => ApplicationImpl.Instance.SessionStack; - /// The that is currently active. - /// The current toplevel. + /// The that is on the top of the . + /// The top runnable. [Obsolete ("The legacy static Application object is going away.")] - public static Toplevel? Current + public static Toplevel? TopRunnable { - get => ApplicationImpl.Instance.Current; - internal set => ApplicationImpl.Instance.Current = value; + get => ApplicationImpl.Instance.TopRunnable; + internal set => ApplicationImpl.Instance.TopRunnable = value; } } diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs index e20fd7ce8..3e08a3f72 100644 --- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs +++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs @@ -178,21 +178,21 @@ public partial class ApplicationImpl #if DEBUG_IDISPOSABLE - // Don't dispose the Current. It's up to caller dispose it - if (View.EnableDebugIDisposableAsserts && !ignoreDisposed && Current is { }) + // Don't dispose the TopRunnable. It's up to caller dispose it + if (View.EnableDebugIDisposableAsserts && !ignoreDisposed && TopRunnable is { }) { - Debug.Assert (Current.WasDisposed, $"Title = {Current.Title}, Id = {Current.Id}"); + Debug.Assert (TopRunnable.WasDisposed, $"Title = {TopRunnable.Title}, Id = {TopRunnable.Id}"); // If End wasn't called _CachedSessionTokenToplevel may be null if (CachedSessionTokenToplevel is { }) { Debug.Assert (CachedSessionTokenToplevel.WasDisposed); - Debug.Assert (CachedSessionTokenToplevel == Current); + Debug.Assert (CachedSessionTokenToplevel == TopRunnable); } } #endif - Current = null; + TopRunnable = null; CachedSessionTokenToplevel = null; // === 4. Clean up driver === diff --git a/Terminal.Gui/App/ApplicationImpl.Run.cs b/Terminal.Gui/App/ApplicationImpl.Run.cs index 4e9f59645..f5b785eb2 100644 --- a/Terminal.Gui/App/ApplicationImpl.Run.cs +++ b/Terminal.Gui/App/ApplicationImpl.Run.cs @@ -36,28 +36,28 @@ public partial class ApplicationImpl var rs = new SessionToken (toplevel); #if DEBUG_IDISPOSABLE - if (View.EnableDebugIDisposableAsserts && Current is { } && toplevel != Current && !SessionStack.Contains (Current)) + if (View.EnableDebugIDisposableAsserts && TopRunnable is { } && toplevel != TopRunnable && !SessionStack.Contains (TopRunnable)) { - // This assertion confirm if the Current was already disposed - Debug.Assert (Current.WasDisposed); - Debug.Assert (Current == CachedSessionTokenToplevel); + // This assertion confirm if the TopRunnable was already disposed + Debug.Assert (TopRunnable.WasDisposed); + Debug.Assert (TopRunnable == CachedSessionTokenToplevel); } #endif lock (SessionStack) { - if (Current is { } && toplevel != Current && !SessionStack.Contains (Current)) + if (TopRunnable is { } && toplevel != TopRunnable && !SessionStack.Contains (TopRunnable)) { - // If Current was already disposed and isn't on the Toplevels Stack, + // If TopRunnable was already disposed and isn't on the Toplevels Stack, // clean it up here if is the same as _CachedSessionTokenToplevel - if (Current == CachedSessionTokenToplevel) + if (TopRunnable == CachedSessionTokenToplevel) { - Current = null; + TopRunnable = null; } else { // Probably this will never hit - throw new ObjectDisposedException (Current.GetType ().FullName); + throw new ObjectDisposedException (TopRunnable.GetType ().FullName); } } @@ -89,35 +89,35 @@ public partial class ApplicationImpl } } - if (Current is null) + if (TopRunnable is null) { toplevel.App = this; - Current = toplevel; + TopRunnable = toplevel; } - if ((Current?.Modal == false && toplevel.Modal) - || (Current?.Modal == false && !toplevel.Modal) - || (Current?.Modal == true && toplevel.Modal)) + if ((TopRunnable?.Modal == false && toplevel.Modal) + || (TopRunnable?.Modal == false && !toplevel.Modal) + || (TopRunnable?.Modal == true && toplevel.Modal)) { if (toplevel.Visible) { - if (Current is { HasFocus: true }) + if (TopRunnable is { HasFocus: true }) { - Current.HasFocus = false; + TopRunnable.HasFocus = false; } - // Force leave events for any entered views in the old Current + // Force leave events for any entered views in the old TopRunnable if (Mouse.LastMousePosition is { }) { Mouse.RaiseMouseEnterLeaveEvents (Mouse.LastMousePosition!.Value, new ()); } - Current?.OnDeactivate (toplevel); - Toplevel previousTop = Current!; + TopRunnable?.OnDeactivate (toplevel); + Toplevel previousTop = TopRunnable!; - Current = toplevel; - Current.App = this; - Current.OnActivate (previousTop); + TopRunnable = toplevel; + TopRunnable.App = this; + TopRunnable.OnActivate (previousTop); } } @@ -194,11 +194,11 @@ public partial class ApplicationImpl throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view"); } - Current = view; + TopRunnable = view; SessionToken rs = Begin (view); - Current.Running = true; + TopRunnable.Running = true; var firstIteration = true; @@ -254,8 +254,8 @@ public partial class ApplicationImpl if (SessionStack.TryPeek (out Toplevel? newTop)) { newTop.App = this; - Current = newTop; - Current?.SetNeedsDraw (); + TopRunnable = newTop; + TopRunnable?.SetNeedsDraw (); } if (sessionToken.Toplevel is { HasFocus: true }) @@ -263,9 +263,9 @@ public partial class ApplicationImpl sessionToken.Toplevel.HasFocus = false; } - if (Current is { HasFocus: false }) + if (TopRunnable is { HasFocus: false }) { - Current.SetFocus (); + TopRunnable.SetFocus (); } CachedSessionTokenToplevel = sessionToken.Toplevel; @@ -285,9 +285,9 @@ public partial class ApplicationImpl /// public void RequestStop (Toplevel? top) { - Logging.Trace ($"Current: '{(top is { } ? top : "null")}'"); + Logging.Trace ($"TopRunnable: '{(top is { } ? top : "null")}'"); - top ??= Current; + top ??= TopRunnable; if (top == null) { @@ -327,7 +327,7 @@ public partial class ApplicationImpl public void Invoke (Action? action) { // If we are already on the main UI thread - if (Current is { Running: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId) + if (TopRunnable is { Running: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId) { action?.Invoke (this); @@ -350,7 +350,7 @@ public partial class ApplicationImpl public void Invoke (Action action) { // If we are already on the main UI thread - if (Current is { Running: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId) + if (TopRunnable is { Running: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId) { action?.Invoke (); diff --git a/Terminal.Gui/App/ApplicationImpl.cs b/Terminal.Gui/App/ApplicationImpl.cs index bcedf0b14..1aca088dd 100644 --- a/Terminal.Gui/App/ApplicationImpl.cs +++ b/Terminal.Gui/App/ApplicationImpl.cs @@ -121,19 +121,19 @@ public partial class ApplicationImpl : IApplication set => _navigation = value ?? throw new ArgumentNullException (nameof (value)); } - private Toplevel? _current; + private Toplevel? _topRunnable; /// - public Toplevel? Current + public Toplevel? TopRunnable { - get => _current; + get => _topRunnable; set { - _current = value; + _topRunnable = value; - if (_current is { }) + if (_topRunnable is { }) { - _current.App = this; + _topRunnable.App = this; } } } diff --git a/Terminal.Gui/App/ApplicationNavigation.cs b/Terminal.Gui/App/ApplicationNavigation.cs index d0ef40d9c..1149c3ad6 100644 --- a/Terminal.Gui/App/ApplicationNavigation.cs +++ b/Terminal.Gui/App/ApplicationNavigation.cs @@ -113,6 +113,6 @@ public class ApplicationNavigation { return visiblePopover.AdvanceFocus (direction, behavior); } - return App?.Current is { } && App.Current.AdvanceFocus (direction, behavior); + return App?.TopRunnable is { } && App.TopRunnable.AdvanceFocus (direction, behavior); } } diff --git a/Terminal.Gui/App/ApplicationPopover.cs b/Terminal.Gui/App/ApplicationPopover.cs index 72f9957df..3a7aac879 100644 --- a/Terminal.Gui/App/ApplicationPopover.cs +++ b/Terminal.Gui/App/ApplicationPopover.cs @@ -41,8 +41,8 @@ public sealed class ApplicationPopover : IDisposable { if (popover is { } && !IsRegistered (popover)) { - // When created, set IPopover.Toplevel to the current Application.Current - popover.Current ??= App?.Current; + // When created, set IPopover.Toplevel to the current Application.TopRunnable + popover.Current ??= App?.TopRunnable; if (popover is View popoverView) { @@ -166,7 +166,7 @@ public sealed class ApplicationPopover : IDisposable { _activePopover = null; popoverView.Visible = false; - popoverView.App?.Current?.SetNeedsDraw (); + popoverView.App?.TopRunnable?.SetNeedsDraw (); } } @@ -215,7 +215,7 @@ public sealed class ApplicationPopover : IDisposable { if (popover == activePopover || popover is not View popoverView - || (popover.Current is { } && popover.Current != App?.Current)) + || (popover.Current is { } && popover.Current != App?.TopRunnable)) { continue; } diff --git a/Terminal.Gui/App/IApplication.cs b/Terminal.Gui/App/IApplication.cs index c02563170..a14f3534b 100644 --- a/Terminal.Gui/App/IApplication.cs +++ b/Terminal.Gui/App/IApplication.cs @@ -301,14 +301,14 @@ public interface IApplication /// /// This will cause to return. /// - /// This is equivalent to calling with as the parameter. + /// This is equivalent to calling with as the parameter. /// /// void RequestStop (); /// Requests that the currently running Session stop. The Session will stop after the current iteration completes. /// - /// The to stop. If , stops the currently running . + /// The to stop. If , stops the currently running . /// /// /// This will cause to return. @@ -356,13 +356,14 @@ public interface IApplication #region Toplevel Management - /// Gets or sets the currently active Toplevel. + /// Gets or sets the Toplevel that is on the top of the . /// /// + /// The top runnable in the session stack captures all mouse and keyboard input. /// This is set by and cleared by . /// /// - Toplevel? Current { get; set; } + Toplevel? TopRunnable { get; set; } /// Gets the stack of all active Toplevel sessions. /// @@ -433,7 +434,7 @@ public interface IApplication /// /// /// This is typically set to when a View's changes and that view - /// has no SuperView (e.g. when is moved or resized). + /// has no SuperView (e.g. when is moved or resized). /// /// /// Automatically reset to after processes it. diff --git a/Terminal.Gui/App/IPopover.cs b/Terminal.Gui/App/IPopover.cs index 87218d07a..816c69613 100644 --- a/Terminal.Gui/App/IPopover.cs +++ b/Terminal.Gui/App/IPopover.cs @@ -55,7 +55,7 @@ public interface IPopover /// events from the . If set, it will only receive keyboard events the Toplevel would normally /// receive. /// When is called, the is set to the current - /// if not already set. + /// if not already set. /// Toplevel? Current { get; set; } } diff --git a/Terminal.Gui/App/Keyboard/KeyboardImpl.cs b/Terminal.Gui/App/Keyboard/KeyboardImpl.cs index e1be672e5..75c37d4b8 100644 --- a/Terminal.Gui/App/Keyboard/KeyboardImpl.cs +++ b/Terminal.Gui/App/Keyboard/KeyboardImpl.cs @@ -138,7 +138,7 @@ internal class KeyboardImpl : IKeyboard return true; } - if (App?.Current is null) + if (App?.TopRunnable is null) { if (App?.SessionStack is { }) { @@ -158,7 +158,7 @@ internal class KeyboardImpl : IKeyboard } else { - if (App.Current.NewKeyDownEvent (key)) + if (App.TopRunnable.NewKeyDownEvent (key)) { return true; } diff --git a/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs b/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs index df03c3187..e52af3f0e 100644 --- a/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs +++ b/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs @@ -82,7 +82,7 @@ public class ApplicationMainLoop : IApplicationMainLoop - /// Handles raising events and setting required draw status etc when changes + /// Handles raising events and setting required draw status etc when changes /// public IToplevelTransitionManager ToplevelTransitionManager = new ToplevelTransitionManager (); @@ -145,10 +145,10 @@ public class ApplicationMainLoop : IApplicationMainLoop : IApplicationMainLoop? currentViewsUnderMouse = App?.Current?.GetViewsUnderLocation (mouseEvent.ScreenPosition, ViewportSettingsFlags.TransparentMouse); + List? currentViewsUnderMouse = App?.TopRunnable?.GetViewsUnderLocation (mouseEvent.ScreenPosition, ViewportSettingsFlags.TransparentMouse); View? deepestViewUnderMouse = currentViewsUnderMouse?.LastOrDefault (); @@ -114,9 +114,9 @@ internal class MouseImpl : IMouse return; } - // if the mouse is outside the Application.Current or Popover hierarchy, we don't want to + // if the mouse is outside the Application.TopRunnable or Popover hierarchy, we don't want to // send the mouse event to the deepest view under the mouse. - if (!View.IsInHierarchy (App?.Current, deepestViewUnderMouse, true) && !View.IsInHierarchy (App?.Popover?.GetActivePopover () as View, deepestViewUnderMouse, true)) + if (!View.IsInHierarchy (App?.TopRunnable, deepestViewUnderMouse, true) && !View.IsInHierarchy (App?.Popover?.GetActivePopover () as View, deepestViewUnderMouse, true)) { return; } diff --git a/Terminal.Gui/App/PopoverBaseImpl.cs b/Terminal.Gui/App/PopoverBaseImpl.cs index 41258401f..70f771de1 100644 --- a/Terminal.Gui/App/PopoverBaseImpl.cs +++ b/Terminal.Gui/App/PopoverBaseImpl.cs @@ -119,7 +119,7 @@ public abstract class PopoverBaseImpl : View, IPopover // Whenever visible is changing to false, we need to reset the focus if (ApplicationNavigation.IsInHierarchy (this, App?.Navigation?.GetFocused ())) { - App?.Navigation?.SetFocused (App?.Current?.MostFocused); + App?.Navigation?.SetFocused (App?.TopRunnable?.MostFocused); } } diff --git a/Terminal.Gui/App/Toplevel/ToplevelTransitionManager.cs b/Terminal.Gui/App/Toplevel/ToplevelTransitionManager.cs index 10166b450..edb09d0e9 100644 --- a/Terminal.Gui/App/Toplevel/ToplevelTransitionManager.cs +++ b/Terminal.Gui/App/Toplevel/ToplevelTransitionManager.cs @@ -13,7 +13,7 @@ public class ToplevelTransitionManager : IToplevelTransitionManager /// public void RaiseReadyEventIfNeeded (IApplication? app) { - Toplevel? top = app?.Current; + Toplevel? top = app?.TopRunnable; if (top != null && !_readiedTopLevels.Contains (top)) { @@ -29,13 +29,13 @@ public class ToplevelTransitionManager : IToplevelTransitionManager /// public void HandleTopMaybeChanging (IApplication? app) { - Toplevel? newTop = app?.Current; + Toplevel? newTop = app?.TopRunnable; if (_lastTop != null && _lastTop != newTop && newTop != null) { newTop.SetNeedsDraw (); } - _lastTop = app?.Current; + _lastTop = app?.TopRunnable; } } diff --git a/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs b/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs index f2e52fab8..3b2caefe4 100644 --- a/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs +++ b/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs @@ -657,7 +657,7 @@ public partial class Border if (Parent!.SuperView is null) { // Redraw the entire app window. - App?.Current?.SetNeedsDraw (); + App?.TopRunnable?.SetNeedsDraw (); } else { diff --git a/Terminal.Gui/ViewBase/View.Hierarchy.cs b/Terminal.Gui/ViewBase/View.Hierarchy.cs index b88a07199..0b18e4e3e 100644 --- a/Terminal.Gui/ViewBase/View.Hierarchy.cs +++ b/Terminal.Gui/ViewBase/View.Hierarchy.cs @@ -362,12 +362,12 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, #endregion AddRemove - // TODO: This drives a weird coupling of Application.Current and View. It's not clear why this is needed. + // TODO: This drives a weird coupling of Application.TopRunnable and View. It's not clear why this is needed. /// Get the top superview of a given . /// The superview view. internal View? GetTopSuperView (View? view = null, View? superview = null) { - View? top = superview ?? App?.Current; + View? top = superview ?? App?.TopRunnable; for (View? v = view?.SuperView ?? this?.SuperView; v != null; v = v.SuperView) { diff --git a/Terminal.Gui/ViewBase/View.Layout.cs b/Terminal.Gui/ViewBase/View.Layout.cs index ecd58e737..76135f600 100644 --- a/Terminal.Gui/ViewBase/View.Layout.cs +++ b/Terminal.Gui/ViewBase/View.Layout.cs @@ -437,7 +437,7 @@ public partial class View // Layout APIs private void NeedsClearScreenNextIteration () { - if (App is { Current: { } } && App.Current == this && App.SessionStack.Count == 1) + if (App is { TopRunnable: { } } && App.TopRunnable == this && App.SessionStack.Count == 1) { // If this is the only TopLevel, we need to redraw the screen App.ClearScreenNextIteration = true; @@ -1113,8 +1113,8 @@ public partial class View // Layout APIs { // TODO: Get rid of refs to Top Size superViewContentSize = SuperView?.GetContentSize () - ?? (App?.Current is { } && App?.Current != this && App!.Current.IsInitialized - ? App.Current.GetContentSize () + ?? (App?.TopRunnable is { } && App?.TopRunnable != this && App!.TopRunnable.IsInitialized + ? App.TopRunnable.GetContentSize () : App?.Screen.Size ?? new (2048, 2048)); return superViewContentSize; @@ -1130,7 +1130,7 @@ public partial class View // Layout APIs /// /// /// If does not have a or it's SuperView is not - /// the position will be bound by . + /// the position will be bound by . /// /// The View that is to be moved. /// The target x location. @@ -1138,7 +1138,7 @@ public partial class View // Layout APIs /// The new x location that will ensure will be fully visible. /// The new y location that will ensure will be fully visible. /// - /// Either (if does not have a Super View) or + /// Either (if does not have a Super View) or /// 's SuperView. This can be used to ensure LayoutSubViews is called on the correct View. /// internal static View? GetLocationEnsuringFullVisibility ( @@ -1154,10 +1154,10 @@ public partial class View // Layout APIs IApplication? app = viewToMove.App; - if (viewToMove?.SuperView is null || viewToMove == app?.Current || viewToMove?.SuperView == app?.Current) + if (viewToMove?.SuperView is null || viewToMove == app?.TopRunnable || viewToMove?.SuperView == app?.TopRunnable) { maxDimension = app?.Screen.Width ?? 0; - superView = app?.Current; + superView = app?.TopRunnable; } else { @@ -1190,9 +1190,9 @@ public partial class View // Layout APIs var menuVisible = false; var statusVisible = false; - if (viewToMove?.SuperView is null || viewToMove == app?.Current || viewToMove?.SuperView == app?.Current) + if (viewToMove?.SuperView is null || viewToMove == app?.TopRunnable || viewToMove?.SuperView == app?.TopRunnable) { - menuVisible = app?.Current?.MenuBar?.Visible == true; + menuVisible = app?.TopRunnable?.MenuBar?.Visible == true; } else { @@ -1209,7 +1209,7 @@ public partial class View // Layout APIs } } - if (viewToMove?.SuperView is null || viewToMove == app?.Current || viewToMove?.SuperView == app?.Current) + if (viewToMove?.SuperView is null || viewToMove == app?.TopRunnable || viewToMove?.SuperView == app?.TopRunnable) { maxDimension = menuVisible ? 1 : 0; } @@ -1220,7 +1220,7 @@ public partial class View // Layout APIs ny = Math.Max (targetY, maxDimension); - if (viewToMove?.SuperView is null || viewToMove == app?.Current || viewToMove?.SuperView == app?.Current) + if (viewToMove?.SuperView is null || viewToMove == app?.TopRunnable || viewToMove?.SuperView == app?.TopRunnable) { if (app is { }) { @@ -1310,7 +1310,7 @@ public partial class View // Layout APIs } } - if (toplevel == App.Current) + if (toplevel == App.TopRunnable) { checkedTop = true; } @@ -1318,7 +1318,7 @@ public partial class View // Layout APIs } // Fallback: If TopLevels is empty or Top is not in TopLevels, check Top directly (for test compatibility) - if (!checkedTop && App?.Current is { Visible: true } top) + if (!checkedTop && App?.TopRunnable is { Visible: true } top) { // For root toplevels, allow hit-testing even if location is outside bounds (for drag/move) List result = GetViewsUnderLocation (top, screenLocation, excludeViewportSettingsFlags); diff --git a/Terminal.Gui/ViewBase/View.Navigation.cs b/Terminal.Gui/ViewBase/View.Navigation.cs index 5ff60aa7e..fb6f13030 100644 --- a/Terminal.Gui/ViewBase/View.Navigation.cs +++ b/Terminal.Gui/ViewBase/View.Navigation.cs @@ -395,7 +395,7 @@ public partial class View // Focus and cross-view navigation management (TabStop public event EventHandler? FocusedChanged; /// Returns a value indicating if this View is currently on Top (Active) - public bool IsCurrentTop => App?.Current == this; + public bool IsCurrentTop => App?.TopRunnable == this; /// /// Returns the most focused SubView down the subview-hierarchy. @@ -853,18 +853,18 @@ public partial class View // Focus and cross-view navigation management (TabStop } } - // Application.Current? - if (newFocusedView is null && App?.Current is { CanFocus: true, HasFocus: false }) + // Application.TopRunnable? + if (newFocusedView is null && App?.TopRunnable is { CanFocus: true, HasFocus: false }) { // Temporarily ensure this view can't get focus bool prevCanFocus = _canFocus; _canFocus = false; - bool restoredFocus = App?.Current.RestoreFocus () ?? false; + bool restoredFocus = App?.TopRunnable.RestoreFocus () ?? false; _canFocus = prevCanFocus; - if (App?.Current is { CanFocus: true, HasFocus: true }) + if (App?.TopRunnable is { CanFocus: true, HasFocus: true }) { - newFocusedView = App?.Current; + newFocusedView = App?.TopRunnable; } else if (restoredFocus) { diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index 4037d7312..1a5b4b362 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -21,7 +21,7 @@ public class Dialog : Window /// /// By default, , , , and are /// set - /// such that the will be centered in, and no larger than 90% of , if + /// such that the will be centered in, and no larger than 90% of , if /// there is one. Otherwise, /// it will be bound by the screen dimensions. /// diff --git a/Terminal.Gui/Views/Menuv1/Menu.cs b/Terminal.Gui/Views/Menuv1/Menu.cs index f538af7dd..61b58649a 100644 --- a/Terminal.Gui/Views/Menuv1/Menu.cs +++ b/Terminal.Gui/Views/Menuv1/Menu.cs @@ -12,10 +12,10 @@ internal sealed class Menu : View { public Menu () { - if (Application.Current is { }) + if (Application.TopRunnable is { }) { - Application.Current.DrawComplete += Top_DrawComplete; - Application.Current.SizeChanging += Current_TerminalResized; + Application.TopRunnable.DrawComplete += Top_DrawComplete; + Application.TopRunnable.SizeChanging += Current_TerminalResized; } Application.MouseEvent += Application_RootMouseEvent; @@ -231,10 +231,10 @@ internal sealed class Menu : View { RemoveKeyBindingsHotKey (_barItems); - if (Application.Current is { }) + if (Application.TopRunnable is { }) { - Application.Current.DrawComplete -= Top_DrawComplete; - Application.Current.SizeChanging -= Current_TerminalResized; + Application.TopRunnable.DrawComplete -= Top_DrawComplete; + Application.TopRunnable.SizeChanging -= Current_TerminalResized; } Application.MouseEvent -= Application_RootMouseEvent; diff --git a/Terminal.Gui/Views/Menuv1/MenuBar.cs b/Terminal.Gui/Views/Menuv1/MenuBar.cs index 78d1c3a29..2c09e6c56 100644 --- a/Terminal.Gui/Views/Menuv1/MenuBar.cs +++ b/Terminal.Gui/Views/Menuv1/MenuBar.cs @@ -421,7 +421,7 @@ public class MenuBar : View, IDesignable _selected = 0; SetNeedsDraw (); - _previousFocused = (SuperView is null ? Application.Current?.Focused : SuperView.Focused)!; + _previousFocused = (SuperView is null ? Application.TopRunnable?.Focused : SuperView.Focused)!; OpenMenu (_selected); if (!SelectEnabledItem ( @@ -490,7 +490,7 @@ public class MenuBar : View, IDesignable if (_openMenu is null) { - _previousFocused = (SuperView is null ? Application.Current?.Focused ?? null : SuperView.Focused)!; + _previousFocused = (SuperView is null ? Application.TopRunnable?.Focused ?? null : SuperView.Focused)!; } OpenMenu (idx, sIdx, subMenu); @@ -702,7 +702,7 @@ public class MenuBar : View, IDesignable } Rectangle superViewFrame = SuperView?.Frame ?? Application.Screen; - View? sv = SuperView ?? Application.Current; + View? sv = SuperView ?? Application.TopRunnable; if (sv is null) { @@ -834,7 +834,7 @@ public class MenuBar : View, IDesignable { case null: // Open a submenu below a MenuBar - _lastFocused ??= SuperView is null ? Application.Current?.MostFocused : SuperView.MostFocused; + _lastFocused ??= SuperView is null ? Application.TopRunnable?.MostFocused : SuperView.MostFocused; if (_openSubMenu is { } && !CloseMenu (false, true)) { diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index fedc501f4..6f9bb2268 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -13,7 +13,7 @@ namespace Terminal.Gui.Views; /// /// /// A Toplevel is created when an application initializes Terminal.Gui by calling . -/// The application Toplevel can be accessed via . Additional Toplevels can be created +/// The application Toplevel can be accessed via . Additional Toplevels can be created /// and run (e.g. s). To run a Toplevel, create the and call /// . /// @@ -152,7 +152,7 @@ public partial class Toplevel : View /// public virtual void RequestStop () { - App?.RequestStop (App?.Current); + App?.RequestStop (App?.TopRunnable); } /// @@ -244,7 +244,7 @@ public partial class Toplevel : View } // BUGBUG: The && true is a temp hack - if ((superView != top || top?.SuperView is { } || (top != App?.Current && top!.Modal) || (top == App?.Current && top?.SuperView is null)) + if ((superView != top || top?.SuperView is { } || (top != App?.TopRunnable && top!.Modal) || (top == App?.TopRunnable && top?.SuperView is null)) && (top!.Frame.X + top.Frame.Width > maxWidth || ny > top.Frame.Y)) { diff --git a/Terminal.Gui/Views/Wizard/Wizard.cs b/Terminal.Gui/Views/Wizard/Wizard.cs index fe3cfcc63..50b21c7ed 100644 --- a/Terminal.Gui/Views/Wizard/Wizard.cs +++ b/Terminal.Gui/Views/Wizard/Wizard.cs @@ -42,7 +42,7 @@ namespace Terminal.Gui.Views; /// Application.RequestStop(); /// }; /// -/// Application.Current.Add (wizard); +/// Application.TopRunnable.Add (wizard); /// Application.Run (); /// Application.Shutdown (); /// diff --git a/Tests/IntegrationTests/FluentTests/GuiTestContextKeyEventTests.cs b/Tests/IntegrationTests/FluentTests/GuiTestContextKeyEventTests.cs index 51097c74c..2e1efe92e 100644 --- a/Tests/IntegrationTests/FluentTests/GuiTestContextKeyEventTests.cs +++ b/Tests/IntegrationTests/FluentTests/GuiTestContextKeyEventTests.cs @@ -16,9 +16,9 @@ public class GuiTestContextKeyEventTests (ITestOutputHelper outputHelper) public void QuitKey_ViaApplication_Stops (TestDriver d) { using GuiTestContext context = With.A (40, 10, d); - Assert.True (context.App?.Current!.Running); + Assert.True (context.App?.TopRunnable!.Running); - Toplevel? top = context.App?.Current; + Toplevel? top = context.App?.TopRunnable; context.Then ((_) => context!.App?.Keyboard.RaiseKeyDownEvent (Application.QuitKey)); Assert.False (top!.Running); } @@ -28,9 +28,9 @@ public class GuiTestContextKeyEventTests (ITestOutputHelper outputHelper) public void QuitKey_ViaEnqueueKey_Stops (TestDriver d) { using GuiTestContext context = With.A (40, 10, d, _out); - Assert.True (context.App?.Current!.Running); + Assert.True (context.App?.TopRunnable!.Running); - Toplevel? top = context.App?.Current; + Toplevel? top = context.App?.TopRunnable; context.EnqueueKeyEvent (Application.QuitKey); Assert.False (top!.Running); diff --git a/Tests/IntegrationTests/FluentTests/GuiTestContextTests.cs b/Tests/IntegrationTests/FluentTests/GuiTestContextTests.cs index 30ea80cd8..e78161774 100644 --- a/Tests/IntegrationTests/FluentTests/GuiTestContextTests.cs +++ b/Tests/IntegrationTests/FluentTests/GuiTestContextTests.cs @@ -43,7 +43,7 @@ public class GuiTestContextTests (ITestOutputHelper outputHelper) public void With_New_A_Runs (TestDriver d) { using GuiTestContext context = With.A (40, 10, d, _out); - Assert.True (context.App!.Current!.Running); + Assert.True (context.App!.TopRunnable!.Running); Assert.NotEqual (Rectangle.Empty, context.App!.Screen); } @@ -54,9 +54,9 @@ public class GuiTestContextTests (ITestOutputHelper outputHelper) using GuiTestContext context = With.A (10, 3, d, _out) .Then ((app) => { - app.Current!.BorderStyle = LineStyle.None; - app.Current!.Border!.Thickness = Thickness.Empty; - app.Current.Text = "hello"; + app.TopRunnable!.BorderStyle = LineStyle.None; + app.TopRunnable!.Border!.Thickness = Thickness.Empty; + app.TopRunnable.Text = "hello"; }) .ScreenShot ("ScreenShot", _out) .AnsiScreenShot ("AnsiScreenShot", _out) diff --git a/Tests/IntegrationTests/FluentTests/MenuBarv2Tests.cs b/Tests/IntegrationTests/FluentTests/MenuBarv2Tests.cs index bc96b6f95..e0ca97b45 100644 --- a/Tests/IntegrationTests/FluentTests/MenuBarv2Tests.cs +++ b/Tests/IntegrationTests/FluentTests/MenuBarv2Tests.cs @@ -143,7 +143,7 @@ public class MenuBarv2Tests .Then ((app) => { menuBar = new MenuBarv2 (); - top = app.Current!; + top = app.TopRunnable!; top.Add ( new View () @@ -153,7 +153,7 @@ public class MenuBarv2Tests }); menuBar.EnableForDesign (ref top); - app.Current!.Add (menuBar); + app.TopRunnable!.Add (menuBar); }) .WaitIteration () .AssertIsNotType (top?.App?.Navigation!.GetFocused ()) @@ -178,7 +178,7 @@ public class MenuBarv2Tests { app = a; menuBar = new MenuBarv2 (); - Toplevel top = app.Current!; + Toplevel top = app.TopRunnable!; top.Add ( new View () @@ -188,7 +188,7 @@ public class MenuBarv2Tests }); menuBar.EnableForDesign (ref top); - app.Current!.Add (menuBar); + app.TopRunnable!.Add (menuBar); }) .WaitIteration () .AssertIsNotType (app?.Navigation!.GetFocused ()) @@ -266,10 +266,10 @@ public class MenuBarv2Tests .Then ((app) => { var menuBar = new MenuBarv2 (); - app.Current!.Add (menuBar); + app.TopRunnable!.Add (menuBar); // Call EnableForDesign - Toplevel top = app.Current!; + Toplevel top = app.TopRunnable!; bool result = menuBar.EnableForDesign (ref top); // Should return true @@ -302,9 +302,9 @@ public class MenuBarv2Tests { app = a; menuBar = new MenuBarv2 (); - Toplevel top = app.Current!; + Toplevel top = app.TopRunnable!; menuBar.EnableForDesign (ref top); - app.Current!.Add (menuBar); + app.TopRunnable!.Add (menuBar); }) .WaitIteration () .ScreenShot ("MenuBar initial state", _out) @@ -342,7 +342,7 @@ public class MenuBarv2Tests { app = a; menuBar = new MenuBarv2 (); - Toplevel top = app.Current!; + Toplevel top = app.TopRunnable!; top.Add ( new View () @@ -352,7 +352,7 @@ public class MenuBarv2Tests }); menuBar.EnableForDesign (ref top); - app.Current!.Add (menuBar); + app.TopRunnable!.Add (menuBar); }) .AssertIsNotType (app!.Navigation!.GetFocused ()) .ScreenShot ("MenuBar initial state", _out) @@ -386,9 +386,9 @@ public class MenuBarv2Tests { app = a; menuBar = new MenuBarv2 (); - Toplevel? toplevel = app.Current; + Toplevel? toplevel = app.TopRunnable; menuBar.EnableForDesign (ref toplevel!); - app.Current!.Add (menuBar); + app.TopRunnable!.Add (menuBar); }) .WaitIteration () .AssertIsNotType (app?.Navigation!.GetFocused ()) @@ -417,7 +417,7 @@ public class MenuBarv2Tests { app = a; menuBar = new MenuBarv2 (); - Toplevel top = app.Current!; + Toplevel top = app.TopRunnable!; top.Add ( new View () @@ -427,18 +427,18 @@ public class MenuBarv2Tests }); menuBar.EnableForDesign (ref top); - app.Current!.Add (menuBar); + app.TopRunnable!.Add (menuBar); }) .WaitIteration () .AssertIsNotType (app!.Navigation!.GetFocused ()) .ScreenShot ("MenuBar initial state", _out) .EnqueueKeyEvent (MenuBarv2.DefaultKey) .AssertEqual ("_New file", app.Navigation!.GetFocused ()!.Title) - .AssertTrue (app?.Current!.Running) + .AssertTrue (app?.TopRunnable!.Running) .ScreenShot ($"After {MenuBarv2.DefaultKey}", _out) .EnqueueKeyEvent (Application.QuitKey) .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) - .AssertTrue (app!.Current!.Running); + .AssertTrue (app!.TopRunnable!.Running); } [Theory] @@ -453,7 +453,7 @@ public class MenuBarv2Tests { app = a; menuBar = new MenuBarv2 (); - Toplevel top = app.Current!; + Toplevel top = app.TopRunnable!; top.Add ( new View () @@ -470,7 +470,7 @@ public class MenuBarv2Tests item.Key = Key.Empty; } - app.Current!.Add (menuBar); + app.TopRunnable!.Add (menuBar); }) .WaitIteration () .AssertIsNotType (app?.Navigation!.GetFocused ()) @@ -480,7 +480,7 @@ public class MenuBarv2Tests .ScreenShot ($"After {MenuBarv2.DefaultKey}", _out) .EnqueueKeyEvent (Application.QuitKey) .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) - .AssertTrue (app?.Current!.Running); + .AssertTrue (app?.TopRunnable!.Running); } [Theory] @@ -506,9 +506,9 @@ public class MenuBarv2Tests .Then ((a) => { var menuBar = new MenuBarv2 (); - Toplevel top = a.Current!; + Toplevel top = a.TopRunnable!; menuBar.EnableForDesign (ref top); - a.Current!.Add (menuBar); + a.TopRunnable!.Add (menuBar); }) .Add (testView) .WaitIteration () @@ -540,9 +540,9 @@ public class MenuBarv2Tests .Then ((a) => { var menuBar = new MenuBarv2 (); - Toplevel top = a.Current!; + Toplevel top = a.TopRunnable!; menuBar.EnableForDesign (ref top); - a.Current!.Add (menuBar); + a.TopRunnable!.Add (menuBar); }) .Add (testView) .WaitIteration () diff --git a/Tests/IntegrationTests/FluentTests/NavigationTests.cs b/Tests/IntegrationTests/FluentTests/NavigationTests.cs index fe74c59ea..3796a741f 100644 --- a/Tests/IntegrationTests/FluentTests/NavigationTests.cs +++ b/Tests/IntegrationTests/FluentTests/NavigationTests.cs @@ -29,8 +29,8 @@ public class NavigationTests (ITestOutputHelper outputHelper) w2.Add (v3, v4); var w3 = new Window { Id = "w3" }; w3.Add (v5, v6); - Toplevel top = app?.Current!; - app?.Current!.Add (w1, w2, w3); + Toplevel top = app?.TopRunnable!; + app?.TopRunnable!.Add (w1, w2, w3); }) .AssertTrue (v5.HasFocus) .EnqueueKeyEvent (Key.F6) diff --git a/Tests/IntegrationTests/FluentTests/PopverMenuTests.cs b/Tests/IntegrationTests/FluentTests/PopverMenuTests.cs index 83a173944..09ce0417d 100644 --- a/Tests/IntegrationTests/FluentTests/PopverMenuTests.cs +++ b/Tests/IntegrationTests/FluentTests/PopverMenuTests.cs @@ -26,10 +26,10 @@ public class PopoverMenuTests .Then ((app) => { PopoverMenu popoverMenu = new (); - app.Current!.Add (popoverMenu); + app.TopRunnable!.Add (popoverMenu); // Call EnableForDesign - Toplevel top = app.Current; + Toplevel top = app.TopRunnable; bool result = popoverMenu.EnableForDesign (ref top); // Should return true @@ -65,7 +65,7 @@ public class PopoverMenuTests }; // Call EnableForDesign - Toplevel top = app.Current!; + Toplevel top = app.TopRunnable!; popoverMenu.EnableForDesign (ref top); var view = new View @@ -76,7 +76,7 @@ public class PopoverMenuTests Id = "focusableView", Text = "View" }; - app.Current!.Add (view); + app.TopRunnable!.Add (view); // EnableForDesign sets to true; undo that popoverMenu.Visible = false; @@ -110,7 +110,7 @@ public class PopoverMenuTests }; // Call EnableForDesign - Toplevel top = app.Current!; + Toplevel top = app.TopRunnable!; bool result = popoverMenu.EnableForDesign (ref top); var view = new View @@ -121,7 +121,7 @@ public class PopoverMenuTests Id = "focusableView", Text = "View" }; - app.Current!.Add (view); + app.TopRunnable!.Add (view); // EnableForDesign sets to true; undo that popoverMenu.Visible = false; @@ -139,7 +139,7 @@ public class PopoverMenuTests .ScreenShot ($"After {Application.QuitKey}", _out) .AssertFalse (app?.Popover!.Popovers.Cast ().FirstOrDefault ()!.Visible) .AssertNull (app?.Popover!.GetActivePopover ()) - .AssertTrue (app?.Current!.Running); + .AssertTrue (app?.TopRunnable!.Running); } [Theory] @@ -157,7 +157,7 @@ public class PopoverMenuTests }; // Call EnableForDesign - Toplevel top = app.Current!; + Toplevel top = app.TopRunnable!; bool result = popoverMenu.EnableForDesign (ref top); var view = new View @@ -168,7 +168,7 @@ public class PopoverMenuTests Id = "focusableView", Text = "View" }; - app.Current!.Add (view); + app.TopRunnable!.Add (view); // EnableForDesign sets to true; undo that popoverMenu.Visible = false; @@ -206,7 +206,7 @@ public class PopoverMenuTests }; // Call EnableForDesign - Toplevel top = app.Current!; + Toplevel top = app.TopRunnable!; bool result = popoverMenu.EnableForDesign (ref top); var view = new View @@ -217,7 +217,7 @@ public class PopoverMenuTests Id = "focusableView", Text = "View" }; - app.Current!.Add (view); + app.TopRunnable!.Add (view); // EnableForDesign sets to true; undo that popoverMenu.Visible = false; @@ -231,11 +231,11 @@ public class PopoverMenuTests .Then ((_) => app?.Popover!.Show (app?.Popover.Popovers.First ())) .ScreenShot ("PopoverMenu after Show", _out) .AssertEqual ("Cu_t", app?.Navigation!.GetFocused ()!.Title) - .AssertTrue (app?.Current!.Running) + .AssertTrue (app?.TopRunnable!.Running) .EnqueueKeyEvent (Application.QuitKey) .ScreenShot ($"After {Application.QuitKey}", _out) .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) - .AssertTrue (app?.Current!.Running); + .AssertTrue (app?.TopRunnable!.Running); } [Theory] @@ -267,7 +267,7 @@ public class PopoverMenuTests { App = app }; - Toplevel top = app.Current!; + Toplevel top = app.TopRunnable!; popoverMenu.EnableForDesign (ref top); app?.Popover!.Register (popoverMenu); }) @@ -307,7 +307,7 @@ public class PopoverMenuTests { App = app }; - Toplevel top = app.Current!; + Toplevel top = app.TopRunnable!; popoverMenu.EnableForDesign (ref top); app?.Popover!.Register (popoverMenu); }) @@ -346,7 +346,7 @@ public class PopoverMenuTests { App = app }; - Toplevel top = app.Current!; + Toplevel top = app.TopRunnable!; popoverMenu.EnableForDesign (ref top); app?.Popover!.Register (popoverMenu); }) diff --git a/Tests/StressTests/ApplicationStressTests.cs b/Tests/StressTests/ApplicationStressTests.cs index 0feadf210..f63bbf9b9 100644 --- a/Tests/StressTests/ApplicationStressTests.cs +++ b/Tests/StressTests/ApplicationStressTests.cs @@ -71,8 +71,8 @@ public class ApplicationStressTests { int tbNow = _tbCounter; - // Wait for Application.Current to be running to ensure timed events can be processed - while (Application.Current is null || Application.Current is { Running: false }) + // Wait for Application.TopRunnable to be running to ensure timed events can be processed + while (Application.TopRunnable is null || Application.TopRunnable is { Running: false }) { Thread.Sleep (1); } diff --git a/Tests/StressTests/ScenariosStressTests.cs b/Tests/StressTests/ScenariosStressTests.cs index fead31b84..467bffbb4 100644 --- a/Tests/StressTests/ScenariosStressTests.cs +++ b/Tests/StressTests/ScenariosStressTests.cs @@ -126,7 +126,7 @@ public class ScenariosStressTests void OnApplicationSessionBegun (object? sender, SessionTokenEventArgs e) { - // Get a list of all subviews under Application.Current (and their subviews, etc.) + // Get a list of all subviews under Application.TopRunnable (and their subviews, etc.) // and subscribe to their DrawComplete event void SubscribeAllSubViews (View view) { @@ -140,7 +140,7 @@ public class ScenariosStressTests } } - SubscribeAllSubViews (Application.Current!); + SubscribeAllSubViews (Application.TopRunnable!); } // If the scenario doesn't close within the abort time, this will force it to quit diff --git a/Tests/TerminalGuiFluentTesting/FakeDriver/FakeApplicationLifecycle.cs b/Tests/TerminalGuiFluentTesting/FakeDriver/FakeApplicationLifecycle.cs index 5fced9338..f24764561 100644 --- a/Tests/TerminalGuiFluentTesting/FakeDriver/FakeApplicationLifecycle.cs +++ b/Tests/TerminalGuiFluentTesting/FakeDriver/FakeApplicationLifecycle.cs @@ -13,7 +13,7 @@ internal class FakeApplicationLifecycle (CancellationTokenSource hardStop) : IDi { hardStop.Cancel (); - Application.Current?.Dispose (); + Application.TopRunnable?.Dispose (); Application.Shutdown (); } } diff --git a/Tests/TerminalGuiFluentTesting/GuiTestContext.Navigation.cs b/Tests/TerminalGuiFluentTesting/GuiTestContext.Navigation.cs index 9bd8f5bc9..aa9522b4a 100644 --- a/Tests/TerminalGuiFluentTesting/GuiTestContext.Navigation.cs +++ b/Tests/TerminalGuiFluentTesting/GuiTestContext.Navigation.cs @@ -40,13 +40,13 @@ public partial class GuiTestContext public GuiTestContext Focus (Func? evaluator = null) where T : View { evaluator ??= _ => true; - Toplevel? t = App?.Current; + Toplevel? t = App?.TopRunnable; HashSet seen = new (); if (t == null) { - Fail ("Application.Current was null when trying to set focus"); + Fail ("Application.TopRunnable was null when trying to set focus"); return this; } diff --git a/Tests/TerminalGuiFluentTesting/GuiTestContext.ViewBase.cs b/Tests/TerminalGuiFluentTesting/GuiTestContext.ViewBase.cs index f96505840..ddcaef2fd 100644 --- a/Tests/TerminalGuiFluentTesting/GuiTestContext.ViewBase.cs +++ b/Tests/TerminalGuiFluentTesting/GuiTestContext.ViewBase.cs @@ -14,7 +14,7 @@ public partial class GuiTestContext { WaitIteration ((app) => { - Toplevel top = app.Current ?? throw new ("Top was null so could not add view"); + Toplevel top = app.TopRunnable ?? throw new ("Top was null so could not add view"); top.Add (v); top.Layout (); _lastView = v; @@ -28,15 +28,15 @@ public partial class GuiTestContext /// /// The last view added (e.g. with ) or the root/current top. /// - public View LastView => _lastView ?? App?.Current ?? throw new ("Could not determine which view to add to"); + public View LastView => _lastView ?? App?.TopRunnable ?? throw new ("Could not determine which view to add to"); private T Find (Func evaluator) where T : View { - Toplevel? t = App?.Current; + Toplevel? t = App?.TopRunnable; if (t == null) { - Fail ("App.Current was null when attempting to find view"); + Fail ("App.TopRunnable was null when attempting to find view"); } T? f = FindRecursive (t!, evaluator); diff --git a/Tests/UnitTests/Application/Application.NavigationTests.cs b/Tests/UnitTests/Application/Application.NavigationTests.cs index 21cafa132..083052ddd 100644 --- a/Tests/UnitTests/Application/Application.NavigationTests.cs +++ b/Tests/UnitTests/Application/Application.NavigationTests.cs @@ -82,7 +82,7 @@ public class ApplicationNavigationTests (ITestOutputHelper output) { IApplication app = Application.Create (); - app.Current = new () + app.TopRunnable = new () { Id = "top", CanFocus = true, @@ -101,10 +101,10 @@ public class ApplicationNavigationTests (ITestOutputHelper output) CanFocus = true }; - app.Current?.Add (subView1, subView2); - Assert.False (app.Current?.HasFocus); + app.TopRunnable?.Add (subView1, subView2); + Assert.False (app.TopRunnable?.HasFocus); - app.Current?.SetFocus (); + app.TopRunnable?.SetFocus (); Assert.True (subView1.HasFocus); Assert.Equal (subView1, app.Navigation?.GetFocused ()); @@ -117,7 +117,7 @@ public class ApplicationNavigationTests (ITestOutputHelper output) { IApplication app = Application.Create (); - app.Current = new () + app.TopRunnable = new () { Id = "top", CanFocus = true, @@ -130,20 +130,20 @@ public class ApplicationNavigationTests (ITestOutputHelper output) CanFocus = true }; - app!.Current.Add (subView1); - Assert.False (app.Current.HasFocus); + app!.TopRunnable.Add (subView1); + Assert.False (app.TopRunnable.HasFocus); - app.Current.SetFocus (); + app.TopRunnable.SetFocus (); Assert.True (subView1.HasFocus); Assert.Equal (subView1, app.Navigation!.GetFocused ()); subView1.HasFocus = false; Assert.False (subView1.HasFocus); - Assert.True (app.Current.HasFocus); - Assert.Equal (app.Current, app.Navigation.GetFocused ()); + Assert.True (app.TopRunnable.HasFocus); + Assert.Equal (app.TopRunnable, app.Navigation.GetFocused ()); - app.Current.HasFocus = false; - Assert.False (app.Current.HasFocus); + app.TopRunnable.HasFocus = false; + Assert.False (app.TopRunnable.HasFocus); Assert.Null (app.Navigation.GetFocused ()); } diff --git a/Tests/UnitTests/Application/ApplicationImplBeginEndTests.cs b/Tests/UnitTests/Application/ApplicationImplBeginEndTests.cs index da9faad9d..ad48cf802 100644 --- a/Tests/UnitTests/Application/ApplicationImplBeginEndTests.cs +++ b/Tests/UnitTests/Application/ApplicationImplBeginEndTests.cs @@ -45,12 +45,12 @@ public class ApplicationImplBeginEndTests try { toplevel = new (); - Assert.Null (app.Current); + Assert.Null (app.TopRunnable); app.Begin (toplevel); - Assert.NotNull (app.Current); - Assert.Same (toplevel, app.Current); + Assert.NotNull (app.TopRunnable); + Assert.Same (toplevel, app.TopRunnable); Assert.Single (app.SessionStack); } finally @@ -74,11 +74,11 @@ public class ApplicationImplBeginEndTests app.Begin (toplevel1); Assert.Single (app.SessionStack); - Assert.Same (toplevel1, app.Current); + Assert.Same (toplevel1, app.TopRunnable); app.Begin (toplevel2); Assert.Equal (2, app.SessionStack.Count); - Assert.Same (toplevel2, app.Current); + Assert.Same (toplevel2, app.TopRunnable); } finally { @@ -163,7 +163,7 @@ public class ApplicationImplBeginEndTests app.End (token2); Assert.Single (app.SessionStack); - Assert.Same (toplevel1, app.Current); + Assert.Same (toplevel1, app.TopRunnable); app.End (token1); @@ -228,13 +228,13 @@ public class ApplicationImplBeginEndTests SessionToken token2 = app.Begin (toplevel2); SessionToken token3 = app.Begin (toplevel3); - Assert.Same (toplevel3, app.Current); + Assert.Same (toplevel3, app.TopRunnable); app.End (token3); - Assert.Same (toplevel2, app.Current); + Assert.Same (toplevel2, app.TopRunnable); app.End (token2); - Assert.Same (toplevel1, app.Current); + Assert.Same (toplevel1, app.TopRunnable); app.End (token1); } @@ -265,7 +265,7 @@ public class ApplicationImplBeginEndTests } Assert.Equal (5, app.SessionStack.Count); - Assert.Same (toplevels [4], app.Current); + Assert.Same (toplevels [4], app.TopRunnable); // End them in reverse order (LIFO) for (var i = 4; i >= 0; i--) @@ -275,7 +275,7 @@ public class ApplicationImplBeginEndTests if (i > 0) { Assert.Equal (i, app.SessionStack.Count); - Assert.Same (toplevels [i - 1], app.Current); + Assert.Same (toplevels [i - 1], app.TopRunnable); } else { @@ -358,7 +358,7 @@ public class ApplicationImplBeginEndTests app.Begin (toplevel2); Assert.Equal (2, app.SessionStack.Count); - Assert.NotNull (app.Current); + Assert.NotNull (app.TopRunnable); } finally { @@ -371,7 +371,7 @@ public class ApplicationImplBeginEndTests // Verify cleanup happened Assert.Empty (app.SessionStack); - Assert.Null (app.Current); + Assert.Null (app.TopRunnable); Assert.Null (app.CachedSessionTokenToplevel); } } @@ -432,7 +432,7 @@ public class ApplicationImplBeginEndTests Assert.True (toplevel1Deactivated); Assert.True (toplevel2Activated); - Assert.Same (toplevel2, app.Current); + Assert.Same (toplevel2, app.TopRunnable); } finally { diff --git a/Tests/UnitTests/Application/ApplicationImplTests.cs b/Tests/UnitTests/Application/ApplicationImplTests.cs index faa4cf7a8..c995c02e8 100644 --- a/Tests/UnitTests/Application/ApplicationImplTests.cs +++ b/Tests/UnitTests/Application/ApplicationImplTests.cs @@ -81,7 +81,7 @@ public class ApplicationImplTests TimeSpan.FromMilliseconds (150), () => { - if (app.Current is { }) + if (app.TopRunnable is { }) { app.RequestStop (); @@ -91,7 +91,7 @@ public class ApplicationImplTests return false; } ); - Assert.Null (app?.Current); + Assert.Null (app?.TopRunnable); // Blocks until the timeout call is hit @@ -100,10 +100,10 @@ public class ApplicationImplTests // We returned false above, so we should not have to remove the timeout Assert.False (app?.RemoveTimeout (timeoutToken!)); - Assert.NotNull (app?.Current); - app.Current?.Dispose (); + Assert.NotNull (app?.TopRunnable); + app.TopRunnable?.Dispose (); app.Shutdown (); - Assert.Null (app.Current); + Assert.Null (app.TopRunnable); } [Fact] @@ -124,7 +124,7 @@ public class ApplicationImplTests { Assert.True (top!.Running); - if (app.Current != null) + if (app.TopRunnable != null) { app.RequestStop (); @@ -146,8 +146,8 @@ public class ApplicationImplTests Assert.False (top!.Running); // BUGBUG: Shutdown sets Top to null, not End. - //Assert.Null (Application.Current); - app.Current?.Dispose (); + //Assert.Null (Application.TopRunnable); + app.TopRunnable?.Dispose (); app.Shutdown (); } @@ -156,13 +156,13 @@ public class ApplicationImplTests { IApplication app = NewMockedApplicationImpl ()!; - Assert.Null (app.Current); + Assert.Null (app.TopRunnable); Assert.Null (app.Driver); app.Init ("fake"); Toplevel top = new Window (); - app.Current = top; + app.TopRunnable = top; var closedCount = 0; @@ -193,7 +193,7 @@ public class ApplicationImplTests Assert.Equal (1, closedCount); Assert.Equal (1, unloadedCount); - app.Current?.Dispose (); + app.TopRunnable?.Dispose (); app.Shutdown (); Assert.Equal (1, closedCount); Assert.Equal (1, unloadedCount); @@ -204,7 +204,7 @@ public class ApplicationImplTests { IApplication app = NewMockedApplicationImpl ()!; - Assert.Null (app.Current); + Assert.Null (app.TopRunnable); Assert.Null (app.Driver); app.Init ("fake"); @@ -228,7 +228,7 @@ public class ApplicationImplTests { Assert.True (top!.Running); - if (app.Current != null) + if (app.TopRunnable != null) { app.RequestStop (); @@ -251,7 +251,7 @@ public class ApplicationImplTests // We returned false above, so we should not have to remove the timeout Assert.False (app.RemoveTimeout (timeoutToken)); - app.Current?.Dispose (); + app.TopRunnable?.Dispose (); app.Shutdown (); Assert.Equal (1, closedCount); Assert.Equal (1, unloadedCount); @@ -275,7 +275,7 @@ public class ApplicationImplTests { Assert.True (top!.Running); - if (app.Current != null) + if (app.TopRunnable != null) { app.Keyboard.RaiseKeyDownEvent (app.Keyboard.QuitKey); } @@ -294,10 +294,10 @@ public class ApplicationImplTests Assert.False (top!.Running); - Assert.NotNull (app.Current); + Assert.NotNull (app.TopRunnable); top.Dispose (); app.Shutdown (); - Assert.Null (app.Current); + Assert.Null (app.TopRunnable); } [Fact] @@ -308,16 +308,16 @@ public class ApplicationImplTests app.Init ("fake"); app.AddTimeout (TimeSpan.Zero, () => IdleExit (app)); - Assert.Null (app.Current); + Assert.Null (app.TopRunnable); // Blocks until the timeout call is hit app.Run (); - Assert.NotNull (app.Current); - app.Current?.Dispose (); + Assert.NotNull (app.TopRunnable); + app.TopRunnable?.Dispose (); app.Shutdown (); - Assert.Null (app.Current); + Assert.Null (app.TopRunnable); } [Fact] @@ -357,7 +357,7 @@ public class ApplicationImplTests app.Run (t); - app.Current?.Dispose (); + app.TopRunnable?.Dispose (); app.Shutdown (); Assert.Equal (2, closing); @@ -366,7 +366,7 @@ public class ApplicationImplTests private bool IdleExit (IApplication app) { - if (app.Current != null) + if (app.TopRunnable != null) { app.RequestStop (); @@ -408,7 +408,7 @@ public class ApplicationImplTests () => { // Run asynchronous logic inside Task.Run - if (app.Current != null) + if (app.TopRunnable != null) { b.NewKeyDownEvent (Key.Enter); b.NewKeyUpEvent (Key.Enter); @@ -417,7 +417,7 @@ public class ApplicationImplTests return false; }); - Assert.Null (app.Current); + Assert.Null (app.TopRunnable); var w = new Window { @@ -428,10 +428,10 @@ public class ApplicationImplTests // Blocks until the timeout call is hit app.Run (w); - Assert.NotNull (app.Current); - app.Current?.Dispose (); + Assert.NotNull (app.TopRunnable); + app.TopRunnable?.Dispose (); app.Shutdown (); - Assert.Null (app.Current); + Assert.Null (app.TopRunnable); Assert.True (result); } @@ -448,7 +448,7 @@ public class ApplicationImplTests //Assert.Null (v2.Popover); //Assert.Null (v2.Navigation); - Assert.Null (v2.Current); + Assert.Null (v2.TopRunnable); Assert.Empty (v2.SessionStack); // Init should populate instance fields @@ -459,7 +459,7 @@ public class ApplicationImplTests Assert.True (v2.Initialized); Assert.NotNull (v2.Popover); Assert.NotNull (v2.Navigation); - Assert.Null (v2.Current); // Top is still null until Run + Assert.Null (v2.TopRunnable); // Top is still null until Run // Shutdown should clean up instance fields v2.Shutdown (); @@ -469,7 +469,7 @@ public class ApplicationImplTests //Assert.Null (v2.Popover); //Assert.Null (v2.Navigation); - Assert.Null (v2.Current); + Assert.Null (v2.TopRunnable); Assert.Empty (v2.SessionStack); } } diff --git a/Tests/UnitTests/Application/ApplicationPopoverTests.cs b/Tests/UnitTests/Application/ApplicationPopoverTests.cs index 1ca1944d0..778e681a9 100644 --- a/Tests/UnitTests/Application/ApplicationPopoverTests.cs +++ b/Tests/UnitTests/Application/ApplicationPopoverTests.cs @@ -197,14 +197,14 @@ public class ApplicationPopoverTests // Arrange Application.Init ("fake"); - Application.Current = new (); + Application.TopRunnable = new (); PopoverTestClass? popover = new (); // Act Application.Popover?.Register (popover); // Assert - Assert.Equal (Application.Current, popover.Current); + Assert.Equal (Application.TopRunnable, popover.Current); } finally { @@ -219,7 +219,7 @@ public class ApplicationPopoverTests { // Arrange Application.Init ("fake"); - Application.Current = new() { Id = "initialTop" }; + Application.TopRunnable = new() { Id = "initialTop" }; PopoverTestClass? popover = new () { }; var keyDownEvents = 0; @@ -234,7 +234,7 @@ public class ApplicationPopoverTests // Act Application.RaiseKeyDownEvent (Key.A); // Goes to initialTop - Application.Current = new() { Id = "secondaryTop" }; + Application.TopRunnable = new() { Id = "secondaryTop" }; Application.RaiseKeyDownEvent (Key.A); // Goes to secondaryTop // Test @@ -267,7 +267,7 @@ public class ApplicationPopoverTests // Arrange Application.Init ("fake"); - Application.Current = new () + Application.TopRunnable = new () { Frame = new (0, 0, 10, 10), Id = "top" @@ -282,7 +282,7 @@ public class ApplicationPopoverTests Height = 2 }; - Application.Current.Add (view); + Application.TopRunnable.Add (view); popover = new () { @@ -316,7 +316,7 @@ public class ApplicationPopoverTests finally { popover?.Dispose (); - Application.Current?.Dispose (); + Application.TopRunnable?.Dispose (); Application.ResetState (true); } } diff --git a/Tests/UnitTests/Application/ApplicationScreenTests.cs b/Tests/UnitTests/Application/ApplicationScreenTests.cs index 9d4185adf..f1ee2ea89 100644 --- a/Tests/UnitTests/Application/ApplicationScreenTests.cs +++ b/Tests/UnitTests/Application/ApplicationScreenTests.cs @@ -46,35 +46,35 @@ public class ApplicationScreenTests Assert.Equal (0, clearedContentsRaised); // Act - Application.Current!.SetNeedsLayout (); + Application.TopRunnable!.SetNeedsLayout (); Application.LayoutAndDraw (); // Assert Assert.Equal (0, clearedContentsRaised); // Act - Application.Current.X = 1; + Application.TopRunnable.X = 1; Application.LayoutAndDraw (); // Assert Assert.Equal (1, clearedContentsRaised); // Act - Application.Current.Width = 10; + Application.TopRunnable.Width = 10; Application.LayoutAndDraw (); // Assert Assert.Equal (2, clearedContentsRaised); // Act - Application.Current.Y = 1; + Application.TopRunnable.Y = 1; Application.LayoutAndDraw (); // Assert Assert.Equal (3, clearedContentsRaised); // Act - Application.Current.Height = 10; + Application.TopRunnable.Height = 10; Application.LayoutAndDraw (); // Assert diff --git a/Tests/UnitTests/Application/ApplicationTests.cs b/Tests/UnitTests/Application/ApplicationTests.cs index f9230fccd..52d9276da 100644 --- a/Tests/UnitTests/Application/ApplicationTests.cs +++ b/Tests/UnitTests/Application/ApplicationTests.cs @@ -70,13 +70,13 @@ public class ApplicationTests [SetupFakeApplication] public void Begin_Sets_Application_Top_To_Console_Size () { - Assert.Null (Application.Current); + Assert.Null (Application.TopRunnable); Application.Driver!.SetScreenSize (80, 25); Toplevel top = new (); Application.Begin (top); - Assert.Equal (new (0, 0, 80, 25), Application.Current!.Frame); + Assert.Equal (new (0, 0, 80, 25), Application.TopRunnable!.Frame); Application.Driver!.SetScreenSize (5, 5); - Assert.Equal (new (0, 0, 5, 5), Application.Current!.Frame); + Assert.Equal (new (0, 0, 5, 5), Application.TopRunnable!.Frame); top.Dispose (); } @@ -84,21 +84,21 @@ public class ApplicationTests [SetupFakeApplication] public void End_And_Shutdown_Should_Not_Dispose_ApplicationTop () { - Assert.Null (Application.Current); + Assert.Null (Application.TopRunnable); SessionToken rs = Application.Begin (new ()); - Application.Current!.Title = "End_And_Shutdown_Should_Not_Dispose_ApplicationTop"; - Assert.Equal (rs.Toplevel, Application.Current); + Application.TopRunnable!.Title = "End_And_Shutdown_Should_Not_Dispose_ApplicationTop"; + Assert.Equal (rs.Toplevel, Application.TopRunnable); Application.End (rs); #if DEBUG_IDISPOSABLE Assert.True (rs.WasDisposed); - Assert.False (Application.Current!.WasDisposed); // Is true because the rs.Toplevel is the same as Application.Current + Assert.False (Application.TopRunnable!.WasDisposed); // Is true because the rs.Toplevel is the same as Application.TopRunnable #endif Assert.Null (rs.Toplevel); - Toplevel top = Application.Current; + Toplevel top = Application.TopRunnable; #if DEBUG_IDISPOSABLE Exception exception = Record.Exception (Application.Shutdown); @@ -132,12 +132,12 @@ public class ApplicationTests Assert.NotNull (sessionToken); Assert.Equal (rs, sessionToken); - Assert.Equal (topLevel, Application.Current); + Assert.Equal (topLevel, Application.TopRunnable); Application.SessionBegun -= newSessionTokenFn; Application.End (sessionToken); - Assert.NotNull (Application.Current); + Assert.NotNull (Application.TopRunnable); Assert.NotNull (Application.Driver); topLevel.Dispose (); @@ -246,7 +246,7 @@ public class ApplicationTests // Check that all fields and properties are set to their default values // Public Properties - Assert.Null (Application.Current); + Assert.Null (Application.TopRunnable); Assert.Null (Application.Mouse.MouseGrabView); // Don't check Application.ForceDriver @@ -391,18 +391,18 @@ public class ApplicationTests Assert.NotNull (sessionToken); Assert.Equal (rs, sessionToken); - Assert.Equal (topLevel, Application.Current); + Assert.Equal (topLevel, Application.TopRunnable); Application.SessionBegun -= newSessionTokenFn; Application.End (sessionToken); - Assert.NotNull (Application.Current); + Assert.NotNull (Application.TopRunnable); Assert.NotNull (Application.Driver); topLevel.Dispose (); Application.Shutdown (); - Assert.Null (Application.Current); + Assert.Null (Application.TopRunnable); Assert.Null (Application.Driver); } @@ -411,11 +411,11 @@ public class ApplicationTests public void Internal_Properties_Correct () { Assert.True (Application.Initialized); - Assert.Null (Application.Current); + Assert.Null (Application.TopRunnable); SessionToken rs = Application.Begin (new ()); - Assert.Equal (Application.Current, rs.Toplevel); + Assert.Equal (Application.TopRunnable, rs.Toplevel); Assert.Null (Application.Mouse.MouseGrabView); // public - Application.Current!.Dispose (); + Application.TopRunnable!.Dispose (); } // Invoke Tests @@ -511,9 +511,9 @@ public class ApplicationTests // Run when already initialized or not with a Driver will not throw (because Window is derived from Toplevel) // Using another type not derived from Toplevel will throws at compile time Application.Run (); - Assert.True (Application.Current is Window); + Assert.True (Application.TopRunnable is Window); - Application.Current!.Dispose (); + Application.TopRunnable!.Dispose (); } [Fact] @@ -524,15 +524,15 @@ public class ApplicationTests // Run when already initialized or not with a Driver will not throw (because Window is derived from Toplevel) // Using another type not derived from Toplevel will throws at compile time Application.Run (null, "fake"); - Assert.True (Application.Current is Window); + Assert.True (Application.TopRunnable is Window); - Application.Current!.Dispose (); + Application.TopRunnable!.Dispose (); // Run when already initialized or not with a Driver will not throw (because Dialog is derived from Toplevel) Application.Run (null, "fake"); - Assert.True (Application.Current is Dialog); + Assert.True (Application.TopRunnable is Dialog); - Application.Current!.Dispose (); + Application.TopRunnable!.Dispose (); Application.Shutdown (); } @@ -540,7 +540,7 @@ public class ApplicationTests [SetupFakeApplication] public void Run_T_After_Init_Does_Not_Disposes_Application_Top () { - // Init doesn't create a Toplevel and assigned it to Application.Current + // Init doesn't create a Toplevel and assigned it to Application.TopRunnable // but Begin does var initTop = new Toplevel (); @@ -554,13 +554,13 @@ public class ApplicationTests initTop.Dispose (); Assert.True (initTop.WasDisposed); #endif - Application.Current!.Dispose (); + Application.TopRunnable!.Dispose (); return; void OnApplicationOnIteration (object s, IterationEventArgs a) { - Assert.NotEqual (initTop, Application.Current); + Assert.NotEqual (initTop, Application.TopRunnable); #if DEBUG_IDISPOSABLE Assert.False (initTop.WasDisposed); #endif @@ -577,7 +577,7 @@ public class ApplicationTests // Init has been called and we're passing no driver to Run. This is ok. Application.Run (); - Application.Current!.Dispose (); + Application.TopRunnable!.Dispose (); } [Fact] @@ -589,7 +589,7 @@ public class ApplicationTests // Init has been called, selecting FakeDriver; we're passing no driver to Run. Should be fine. Application.Run (); - Application.Current!.Dispose (); + Application.TopRunnable!.Dispose (); } [Fact] @@ -620,7 +620,7 @@ public class ApplicationTests // Init has NOT been called and we're passing a valid driver to Run. This is ok. Application.Run (null, "fake"); - Application.Current!.Dispose (); + Application.TopRunnable!.Dispose (); } [Fact] @@ -744,9 +744,9 @@ public class ApplicationTests Assert.NotNull (w); Assert.Equal (string.Empty, w.Title); // Valid - w has not been disposed. The user may want to run it again - Assert.NotNull (Application.Current); - Assert.Equal (w, Application.Current); - Assert.NotEqual (top, Application.Current); + Assert.NotNull (Application.TopRunnable); + Assert.Equal (w, Application.TopRunnable); + Assert.NotEqual (top, Application.TopRunnable); Application.Run (w); // Valid - w has not been disposed. @@ -774,14 +774,14 @@ public class ApplicationTests [Fact] public void Run_Creates_Top_Without_Init () { - Assert.Null (Application.Current); + Assert.Null (Application.TopRunnable); Application.StopAfterFirstIteration = true; Application.Iteration += OnApplicationOnIteration; Toplevel top = Application.Run (null, "fake"); Application.Iteration -= OnApplicationOnIteration; #if DEBUG_IDISPOSABLE - Assert.Equal (top, Application.Current); + Assert.Equal (top, Application.TopRunnable); Assert.False (top.WasDisposed); Exception exception = Record.Exception (Application.Shutdown); Assert.NotNull (exception); @@ -794,38 +794,38 @@ public class ApplicationTests #if DEBUG_IDISPOSABLE Assert.True (top.WasDisposed); #endif - Assert.NotNull (Application.Current); + Assert.NotNull (Application.TopRunnable); Application.Shutdown (); - Assert.Null (Application.Current); + Assert.Null (Application.TopRunnable); return; - void OnApplicationOnIteration (object s, IterationEventArgs e) { Assert.NotNull (Application.Current); } + void OnApplicationOnIteration (object s, IterationEventArgs e) { Assert.NotNull (Application.TopRunnable); } } [Fact] public void Run_T_Creates_Top_Without_Init () { - Assert.Null (Application.Current); + Assert.Null (Application.TopRunnable); Application.StopAfterFirstIteration = true; Application.Run (null, "fake"); #if DEBUG_IDISPOSABLE - Assert.False (Application.Current!.WasDisposed); + Assert.False (Application.TopRunnable!.WasDisposed); Exception exception = Record.Exception (Application.Shutdown); Assert.NotNull (exception); - Assert.False (Application.Current!.WasDisposed); + Assert.False (Application.TopRunnable!.WasDisposed); // It's up to caller to dispose it - Application.Current!.Dispose (); - Assert.True (Application.Current!.WasDisposed); + Application.TopRunnable!.Dispose (); + Assert.True (Application.TopRunnable!.WasDisposed); #endif - Assert.NotNull (Application.Current); + Assert.NotNull (Application.TopRunnable); Application.Shutdown (); - Assert.Null (Application.Current); + Assert.Null (Application.TopRunnable); } [Fact] @@ -839,7 +839,7 @@ public class ApplicationTests // the new(Toplevel) may be a derived class that is possible using Application static // properties that is only available after the Application.Init was called - Assert.Null (Application.Current); + Assert.Null (Application.TopRunnable); Assert.Throws (() => Application.Run (new Toplevel ())); @@ -849,25 +849,25 @@ public class ApplicationTests Application.Run (new Toplevel ()); Application.Iteration -= OnApplication_OnIteration; #if DEBUG_IDISPOSABLE - Assert.False (Application.Current!.WasDisposed); + Assert.False (Application.TopRunnable!.WasDisposed); Exception exception = Record.Exception (Application.Shutdown); Assert.NotNull (exception); - Assert.False (Application.Current!.WasDisposed); + Assert.False (Application.TopRunnable!.WasDisposed); // It's up to caller to dispose it - Application.Current!.Dispose (); - Assert.True (Application.Current!.WasDisposed); + Application.TopRunnable!.Dispose (); + Assert.True (Application.TopRunnable!.WasDisposed); #endif - Assert.NotNull (Application.Current); + Assert.NotNull (Application.TopRunnable); Application.Shutdown (); - Assert.Null (Application.Current); + Assert.Null (Application.TopRunnable); return; void OnApplication_OnIteration (object s, IterationEventArgs e) { - Assert.NotNull (Application.Current); + Assert.NotNull (Application.TopRunnable); Application.RequestStop (); } } @@ -892,9 +892,9 @@ public class ApplicationTests TaskScheduler.FromCurrentSynchronizationContext ()); Application.Run (); Assert.NotNull (Application.Driver); - Assert.NotNull (Application.Current); - Assert.False (Application.Current!.Running); - Application.Current!.Dispose (); + Assert.NotNull (Application.TopRunnable); + Assert.False (Application.TopRunnable!.Running); + Application.TopRunnable!.Dispose (); Application.Shutdown (); } diff --git a/Tests/UnitTests/Application/Mouse/ApplicationMouseEnterLeaveTests.cs b/Tests/UnitTests/Application/Mouse/ApplicationMouseEnterLeaveTests.cs index 06c3cc4d1..7fd6468d7 100644 --- a/Tests/UnitTests/Application/Mouse/ApplicationMouseEnterLeaveTests.cs +++ b/Tests/UnitTests/Application/Mouse/ApplicationMouseEnterLeaveTests.cs @@ -41,9 +41,9 @@ public class ApplicationMouseEnterLeaveTests public void RaiseMouseEnterLeaveEvents_MouseEntersView_CallsOnMouseEnter () { // Arrange - Application.Current = new () { Frame = new (0, 0, 10, 10) }; + Application.TopRunnable = new () { Frame = new (0, 0, 10, 10) }; var view = new TestView (); - Application.Current.Add (view); + Application.TopRunnable.Add (view); var mousePosition = new Point (1, 1); List currentViewsUnderMouse = new () { view }; @@ -66,7 +66,7 @@ public class ApplicationMouseEnterLeaveTests finally { // Cleanup - Application.Current?.Dispose (); + Application.TopRunnable?.Dispose (); Application.ResetState (); } } @@ -75,9 +75,9 @@ public class ApplicationMouseEnterLeaveTests public void RaiseMouseEnterLeaveEvents_MouseLeavesView_CallsOnMouseLeave () { // Arrange - Application.Current = new () { Frame = new (0, 0, 10, 10) }; + Application.TopRunnable = new () { Frame = new (0, 0, 10, 10) }; var view = new TestView (); - Application.Current.Add (view); + Application.TopRunnable.Add (view); var mousePosition = new Point (0, 0); List currentViewsUnderMouse = new (); var mouseEvent = new MouseEventArgs (); @@ -97,7 +97,7 @@ public class ApplicationMouseEnterLeaveTests finally { // Cleanup - Application.Current?.Dispose (); + Application.TopRunnable?.Dispose (); Application.ResetState (); } } @@ -106,7 +106,7 @@ public class ApplicationMouseEnterLeaveTests public void RaiseMouseEnterLeaveEvents_MouseMovesBetweenAdjacentViews_CallsOnMouseEnterAndLeave () { // Arrange - Application.Current = new () { Frame = new (0, 0, 10, 10) }; + Application.TopRunnable = new () { Frame = new (0, 0, 10, 10) }; var view1 = new TestView (); // at 1,1 to 2,2 var view2 = new TestView () // at 2,2 to 3,3 @@ -114,8 +114,8 @@ public class ApplicationMouseEnterLeaveTests X = 2, Y = 2 }; - Application.Current.Add (view1); - Application.Current.Add (view2); + Application.TopRunnable.Add (view1); + Application.TopRunnable.Add (view2); Application.CachedViewsUnderMouse.Clear (); @@ -126,7 +126,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (0, view1.OnMouseEnterCalled); @@ -139,7 +139,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -152,7 +152,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -165,7 +165,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -178,7 +178,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -189,7 +189,7 @@ public class ApplicationMouseEnterLeaveTests finally { // Cleanup - Application.Current?.Dispose (); + Application.TopRunnable?.Dispose (); Application.ResetState (); } } @@ -198,9 +198,9 @@ public class ApplicationMouseEnterLeaveTests public void RaiseMouseEnterLeaveEvents_NoViewsUnderMouse_DoesNotCallOnMouseEnterOrLeave () { // Arrange - Application.Current = new () { Frame = new (0, 0, 10, 10) }; + Application.TopRunnable = new () { Frame = new (0, 0, 10, 10) }; var view = new TestView (); - Application.Current.Add (view); + Application.TopRunnable.Add (view); var mousePosition = new Point (0, 0); List currentViewsUnderMouse = new (); var mouseEvent = new MouseEventArgs (); @@ -219,7 +219,7 @@ public class ApplicationMouseEnterLeaveTests finally { // Cleanup - Application.Current?.Dispose (); + Application.TopRunnable?.Dispose (); Application.ResetState (); } } @@ -228,7 +228,7 @@ public class ApplicationMouseEnterLeaveTests public void RaiseMouseEnterLeaveEvents_MouseMovesBetweenOverlappingPeerViews_CallsOnMouseEnterAndLeave () { // Arrange - Application.Current = new () { Frame = new (0, 0, 10, 10) }; + Application.TopRunnable = new () { Frame = new (0, 0, 10, 10) }; var view1 = new TestView { @@ -241,8 +241,8 @@ public class ApplicationMouseEnterLeaveTests X = 2, Y = 2 }; - Application.Current.Add (view1); - Application.Current.Add (view2); + Application.TopRunnable.Add (view1); + Application.TopRunnable.Add (view2); Application.CachedViewsUnderMouse.Clear (); @@ -253,7 +253,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (0, view1.OnMouseEnterCalled); @@ -266,7 +266,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -279,7 +279,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -292,7 +292,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -305,7 +305,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -318,7 +318,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -329,7 +329,7 @@ public class ApplicationMouseEnterLeaveTests finally { // Cleanup - Application.Current?.Dispose (); + Application.TopRunnable?.Dispose (); Application.ResetState (); } } @@ -338,7 +338,7 @@ public class ApplicationMouseEnterLeaveTests public void RaiseMouseEnterLeaveEvents_MouseMovesBetweenOverlappingSubViews_CallsOnMouseEnterAndLeave () { // Arrange - Application.Current = new () { Frame = new (0, 0, 10, 10) }; + Application.TopRunnable = new () { Frame = new (0, 0, 10, 10) }; var view1 = new TestView { @@ -358,7 +358,7 @@ public class ApplicationMouseEnterLeaveTests Arrangement = ViewArrangement.Overlapped }; // at 2,2 to 4,4 (screen) view1.Add (subView); - Application.Current.Add (view1); + Application.TopRunnable.Add (view1); Application.CachedViewsUnderMouse.Clear (); @@ -372,7 +372,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (0, view1.OnMouseEnterCalled); @@ -385,7 +385,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -398,7 +398,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -411,7 +411,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (1, view1.OnMouseEnterCalled); @@ -424,7 +424,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (2, view1.OnMouseEnterCalled); @@ -437,7 +437,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (2, view1.OnMouseEnterCalled); @@ -450,7 +450,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (2, view1.OnMouseEnterCalled); @@ -463,7 +463,7 @@ public class ApplicationMouseEnterLeaveTests Application.RaiseMouseEnterLeaveEvents ( mousePosition, - Application.Current.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); // Assert Assert.Equal (3, view1.OnMouseEnterCalled); @@ -474,7 +474,7 @@ public class ApplicationMouseEnterLeaveTests finally { // Cleanup - Application.Current?.Dispose (); + Application.TopRunnable?.Dispose (); Application.ResetState (); } } diff --git a/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs b/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs index 6116bcf63..08a97ea63 100644 --- a/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs +++ b/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs @@ -202,15 +202,15 @@ public class ApplicationMouseTests var clicked = false; - Application.Current = new Toplevel () + Application.TopRunnable = new Toplevel () { Id = "top", }; - Application.Current.X = 0; - Application.Current.Y = 0; - Application.Current.Width = size.Width * 2; - Application.Current.Height = size.Height * 2; - Application.Current.BorderStyle = LineStyle.None; + Application.TopRunnable.X = 0; + Application.TopRunnable.Y = 0; + Application.TopRunnable.Width = size.Width * 2; + Application.TopRunnable.Height = size.Height * 2; + Application.TopRunnable.BorderStyle = LineStyle.None; var view = new View { Id = "view", X = pos.X, Y = pos.Y, Width = size.Width, Height = size.Height }; @@ -218,7 +218,7 @@ public class ApplicationMouseTests view.BorderStyle = LineStyle.Single; view.CanFocus = true; - Application.Current.Add (view); + Application.TopRunnable.Add (view); var mouseEvent = new MouseEventArgs { Position = new (clickX, clickY), ScreenPosition = new (clickX, clickY), Flags = MouseFlags.Button1Clicked }; @@ -231,7 +231,7 @@ public class ApplicationMouseTests Application.RaiseMouseEvent (mouseEvent); Assert.Equal (expectedClicked, clicked); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (ignoreDisposed: true); } diff --git a/Tests/UnitTests/Application/SessionTokenTests.cs b/Tests/UnitTests/Application/SessionTokenTests.cs index 1e642d04d..f77aec9e9 100644 --- a/Tests/UnitTests/Application/SessionTokenTests.cs +++ b/Tests/UnitTests/Application/SessionTokenTests.cs @@ -29,7 +29,7 @@ public class SessionTokenTests Assert.NotNull (rs); Application.End (rs); - Assert.NotNull (Application.Current); + Assert.NotNull (Application.TopRunnable); // v2 does not use main loop, it uses MainLoop and its internal //Assert.NotNull (Application.MainLoop); diff --git a/Tests/UnitTests/Application/SynchronizatonContextTests.cs b/Tests/UnitTests/Application/SynchronizatonContextTests.cs index 915cb3024..7137389f7 100644 --- a/Tests/UnitTests/Application/SynchronizatonContextTests.cs +++ b/Tests/UnitTests/Application/SynchronizatonContextTests.cs @@ -39,7 +39,7 @@ public class SyncrhonizationContextTests Task.Run (() => { - while (Application.Current is null || Application.Current is { Running: false }) + while (Application.TopRunnable is null || Application.TopRunnable is { Running: false }) { Thread.Sleep (500); } @@ -56,7 +56,7 @@ public class SyncrhonizationContextTests null ); - if (Application.Current is { Running: true }) + if (Application.TopRunnable is { Running: true }) { Assert.False (success); } diff --git a/Tests/UnitTests/Dialogs/DialogTests.cs b/Tests/UnitTests/Dialogs/DialogTests.cs index a4ba9591a..72d479e1e 100644 --- a/Tests/UnitTests/Dialogs/DialogTests.cs +++ b/Tests/UnitTests/Dialogs/DialogTests.cs @@ -902,8 +902,8 @@ public class DialogTests (ITestOutputHelper output) #if DEBUG_IDISPOSABLE Assert.False (dlg.WasDisposed); - Assert.False (Application.Current!.WasDisposed); - Assert.Equal (dlg, Application.Current); + Assert.False (Application.TopRunnable!.WasDisposed); + Assert.Equal (dlg, Application.TopRunnable); #endif Assert.True (dlg.Canceled); @@ -925,8 +925,8 @@ public class DialogTests (ITestOutputHelper output) Application.Run (dlg2); Assert.True (dlg.WasDisposed); - Assert.False (Application.Current.WasDisposed); - Assert.Equal (dlg2, Application.Current); + Assert.False (Application.TopRunnable.WasDisposed); + Assert.Equal (dlg2, Application.TopRunnable); Assert.False (dlg2.WasDisposed); dlg2.Dispose (); @@ -937,10 +937,10 @@ public class DialogTests (ITestOutputHelper output) //Assert.NotNull (exception); //Assert.StartsWith ("Cannot access a disposed object.", exception.Message); - Assert.True (Application.Current.WasDisposed); + Assert.True (Application.TopRunnable.WasDisposed); Application.Shutdown (); Assert.True (dlg2.WasDisposed); - Assert.Null (Application.Current); + Assert.Null (Application.TopRunnable); #endif return; @@ -1174,8 +1174,8 @@ public class DialogTests (ITestOutputHelper output) switch (iterations) { case 0: - Application.Current!.SetNeedsLayout (); - Application.Current.SetNeedsDraw (); + Application.TopRunnable!.SetNeedsLayout (); + Application.TopRunnable.SetNeedsDraw (); break; @@ -1216,7 +1216,7 @@ public class DialogTests (ITestOutputHelper output) └───────────────────────┘", output); - Assert.False (Application.Current!.NewKeyDownEvent (Key.Enter)); + Assert.False (Application.TopRunnable!.NewKeyDownEvent (Key.Enter)); break; case 7: @@ -1410,9 +1410,9 @@ public class DialogTests (ITestOutputHelper output) #if DEBUG_IDISPOSABLE Assert.False (dlg.WasDisposed); - Assert.False (Application.Current!.WasDisposed); - Assert.NotEqual (top, Application.Current); - Assert.Equal (dlg, Application.Current); + Assert.False (Application.TopRunnable!.WasDisposed); + Assert.NotEqual (top, Application.TopRunnable); + Assert.Equal (dlg, Application.TopRunnable); #endif // dlg wasn't disposed yet and it's possible to access to his properties @@ -1426,11 +1426,11 @@ public class DialogTests (ITestOutputHelper output) top.Dispose (); #if DEBUG_IDISPOSABLE Assert.True (dlg.WasDisposed); - Assert.True (Application.Current.WasDisposed); - Assert.NotNull (Application.Current); + Assert.True (Application.TopRunnable.WasDisposed); + Assert.NotNull (Application.TopRunnable); #endif Application.Shutdown (); - Assert.Null (Application.Current); + Assert.Null (Application.TopRunnable); return; diff --git a/Tests/UnitTests/Dialogs/MessageBoxTests.cs b/Tests/UnitTests/Dialogs/MessageBoxTests.cs index 0699309b7..d602eef96 100644 --- a/Tests/UnitTests/Dialogs/MessageBoxTests.cs +++ b/Tests/UnitTests/Dialogs/MessageBoxTests.cs @@ -193,7 +193,7 @@ public class MessageBoxTests (ITestOutputHelper output) } else if (iterations == 1) { - mbFrame = Application.Current!.Frame; + mbFrame = Application.TopRunnable!.Frame; Application.RequestStop (); } } @@ -378,8 +378,8 @@ public class MessageBoxTests (ITestOutputHelper output) { AutoInitShutdownAttribute.RunIteration (); - Assert.IsType (Application.Current); - Assert.Equal (new (height, width), Application.Current.Frame.Size); + Assert.IsType (Application.TopRunnable); + Assert.Equal (new (height, width), Application.TopRunnable.Frame.Size); Application.RequestStop (); } @@ -415,8 +415,8 @@ public class MessageBoxTests (ITestOutputHelper output) { AutoInitShutdownAttribute.RunIteration (); - Assert.IsType (Application.Current); - Assert.Equal (new (height, width), Application.Current.Frame.Size); + Assert.IsType (Application.TopRunnable); + Assert.Equal (new (height, width), Application.TopRunnable.Frame.Size); Application.RequestStop (); } @@ -448,8 +448,8 @@ public class MessageBoxTests (ITestOutputHelper output) { AutoInitShutdownAttribute.RunIteration (); - Assert.IsType (Application.Current); - Assert.Equal (new (height, width), Application.Current.Frame.Size); + Assert.IsType (Application.TopRunnable); + Assert.Equal (new (height, width), Application.TopRunnable.Frame.Size); Application.RequestStop (); } diff --git a/Tests/UnitTests/View/Adornment/AdornmentSubViewTests.cs b/Tests/UnitTests/View/Adornment/AdornmentSubViewTests.cs index 7bfa4747a..d5015c7b4 100644 --- a/Tests/UnitTests/View/Adornment/AdornmentSubViewTests.cs +++ b/Tests/UnitTests/View/Adornment/AdornmentSubViewTests.cs @@ -14,14 +14,14 @@ public class AdornmentSubViewTests (ITestOutputHelper output) [InlineData (2, 1, true)] public void Adornment_WithSubView_Finds (int viewMargin, int subViewMargin, bool expectedFound) { - Application.Current = new Toplevel() + Application.TopRunnable = new Toplevel() { Width = 10, Height = 10 }; - Application.Current.Margin!.Thickness = new Thickness (viewMargin); + Application.TopRunnable.Margin!.Thickness = new Thickness (viewMargin); // Turn of TransparentMouse for the test - Application.Current.Margin!.ViewportSettings = ViewportSettingsFlags.None; + Application.TopRunnable.Margin!.ViewportSettings = ViewportSettingsFlags.None; var subView = new View () { @@ -34,26 +34,26 @@ public class AdornmentSubViewTests (ITestOutputHelper output) // Turn of TransparentMouse for the test subView.Margin!.ViewportSettings = ViewportSettingsFlags.None; - Application.Current.Margin!.Add (subView); - Application.Current.Layout (); + Application.TopRunnable.Margin!.Add (subView); + Application.TopRunnable.Layout (); - var foundView = Application.Current.GetViewsUnderLocation (new Point(0, 0), ViewportSettingsFlags.None).LastOrDefault (); + var foundView = Application.TopRunnable.GetViewsUnderLocation (new Point(0, 0), ViewportSettingsFlags.None).LastOrDefault (); bool found = foundView == subView || foundView == subView.Margin; Assert.Equal (expectedFound, found); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (ignoreDisposed: true); } [Fact] public void Adornment_WithNonVisibleSubView_Finds_Adornment () { - Application.Current = new Toplevel () + Application.TopRunnable = new Toplevel () { Width = 10, Height = 10 }; - Application.Current.Padding.Thickness = new Thickness (1); + Application.TopRunnable.Padding.Thickness = new Thickness (1); var subView = new View () { @@ -63,11 +63,11 @@ public class AdornmentSubViewTests (ITestOutputHelper output) Height = 1, Visible = false }; - Application.Current.Padding.Add (subView); - Application.Current.Layout (); + Application.TopRunnable.Padding.Add (subView); + Application.TopRunnable.Layout (); - Assert.Equal (Application.Current.Padding, Application.Current.GetViewsUnderLocation (new Point(0, 0), ViewportSettingsFlags.None).LastOrDefault ()); - Application.Current?.Dispose (); + Assert.Equal (Application.TopRunnable.Padding, Application.TopRunnable.GetViewsUnderLocation (new Point(0, 0), ViewportSettingsFlags.None).LastOrDefault ()); + Application.TopRunnable?.Dispose (); Application.ResetState (ignoreDisposed: true); } } diff --git a/Tests/UnitTests/View/Adornment/MarginTests.cs b/Tests/UnitTests/View/Adornment/MarginTests.cs index 3a4bc20ba..6a3cd0b8d 100644 --- a/Tests/UnitTests/View/Adornment/MarginTests.cs +++ b/Tests/UnitTests/View/Adornment/MarginTests.cs @@ -15,27 +15,27 @@ public class MarginTests (ITestOutputHelper output) view.Margin!.Diagnostics = ViewDiagnosticFlags.Thickness; view.Margin.Thickness = new (1); - Application.Current = new Toplevel (); - Application.SessionStack.Push (Application.Current); + Application.TopRunnable = new Toplevel (); + Application.SessionStack.Push (Application.TopRunnable); - Application.Current.SetScheme (new() + Application.TopRunnable.SetScheme (new() { Normal = new (Color.Red, Color.Green), Focus = new (Color.Green, Color.Red) }); - Application.Current.Add (view); + Application.TopRunnable.Add (view); Assert.Equal (ColorName16.Red, view.Margin.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); - Assert.Equal (ColorName16.Red, Application.Current.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); + Assert.Equal (ColorName16.Red, Application.TopRunnable.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); - Application.Current.BeginInit (); - Application.Current.EndInit (); + Application.TopRunnable.BeginInit (); + Application.TopRunnable.EndInit (); Application.LayoutAndDraw(); DriverAssert.AssertDriverContentsAre ( @"", output ); - DriverAssert.AssertDriverAttributesAre ("0", output, null, Application.Current.GetAttributeForRole (VisualRole.Normal)); + DriverAssert.AssertDriverAttributesAre ("0", output, null, Application.TopRunnable.GetAttributeForRole (VisualRole.Normal)); Application.ResetState (true); } @@ -51,20 +51,20 @@ public class MarginTests (ITestOutputHelper output) view.Margin.Thickness = new (1); view.Margin.ViewportSettings = ViewportSettingsFlags.None; - Application.Current = new Toplevel (); - Application.SessionStack.Push (Application.Current); + Application.TopRunnable = new Toplevel (); + Application.SessionStack.Push (Application.TopRunnable); - Application.Current.SetScheme (new () + Application.TopRunnable.SetScheme (new () { Normal = new (Color.Red, Color.Green), Focus = new (Color.Green, Color.Red) }); - Application.Current.Add (view); + Application.TopRunnable.Add (view); Assert.Equal (ColorName16.Red, view.Margin.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); - Assert.Equal (ColorName16.Red, Application.Current.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); + Assert.Equal (ColorName16.Red, Application.TopRunnable.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); - Application.Current.BeginInit (); - Application.Current.EndInit (); + Application.TopRunnable.BeginInit (); + Application.TopRunnable.EndInit (); Application.LayoutAndDraw (); DriverAssert.AssertDriverContentsAre ( @@ -74,7 +74,7 @@ M M MMM", output ); - DriverAssert.AssertDriverAttributesAre ("0", output, null, Application.Current.GetAttributeForRole (VisualRole.Normal)); + DriverAssert.AssertDriverAttributesAre ("0", output, null, Application.TopRunnable.GetAttributeForRole (VisualRole.Normal)); Application.ResetState (true); } diff --git a/Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs b/Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs index 236ba0600..eddee1293 100644 --- a/Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs +++ b/Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs @@ -160,14 +160,14 @@ public class KeyBindingsTests () var hotKeyRaised = false; var acceptRaised = false; var selectRaised = false; - Application.Current = new Toplevel (); + Application.TopRunnable = new Toplevel (); var view = new View { CanFocus = true, HotKeySpecifier = new Rune ('_'), Title = "_Test" }; - Application.Current.Add (view); + Application.TopRunnable.Add (view); view.HandlingHotKey += (s, e) => hotKeyRaised = true; view.Accepting += (s, e) => acceptRaised = true; view.Selecting += (s, e) => selectRaised = true; @@ -191,7 +191,7 @@ public class KeyBindingsTests () Assert.False (acceptRaised); Assert.False (selectRaised); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } // tests that test KeyBindingScope.Focus and KeyBindingScope.HotKey (tests for KeyBindingScope.Application are in Application/KeyboardTests.cs) diff --git a/Tests/UnitTests/View/Layout/GetViewsUnderLocationTests.cs b/Tests/UnitTests/View/Layout/GetViewsUnderLocationTests.cs index 97a4fc19d..c19411ac6 100644 --- a/Tests/UnitTests/View/Layout/GetViewsUnderLocationTests.cs +++ b/Tests/UnitTests/View/Layout/GetViewsUnderLocationTests.cs @@ -70,22 +70,22 @@ public class GetViewsUnderLocationTests ) { // Arrange - Application.Current = new () + Application.TopRunnable = new () { Id = "Top", Frame = new (frameX, frameY, 10, 10) }; - Application.Current.Margin!.Thickness = new (marginThickness); - Application.Current.Margin!.Id = "Margin"; - Application.Current.Border!.Thickness = new (borderThickness); - Application.Current.Border!.Id = "Border"; - Application.Current.Padding!.Thickness = new (paddingThickness); - Application.Current.Padding.Id = "Padding"; + Application.TopRunnable.Margin!.Thickness = new (marginThickness); + Application.TopRunnable.Margin!.Id = "Margin"; + Application.TopRunnable.Border!.Thickness = new (borderThickness); + Application.TopRunnable.Border!.Id = "Border"; + Application.TopRunnable.Padding!.Thickness = new (paddingThickness); + Application.TopRunnable.Padding.Id = "Padding"; var location = new Point (testX, testY); // Act - List viewsUnderMouse = Application.Current.GetViewsUnderLocation (location, ViewportSettingsFlags.TransparentMouse); + List viewsUnderMouse = Application.TopRunnable.GetViewsUnderLocation (location, ViewportSettingsFlags.TransparentMouse); // Assert if (expectedViewsFound.Length == 0) @@ -98,7 +98,7 @@ public class GetViewsUnderLocationTests Assert.Equal (expectedViewsFound, foundIds); } - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -109,7 +109,7 @@ public class GetViewsUnderLocationTests public void Returns_Top_If_No_SubViews (int testX, int testY) { // Arrange - Application.Current = new () + Application.TopRunnable = new () { Frame = new (0, 0, 10, 10) }; @@ -117,11 +117,11 @@ public class GetViewsUnderLocationTests var location = new Point (testX, testY); // Act - List viewsUnderMouse = Application.Current.GetViewsUnderLocation (location, ViewportSettingsFlags.TransparentMouse); + List viewsUnderMouse = Application.TopRunnable.GetViewsUnderLocation (location, ViewportSettingsFlags.TransparentMouse); // Assert - Assert.Contains (viewsUnderMouse, v => v == Application.Current); - Application.Current.Dispose (); + Assert.Contains (viewsUnderMouse, v => v == Application.TopRunnable); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -134,13 +134,13 @@ public class GetViewsUnderLocationTests { Application.ResetState (true); - Application.Current = new () + Application.TopRunnable = new () { Width = 10, Height = 10 }; - Assert.Same (Application.Current, Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault ()); - Application.Current.Dispose (); + Assert.Same (Application.TopRunnable, Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault ()); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -155,7 +155,7 @@ public class GetViewsUnderLocationTests [InlineData (5, 6, true)] public void Returns_Correct_If_SubViews (int testX, int testY, bool expectedSubViewFound) { - Application.Current = new () + Application.TopRunnable = new () { Width = 10, Height = 10 }; @@ -165,12 +165,12 @@ public class GetViewsUnderLocationTests X = 1, Y = 2, Width = 5, Height = 5 }; - Application.Current.Add (subview); + Application.TopRunnable.Add (subview); - View? found = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + View? found = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); Assert.Equal (expectedSubViewFound, found == subview); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -184,7 +184,7 @@ public class GetViewsUnderLocationTests [InlineData (5, 6, false)] public void Returns_Null_If_SubView_NotVisible (int testX, int testY, bool expectedSubViewFound) { - Application.Current = new () + Application.TopRunnable = new () { Width = 10, Height = 10 }; @@ -195,12 +195,12 @@ public class GetViewsUnderLocationTests Width = 5, Height = 5, Visible = false }; - Application.Current.Add (subview); + Application.TopRunnable.Add (subview); - View? found = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + View? found = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); Assert.Equal (expectedSubViewFound, found == subview); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -214,7 +214,7 @@ public class GetViewsUnderLocationTests [InlineData (5, 6, false)] public void Returns_Null_If_Not_Visible_And_SubView_Visible (int testX, int testY, bool expectedSubViewFound) { - Application.Current = new () + Application.TopRunnable = new () { Width = 10, Height = 10, Visible = false @@ -225,14 +225,14 @@ public class GetViewsUnderLocationTests X = 1, Y = 2, Width = 5, Height = 5 }; - Application.Current.Add (subview); + Application.TopRunnable.Add (subview); subview.Visible = true; Assert.True (subview.Visible); - Assert.False (Application.Current.Visible); - View? found = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + Assert.False (Application.TopRunnable.Visible); + View? found = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); Assert.Equal (expectedSubViewFound, found == subview); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -249,23 +249,23 @@ public class GetViewsUnderLocationTests [InlineData (6, 7, true)] public void Returns_Correct_If_Start_Has_Adornments (int testX, int testY, bool expectedSubViewFound) { - Application.Current = new () + Application.TopRunnable = new () { Width = 10, Height = 10 }; - Application.Current.Margin!.Thickness = new (1); + Application.TopRunnable.Margin!.Thickness = new (1); var subview = new View { X = 1, Y = 2, Width = 5, Height = 5 }; - Application.Current.Add (subview); + Application.TopRunnable.Add (subview); - View? found = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + View? found = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); Assert.Equal (expectedSubViewFound, found == subview); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -280,24 +280,24 @@ public class GetViewsUnderLocationTests [InlineData (-1, 0, 0, false)] public void Returns_Correct_If_Start_Has_Offset_Viewport (int offset, int testX, int testY, bool expectedSubViewFound) { - Application.Current = new () + Application.TopRunnable = new () { Width = 10, Height = 10, ViewportSettings = ViewportSettingsFlags.AllowNegativeLocation }; - Application.Current.Viewport = new (offset, offset, 10, 10); + Application.TopRunnable.Viewport = new (offset, offset, 10, 10); var subview = new View { X = 1, Y = 1, Width = 2, Height = 2 }; - Application.Current.Add (subview); + Application.TopRunnable.Add (subview); - View? found = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + View? found = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); Assert.Equal (expectedSubViewFound, found == subview); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -313,25 +313,25 @@ public class GetViewsUnderLocationTests [InlineData (6, 7, false)] public void Returns_Correct_If_Start_Has_Adornment_WithSubView (int testX, int testY, bool expectedSubViewFound) { - Application.Current = new () + Application.TopRunnable = new () { Width = 10, Height = 10 }; - Application.Current.Padding!.Thickness = new (1); + Application.TopRunnable.Padding!.Thickness = new (1); var subview = new View { X = Pos.AnchorEnd (1), Y = Pos.AnchorEnd (1), Width = 1, Height = 1 }; - Application.Current.Padding.Add (subview); - Application.Current.BeginInit (); - Application.Current.EndInit (); + Application.TopRunnable.Padding.Add (subview); + Application.TopRunnable.BeginInit (); + Application.TopRunnable.EndInit (); - View? found = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + View? found = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); Assert.Equal (expectedSubViewFound, found == subview); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -347,17 +347,17 @@ public class GetViewsUnderLocationTests { Application.ResetState (true); - Application.Current = new () + Application.TopRunnable = new () { Id = "Top", Width = 10, Height = 10 }; - Application.Current.Margin!.Thickness = new (1); - Application.Current.Margin!.Id = "Margin"; - Application.Current.Border!.Thickness = new (1); - Application.Current.Border!.Id = "Border"; - Application.Current.Padding!.Thickness = new (1); - Application.Current.Padding.Id = "Padding"; + Application.TopRunnable.Margin!.Thickness = new (1); + Application.TopRunnable.Margin!.Id = "Margin"; + Application.TopRunnable.Border!.Thickness = new (1); + Application.TopRunnable.Border!.Id = "Border"; + Application.TopRunnable.Padding!.Thickness = new (1); + Application.TopRunnable.Padding.Id = "Padding"; var subview = new View { @@ -365,13 +365,13 @@ public class GetViewsUnderLocationTests X = 1, Y = 1, Width = 1, Height = 1 }; - Application.Current.Add (subview); + Application.TopRunnable.Add (subview); - List viewsUnderMouse = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse); + List viewsUnderMouse = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse); string [] foundIds = viewsUnderMouse.Select (v => v!.Id).ToArray (); Assert.Equal (expectedViewsFound, foundIds); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -388,7 +388,7 @@ public class GetViewsUnderLocationTests [InlineData (2, 3, new [] { "Top", "subview" })] public void Returns_Correct_If_SubView_Has_Adornments (int testX, int testY, string [] expectedViewsFound) { - Application.Current = new () + Application.TopRunnable = new () { Id = "Top", Width = 10, Height = 10 @@ -402,13 +402,13 @@ public class GetViewsUnderLocationTests }; subview.Border!.Thickness = new (1); subview.Border!.Id = "border"; - Application.Current.Add (subview); + Application.TopRunnable.Add (subview); - List viewsUnderMouse = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse); + List viewsUnderMouse = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse); string [] foundIds = viewsUnderMouse.Select (v => v!.Id).ToArray (); Assert.Equal (expectedViewsFound, foundIds); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -425,7 +425,7 @@ public class GetViewsUnderLocationTests [InlineData (2, 3, new [] { "Top", "subview" })] public void Returns_Correct_If_SubView_Has_Adornments_With_TransparentMouse (int testX, int testY, string [] expectedViewsFound) { - Application.Current = new () + Application.TopRunnable = new () { Id = "Top", Width = 10, Height = 10 @@ -440,13 +440,13 @@ public class GetViewsUnderLocationTests subview.Border!.Thickness = new (1); subview.Border!.ViewportSettings = ViewportSettingsFlags.TransparentMouse; subview.Border!.Id = "border"; - Application.Current.Add (subview); + Application.TopRunnable.Add (subview); - List viewsUnderMouse = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse); + List viewsUnderMouse = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse); string [] foundIds = viewsUnderMouse.Select (v => v!.Id).ToArray (); Assert.Equal (expectedViewsFound, foundIds); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -463,7 +463,7 @@ public class GetViewsUnderLocationTests [InlineData (5, 5, true)] public void Returns_Correct_If_SubView_Has_Adornment_WithSubView (int testX, int testY, bool expectedSubViewFound) { - Application.Current = new () + Application.TopRunnable = new () { Width = 10, Height = 10 }; @@ -486,14 +486,14 @@ public class GetViewsUnderLocationTests Height = 1 }; subview.Padding.Add (paddingSubView); - Application.Current.Add (subview); - Application.Current.BeginInit (); - Application.Current.EndInit (); + Application.TopRunnable.Add (subview); + Application.TopRunnable.BeginInit (); + Application.TopRunnable.EndInit (); - View? found = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + View? found = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); Assert.Equal (expectedSubViewFound, found == paddingSubView); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -510,7 +510,7 @@ public class GetViewsUnderLocationTests [InlineData (5, 5, true)] public void Returns_Correct_If_SubView_Is_Scrolled_And_Has_Adornment_WithSubView (int testX, int testY, bool expectedSubViewFound) { - Application.Current = new () + Application.TopRunnable = new () { Width = 10, Height = 10 }; @@ -537,14 +537,14 @@ public class GetViewsUnderLocationTests Height = 1 }; subview.Padding.Add (paddingSubView); - Application.Current.Add (subview); - Application.Current.BeginInit (); - Application.Current.EndInit (); + Application.TopRunnable.Add (subview); + Application.TopRunnable.BeginInit (); + Application.TopRunnable.EndInit (); - View? found = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + View? found = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); Assert.Equal (expectedSubViewFound, found == paddingSubView); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -560,7 +560,7 @@ public class GetViewsUnderLocationTests [InlineData (5, 5, 2)] public void Returns_Correct_With_NestedSubViews (int testX, int testY, int expectedSubViewFound) { - Application.Current = new () + Application.TopRunnable = new () { Width = 10, Height = 10 }; @@ -583,11 +583,11 @@ public class GetViewsUnderLocationTests } } - Application.Current.Add (subviews [0]); + Application.TopRunnable.Add (subviews [0]); - View? found = Application.Current.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + View? found = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); Assert.Equal (expectedSubViewFound, subviews.IndexOf (found!)); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -604,7 +604,7 @@ public class GetViewsUnderLocationTests public void Tiled_SubViews (int mouseX, int mouseY, string [] viewIdStrings) { // Arrange - Application.Current = new () + Application.TopRunnable = new () { Frame = new (0, 0, 10, 10), Id = "top" @@ -630,15 +630,15 @@ public class GetViewsUnderLocationTests Arrangement = ViewArrangement.Overlapped }; // at 2,2 to 4,3 (screen) view.Add (subView); - Application.Current.Add (view); + Application.TopRunnable.Add (view); - List found = Application.Current.GetViewsUnderLocation (new (mouseX, mouseY), ViewportSettingsFlags.TransparentMouse); + List found = Application.TopRunnable.GetViewsUnderLocation (new (mouseX, mouseY), ViewportSettingsFlags.TransparentMouse); string [] foundIds = found.Select (v => v!.Id).ToArray (); Assert.Equal (viewIdStrings, foundIds); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -656,7 +656,7 @@ public class GetViewsUnderLocationTests public void Popover (int mouseX, int mouseY, string [] viewIdStrings) { // Arrange - Application.Current = new () + Application.TopRunnable = new () { Frame = new (0, 0, 10, 10), Id = "top" @@ -683,15 +683,15 @@ public class GetViewsUnderLocationTests }; // at 2,2 to 4,3 (screen) view.Add (popOver); - Application.Current.Add (view); + Application.TopRunnable.Add (view); - List found = Application.Current.GetViewsUnderLocation (new (mouseX, mouseY), ViewportSettingsFlags.TransparentMouse); + List found = Application.TopRunnable.GetViewsUnderLocation (new (mouseX, mouseY), ViewportSettingsFlags.TransparentMouse); string [] foundIds = found.Select (v => v!.Id).ToArray (); Assert.Equal (viewIdStrings, foundIds); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -717,9 +717,9 @@ public class GetViewsUnderLocationTests Application.SessionStack.Clear (); Application.SessionStack.Push (topToplevel); Application.SessionStack.Push (secondaryToplevel); - Application.Current = secondaryToplevel; + Application.TopRunnable = secondaryToplevel; - List found = Application.Current.GetViewsUnderLocation (new (2, 2), ViewportSettingsFlags.TransparentMouse); + List found = Application.TopRunnable.GetViewsUnderLocation (new (2, 2), ViewportSettingsFlags.TransparentMouse); Assert.Contains (found, v => v?.Id == topToplevel.Id); Assert.Contains (found, v => v == topToplevel); @@ -751,9 +751,9 @@ public class GetViewsUnderLocationTests Application.SessionStack.Clear (); Application.SessionStack.Push (topToplevel); Application.SessionStack.Push (secondaryToplevel); - Application.Current = secondaryToplevel; + Application.TopRunnable = secondaryToplevel; - List found = Application.Current.GetViewsUnderLocation (new (7, 7), ViewportSettingsFlags.TransparentMouse); + List found = Application.TopRunnable.GetViewsUnderLocation (new (7, 7), ViewportSettingsFlags.TransparentMouse); Assert.Contains (found, v => v?.Id == secondaryToplevel.Id); Assert.DoesNotContain (found, v => v?.Id == topToplevel.Id); @@ -784,17 +784,17 @@ public class GetViewsUnderLocationTests Application.SessionStack.Clear (); Application.SessionStack.Push (topToplevel); Application.SessionStack.Push (secondaryToplevel); - Application.Current = secondaryToplevel; + Application.TopRunnable = secondaryToplevel; secondaryToplevel.Margin!.ViewportSettings = ViewportSettingsFlags.None; - List found = Application.Current.GetViewsUnderLocation (new (5, 5), ViewportSettingsFlags.TransparentMouse); + List found = Application.TopRunnable.GetViewsUnderLocation (new (5, 5), ViewportSettingsFlags.TransparentMouse); Assert.Contains (found, v => v == secondaryToplevel); Assert.Contains (found, v => v == secondaryToplevel.Margin); Assert.DoesNotContain (found, v => v?.Id == topToplevel.Id); secondaryToplevel.Margin!.ViewportSettings = ViewportSettingsFlags.TransparentMouse; - found = Application.Current.GetViewsUnderLocation (new (5, 5), ViewportSettingsFlags.TransparentMouse); + found = Application.TopRunnable.GetViewsUnderLocation (new (5, 5), ViewportSettingsFlags.TransparentMouse); Assert.DoesNotContain (found, v => v == secondaryToplevel); Assert.DoesNotContain (found, v => v == secondaryToplevel.Margin); Assert.Contains (found, v => v?.Id == topToplevel.Id); @@ -827,9 +827,9 @@ public class GetViewsUnderLocationTests Application.SessionStack.Clear (); Application.SessionStack.Push (topToplevel); Application.SessionStack.Push (secondaryToplevel); - Application.Current = secondaryToplevel; + Application.TopRunnable = secondaryToplevel; - List found = Application.Current.GetViewsUnderLocation (new (20, 20), ViewportSettingsFlags.TransparentMouse); + List found = Application.TopRunnable.GetViewsUnderLocation (new (20, 20), ViewportSettingsFlags.TransparentMouse); Assert.Empty (found); topToplevel.Dispose (); diff --git a/Tests/UnitTests/View/Layout/Pos.CombineTests.cs b/Tests/UnitTests/View/Layout/Pos.CombineTests.cs index cbbf50da6..6a495968d 100644 --- a/Tests/UnitTests/View/Layout/Pos.CombineTests.cs +++ b/Tests/UnitTests/View/Layout/Pos.CombineTests.cs @@ -40,7 +40,7 @@ public class PosCombineTests (ITestOutputHelper output) [SetupFakeApplication] public void PosCombine_DimCombine_View_With_SubViews () { - Application.Current = new Toplevel () { Width = 80, Height = 25 }; + Application.TopRunnable = new Toplevel () { Width = 80, Height = 25 }; var win1 = new Window { Id = "win1", Width = 20, Height = 10 }; var view1 = new View { @@ -59,18 +59,18 @@ public class PosCombineTests (ITestOutputHelper output) view2.Add (view3); win2.Add (view2); win1.Add (view1, win2); - Application.Current.Add (win1); - Application.Current.Layout (); + Application.TopRunnable.Add (win1); + Application.TopRunnable.Layout (); - Assert.Equal (new Rectangle (0, 0, 80, 25), Application.Current.Frame); + Assert.Equal (new Rectangle (0, 0, 80, 25), Application.TopRunnable.Frame); Assert.Equal (new Rectangle (0, 0, 5, 1), view1.Frame); Assert.Equal (new Rectangle (0, 0, 20, 10), win1.Frame); Assert.Equal (new Rectangle (0, 2, 10, 3), win2.Frame); Assert.Equal (new Rectangle (0, 0, 8, 1), view2.Frame); Assert.Equal (new Rectangle (0, 0, 7, 1), view3.Frame); - var foundView = Application.Current.GetViewsUnderLocation (new Point(9, 4), ViewportSettingsFlags.None).LastOrDefault (); + var foundView = Application.TopRunnable.GetViewsUnderLocation (new Point(9, 4), ViewportSettingsFlags.None).LastOrDefault (); Assert.Equal (foundView, view2); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Fact] @@ -89,13 +89,13 @@ public class PosCombineTests (ITestOutputHelper output) top.Add (w); Application.Begin (top); - f.X = Pos.X (Application.Current) + Pos.X (v2) - Pos.X (v1); - f.Y = Pos.Y (Application.Current) + Pos.Y (v2) - Pos.Y (v1); + f.X = Pos.X (Application.TopRunnable) + Pos.X (v2) - Pos.X (v1); + f.Y = Pos.Y (Application.TopRunnable) + Pos.Y (v2) - Pos.Y (v1); - Application.Current.SubViewsLaidOut += (s, e) => + Application.TopRunnable.SubViewsLaidOut += (s, e) => { - Assert.Equal (0, Application.Current.Frame.X); - Assert.Equal (0, Application.Current.Frame.Y); + Assert.Equal (0, Application.TopRunnable.Frame.X); + Assert.Equal (0, Application.TopRunnable.Frame.Y); Assert.Equal (2, w.Frame.X); Assert.Equal (2, w.Frame.Y); Assert.Equal (2, f.Frame.X); @@ -109,7 +109,7 @@ public class PosCombineTests (ITestOutputHelper output) Application.StopAfterFirstIteration = true; Assert.Throws (() => Application.Run ()); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); top.Dispose (); Application.Shutdown (); } diff --git a/Tests/UnitTests/View/Layout/SetLayoutTests.cs b/Tests/UnitTests/View/Layout/SetLayoutTests.cs index 7ccf52dca..e5eb6b027 100644 --- a/Tests/UnitTests/View/Layout/SetLayoutTests.cs +++ b/Tests/UnitTests/View/Layout/SetLayoutTests.cs @@ -12,7 +12,7 @@ public class SetLayoutTests (ITestOutputHelper output) [AutoInitShutdown] public void Screen_Size_Change_Causes_Layout () { - Application.Current = new (); + Application.TopRunnable = new (); var view = new View { @@ -22,21 +22,21 @@ public class SetLayoutTests (ITestOutputHelper output) Height = 1, Text = "0123456789" }; - Application.Current.Add (view); + Application.TopRunnable.Add (view); - var rs = Application.Begin (Application.Current); + var rs = Application.Begin (Application.TopRunnable); Application.Driver!.SetScreenSize (80, 25); Assert.Equal (new (0, 0, 80, 25), new Rectangle (0, 0, Application.Screen.Width, Application.Screen.Height)); - Assert.Equal (new (0, 0, Application.Screen.Width, Application.Screen.Height), Application.Current.Frame); - Assert.Equal (new (0, 0, 80, 25), Application.Current.Frame); + Assert.Equal (new (0, 0, Application.Screen.Width, Application.Screen.Height), Application.TopRunnable.Frame); + Assert.Equal (new (0, 0, 80, 25), Application.TopRunnable.Frame); Application.Driver!.SetScreenSize (20, 10); - Assert.Equal (new (0, 0, Application.Screen.Width, Application.Screen.Height), Application.Current.Frame); + Assert.Equal (new (0, 0, Application.Screen.Width, Application.Screen.Height), Application.TopRunnable.Frame); - Assert.Equal (new (0, 0, 20, 10), Application.Current.Frame); + Assert.Equal (new (0, 0, 20, 10), Application.TopRunnable.Frame); Application.End (rs); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } } diff --git a/Tests/UnitTests/View/Navigation/CanFocusTests.cs b/Tests/UnitTests/View/Navigation/CanFocusTests.cs index 8a82c605d..09b71ef0d 100644 --- a/Tests/UnitTests/View/Navigation/CanFocusTests.cs +++ b/Tests/UnitTests/View/Navigation/CanFocusTests.cs @@ -89,13 +89,13 @@ public class CanFocusTests public void CanFocus_Set_True_Get_AdvanceFocus_Works () { IApplication app = Application.Create (); - app.Current = new () { App = app }; + app.TopRunnable = new () { App = app }; Label label = new () { Text = "label" }; View view = new () { Text = "view", CanFocus = true }; - app.Current.Add (label, view); + app.TopRunnable.Add (label, view); - app.Current.SetFocus (); + app.TopRunnable.SetFocus (); Assert.Equal (view, app.Navigation!.GetFocused ()); Assert.False (label.CanFocus); Assert.False (label.HasFocus); @@ -125,7 +125,7 @@ public class CanFocusTests Assert.True (label.HasFocus); Assert.False (view.HasFocus); - app.Current.Dispose (); + app.TopRunnable.Dispose (); app.ResetState (); } } diff --git a/Tests/UnitTests/View/Navigation/NavigationTests.cs b/Tests/UnitTests/View/Navigation/NavigationTests.cs index 466d6a1ca..d52d5a0ef 100644 --- a/Tests/UnitTests/View/Navigation/NavigationTests.cs +++ b/Tests/UnitTests/View/Navigation/NavigationTests.cs @@ -27,7 +27,7 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews } Toplevel top = new (); - Application.Current = top; + Application.TopRunnable = top; View otherView = new () { @@ -117,7 +117,7 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews } Toplevel top = new (); - Application.Current = top; + Application.TopRunnable = top; View otherView = new () { @@ -148,8 +148,8 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews // Ensure the view is Visible view.Visible = true; - Application.Current.SetFocus (); - Assert.True (Application.Current!.HasFocus); + Application.TopRunnable.SetFocus (); + Assert.True (Application.TopRunnable!.HasFocus); Assert.True (top.HasFocus); // Start with the focus on our test view @@ -280,7 +280,7 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews Toplevel top = new (); - Application.Current = top; + Application.TopRunnable = top; View otherView = new () { diff --git a/Tests/UnitTests/View/TextTests.cs b/Tests/UnitTests/View/TextTests.cs index dba01c151..53d882756 100644 --- a/Tests/UnitTests/View/TextTests.cs +++ b/Tests/UnitTests/View/TextTests.cs @@ -396,7 +396,7 @@ Y Assert.Equal (new (1, 5), view.TextFormatter.ConstrainToSize); Assert.Equal (new () { "Views" }, view.TextFormatter.GetLines ()); Assert.Equal (new (0, 0, 4, 10), win.Frame); - Assert.Equal (new (0, 0, 4, 10), Application.Current.Frame); + Assert.Equal (new (0, 0, 4, 10), Application.TopRunnable.Frame); var expected = @" ┌──┐ diff --git a/Tests/UnitTests/View/ViewCommandTests.cs b/Tests/UnitTests/View/ViewCommandTests.cs index cc48653eb..bb611ac03 100644 --- a/Tests/UnitTests/View/ViewCommandTests.cs +++ b/Tests/UnitTests/View/ViewCommandTests.cs @@ -46,9 +46,9 @@ public class ViewCommandTests w.LayoutSubViews (); - Application.Current = w; + Application.TopRunnable = w; Application.SessionStack.Push (w); - Assert.Same (Application.Current, w); + Assert.Same (Application.TopRunnable, w); // Click button 2 Rectangle btn2Frame = btnB.FrameToScreen (); @@ -121,9 +121,9 @@ public class ViewCommandTests w.Add (btn); - Application.Current = w; + Application.TopRunnable = w; Application.SessionStack.Push (w); - Assert.Same (Application.Current, w); + Assert.Same (Application.TopRunnable, w); w.LayoutSubViews (); diff --git a/Tests/UnitTests/View/Viewport/ViewportSettings.TransparentMouseTests.cs b/Tests/UnitTests/View/Viewport/ViewportSettings.TransparentMouseTests.cs index 2e66c4c9b..94082c3db 100644 --- a/Tests/UnitTests/View/Viewport/ViewportSettings.TransparentMouseTests.cs +++ b/Tests/UnitTests/View/Viewport/ViewportSettings.TransparentMouseTests.cs @@ -23,7 +23,7 @@ public class TransparentMouseTests { Id = "top", }; - Application.Current = top; + Application.TopRunnable = top; var underlying = new MouseTrackingView { Id = "underlying", X = 0, Y = 0, Width = 10, Height = 10 }; var overlay = new MouseTrackingView { Id = "overlay", X = 0, Y = 0, Width = 10, Height = 10, ViewportSettings = ViewportSettingsFlags.TransparentMouse }; @@ -56,7 +56,7 @@ public class TransparentMouseTests { // Arrange var top = new Toplevel (); - Application.Current = top; + Application.TopRunnable = top; var underlying = new MouseTrackingView { X = 0, Y = 0, Width = 10, Height = 10 }; var overlay = new MouseTrackingView { X = 0, Y = 0, Width = 10, Height = 10, ViewportSettings = ViewportSettingsFlags.None }; @@ -90,7 +90,7 @@ public class TransparentMouseTests { // Arrange var top = new Toplevel (); - Application.Current = top; + Application.TopRunnable = top; var underlying = new MouseTrackingView { X = 0, Y = 0, Width = 10, Height = 10, ViewportSettings = ViewportSettingsFlags.TransparentMouse }; var overlay = new MouseTrackingView { X = 0, Y = 0, Width = 10, Height = 10, ViewportSettings = ViewportSettingsFlags.TransparentMouse }; diff --git a/Tests/UnitTests/Views/AppendAutocompleteTests.cs b/Tests/UnitTests/Views/AppendAutocompleteTests.cs index 3fa75b7b8..ceb4f883a 100644 --- a/Tests/UnitTests/Views/AppendAutocompleteTests.cs +++ b/Tests/UnitTests/Views/AppendAutocompleteTests.cs @@ -30,12 +30,12 @@ public class AppendAutocompleteTests (ITestOutputHelper output) Assert.Equal ("f", tf.Text); // Still has focus though - Assert.Same (tf, Application.Current.Focused); + Assert.Same (tf, Application.TopRunnable.Focused); // But can tab away Application.RaiseKeyDownEvent ('\t'); - Assert.NotSame (tf, Application.Current.Focused); - Application.Current.Dispose (); + Assert.NotSame (tf, Application.TopRunnable.Focused); + Application.TopRunnable.Dispose (); } [Fact] @@ -69,7 +69,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) tf.PositionCursor (); DriverAssert.AssertDriverContentsAre ("fish", output); Assert.Equal ("fi", tf.Text); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Theory] @@ -107,7 +107,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) tf.PositionCursor (); DriverAssert.AssertDriverContentsAre ("fish", output); Assert.Equal ("f", tf.Text); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Fact] @@ -133,7 +133,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) tf.Draw (); DriverAssert.AssertDriverContentsAre ("f", output); Assert.Equal ("f ", tf.Text); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Fact] @@ -157,7 +157,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) tf.Draw (); DriverAssert.AssertDriverContentsAre ("fx", output); Assert.Equal ("fx", tf.Text); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Fact] @@ -195,7 +195,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) tf.Draw (); DriverAssert.AssertDriverContentsAre ("my FISH", output); Assert.Equal ("my FISH", tf.Text); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Fact] @@ -231,12 +231,12 @@ public class AppendAutocompleteTests (ITestOutputHelper output) Assert.Equal ("fish", tf.Text); // Tab should autcomplete but not move focus - Assert.Same (tf, Application.Current.Focused); + Assert.Same (tf, Application.TopRunnable.Focused); // Second tab should move focus (nothing to autocomplete) Application.RaiseKeyDownEvent ('\t'); - Assert.NotSame (tf, Application.Current.Focused); - Application.Current.Dispose (); + Assert.NotSame (tf, Application.TopRunnable.Focused); + Application.TopRunnable.Dispose (); } [Theory] @@ -256,7 +256,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) tf.PositionCursor (); DriverAssert.AssertDriverContentsAre (expectRender, output); Assert.Equal ("f", tf.Text); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } private TextField GetTextFieldsInView () diff --git a/Tests/UnitTests/Views/ButtonTests.cs b/Tests/UnitTests/Views/ButtonTests.cs index 83d8cc032..1f97316d7 100644 --- a/Tests/UnitTests/Views/ButtonTests.cs +++ b/Tests/UnitTests/Views/ButtonTests.cs @@ -209,7 +209,7 @@ public class ButtonTests (ITestOutputHelper output) clicked = false; // Toplevel does not handle Enter, so it should get passed on to button - Assert.False (Application.Current.NewKeyDownEvent (Key.Enter)); + Assert.False (Application.TopRunnable.NewKeyDownEvent (Key.Enter)); Assert.True (clicked); clicked = false; diff --git a/Tests/UnitTests/Views/CheckBoxTests.cs b/Tests/UnitTests/Views/CheckBoxTests.cs index 39fe5db91..0324e0fde 100644 --- a/Tests/UnitTests/Views/CheckBoxTests.cs +++ b/Tests/UnitTests/Views/CheckBoxTests.cs @@ -15,11 +15,11 @@ public class CheckBoxTests (ITestOutputHelper output) [Fact] public void Commands_Select () { - Application.Current = new (); + Application.TopRunnable = new (); View otherView = new () { CanFocus = true }; var ckb = new CheckBox (); - Application.Current.Add (ckb, otherView); - Application.Current.SetFocus (); + Application.TopRunnable.Add (ckb, otherView); + Application.TopRunnable.SetFocus (); Assert.True (ckb.HasFocus); var checkedStateChangingCount = 0; @@ -63,7 +63,7 @@ public class CheckBoxTests (ITestOutputHelper output) Assert.Equal (3, selectCount); Assert.Equal (1, acceptCount); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (); } diff --git a/Tests/UnitTests/Views/ColorPickerTests.cs b/Tests/UnitTests/Views/ColorPickerTests.cs index 8e454a289..7c960a18b 100644 --- a/Tests/UnitTests/Views/ColorPickerTests.cs +++ b/Tests/UnitTests/Views/ColorPickerTests.cs @@ -12,7 +12,7 @@ public class ColorPickerTests var otherView = new View { CanFocus = true }; - Application.Current?.Add (otherView); // thi sets focus to otherView + Application.TopRunnable?.Add (otherView); // thi sets focus to otherView Assert.True (otherView.HasFocus); cp.SetFocus (); @@ -772,11 +772,11 @@ public class ColorPickerTests cp.Style.ShowColorName = showName; cp.ApplyStyleChanges (); - Application.Current = new () { Width = 20, Height = 5 }; - Application.Current.Add (cp); + Application.TopRunnable = new () { Width = 20, Height = 5 }; + Application.TopRunnable.Add (cp); - Application.Current.LayoutSubViews (); - Application.Current.SetFocus (); + Application.TopRunnable.LayoutSubViews (); + Application.TopRunnable.SetFocus (); return cp; } diff --git a/Tests/UnitTests/Views/ComboBoxTests.cs b/Tests/UnitTests/Views/ComboBoxTests.cs index 260251659..43059f823 100644 --- a/Tests/UnitTests/Views/ComboBoxTests.cs +++ b/Tests/UnitTests/Views/ComboBoxTests.cs @@ -1015,7 +1015,7 @@ Three { var cb = new ComboBox (); var top = new Toplevel (); - Application.Current = top; + Application.TopRunnable = top; top.Add (cb); top.FocusDeepest (NavigationDirection.Forward, null); @@ -1054,7 +1054,7 @@ Three Assert.Equal (-1, cb.SelectedItem); Assert.Equal ("", cb.Text); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } } diff --git a/Tests/UnitTests/Views/HexViewTests.cs b/Tests/UnitTests/Views/HexViewTests.cs index 63ea65cea..e2fe7e365 100644 --- a/Tests/UnitTests/Views/HexViewTests.cs +++ b/Tests/UnitTests/Views/HexViewTests.cs @@ -32,9 +32,9 @@ public class HexViewTests { var hv = new HexView (LoadStream (null, out _, true)) { Width = 20, Height = 20 }; - Application.Current = new (); - Application.Current.Add (hv); - Application.Current.SetFocus (); + Application.TopRunnable = new (); + Application.TopRunnable.Add (hv); + Application.TopRunnable.SetFocus (); // Needed because HexView relies on LayoutComplete to calc sizes hv.LayoutSubViews (); @@ -69,14 +69,14 @@ public class HexViewTests Assert.Empty (hv.Edits); Assert.Equal (127, hv.Source.Length); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } [Fact] public void ApplyEdits_With_Argument () { - Application.Current = new (); + Application.TopRunnable = new (); byte [] buffer = Encoding.Default.GetBytes ("Fest"); var original = new MemoryStream (); @@ -87,8 +87,8 @@ public class HexViewTests original.CopyTo (copy); copy.Flush (); var hv = new HexView (copy) { Width = Dim.Fill (), Height = Dim.Fill () }; - Application.Current.Add (hv); - Application.Current.SetFocus (); + Application.TopRunnable.Add (hv); + Application.TopRunnable.SetFocus (); // Needed because HexView relies on LayoutComplete to calc sizes hv.LayoutSubViews (); @@ -119,7 +119,7 @@ public class HexViewTests Assert.Equal ("Zest", Encoding.Default.GetString (readBuffer)); Assert.Equal (Encoding.Default.GetString (buffer), Encoding.Default.GetString (readBuffer)); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -143,10 +143,10 @@ public class HexViewTests public void Position_Encoding_Default () { var hv = new HexView (LoadStream (null, out _)) { Width = 100, Height = 100 }; - Application.Current = new (); - Application.Current.Add (hv); + Application.TopRunnable = new (); + Application.TopRunnable.Add (hv); - Application.Current.LayoutSubViews (); + Application.TopRunnable.LayoutSubViews (); Assert.Equal (63, hv.Source!.Length); Assert.Equal (20, hv.BytesPerLine); @@ -171,7 +171,7 @@ public class HexViewTests Assert.Equal (new (3, 3), hv.GetPosition (hv.Address)); Assert.Equal (hv.Source!.Length, hv.Address); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -179,8 +179,8 @@ public class HexViewTests public void Position_Encoding_Unicode () { var hv = new HexView (LoadStream (null, out _, true)) { Width = 100, Height = 100 }; - Application.Current = new (); - Application.Current.Add (hv); + Application.TopRunnable = new (); + Application.TopRunnable.Add (hv); hv.LayoutSubViews (); @@ -206,7 +206,7 @@ public class HexViewTests Assert.Equal (new (6, 6), hv.GetPosition (hv.Address)); Assert.Equal (hv.Source!.Length, hv.Address); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -258,9 +258,9 @@ public class HexViewTests [Fact] public void KeyBindings_Test_Movement_LeftSide () { - Application.Current = new (); + Application.TopRunnable = new (); var hv = new HexView (LoadStream (null, out _)) { Width = 20, Height = 10 }; - Application.Current.Add (hv); + Application.TopRunnable.Add (hv); hv.LayoutSubViews (); @@ -306,7 +306,7 @@ public class HexViewTests Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp.WithCtrl)); Assert.Equal (0, hv.Address); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -314,10 +314,10 @@ public class HexViewTests public void PositionChanged_Event () { var hv = new HexView (LoadStream (null, out _)) { Width = 20, Height = 10 }; - Application.Current = new (); - Application.Current.Add (hv); + Application.TopRunnable = new (); + Application.TopRunnable.Add (hv); - Application.Current.LayoutSubViews (); + Application.TopRunnable.LayoutSubViews (); HexViewEventArgs hexViewEventArgs = null!; hv.PositionChanged += (s, e) => hexViewEventArgs = e; @@ -331,7 +331,7 @@ public class HexViewTests Assert.Equal (4, hexViewEventArgs.BytesPerLine); Assert.Equal (new (1, 1), hexViewEventArgs.Position); Assert.Equal (5, hexViewEventArgs.Address); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -339,32 +339,32 @@ public class HexViewTests public void Source_Sets_Address_To_Zero_If_Greater_Than_Source_Length () { var hv = new HexView (LoadStream (null, out _)) { Width = 10, Height = 5 }; - Application.Current = new (); - Application.Current.Add (hv); + Application.TopRunnable = new (); + Application.TopRunnable.Add (hv); - Application.Current.Layout (); + Application.TopRunnable.Layout (); Assert.True (hv.NewKeyDownEvent (Key.End)); Assert.Equal (MEM_STRING_LENGTH, hv.Address); hv.Source = new MemoryStream (); - Application.Current.Layout (); + Application.TopRunnable.Layout (); Assert.Equal (0, hv.Address); hv.Source = LoadStream (null, out _); hv.Width = Dim.Fill (); hv.Height = Dim.Fill (); - Application.Current.Layout (); + Application.TopRunnable.Layout (); Assert.Equal (0, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.End)); Assert.Equal (MEM_STRING_LENGTH, hv.Address); hv.Source = new MemoryStream (); - Application.Current.Layout (); + Application.TopRunnable.Layout (); Assert.Equal (0, hv.Address); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } diff --git a/Tests/UnitTests/Views/LabelTests.cs b/Tests/UnitTests/Views/LabelTests.cs index bdda95498..56b701668 100644 --- a/Tests/UnitTests/Views/LabelTests.cs +++ b/Tests/UnitTests/Views/LabelTests.cs @@ -1098,7 +1098,7 @@ t Assert.Equal (new (5, 1), label.TextFormatter.ConstrainToSize); Assert.Equal (["Label"], label.TextFormatter.GetLines ()); Assert.Equal (new (0, 0, 10, 4), win.Frame); - Assert.Equal (new (0, 0, 10, 4), Application.Current.Frame); + Assert.Equal (new (0, 0, 10, 4), Application.TopRunnable.Frame); var expected = @" ┌────────┐ @@ -1157,7 +1157,7 @@ t Assert.Equal (new (5, 1), label.TextFormatter.ConstrainToSize); Assert.Equal (["Label"], label.TextFormatter.GetLines ()); Assert.Equal (new (0, 0, 10, 4), win.Frame); - Assert.Equal (new (0, 0, 10, 4), Application.Current.Frame); + Assert.Equal (new (0, 0, 10, 4), Application.TopRunnable.Frame); var expected = @" ┌────────┐ @@ -1230,10 +1230,10 @@ t CanFocus = true }; - Application.Current = new (); - Application.Current.Add (otherView, label, nextView); + Application.TopRunnable = new (); + Application.TopRunnable.Add (otherView, label, nextView); - Application.Current.SetFocus (); + Application.TopRunnable.SetFocus (); Assert.True (otherView.HasFocus); Assert.True (Application.RaiseKeyDownEvent (label.HotKey)); @@ -1241,7 +1241,7 @@ t Assert.False (label.HasFocus); Assert.True (nextView.HasFocus); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (); } @@ -1251,18 +1251,18 @@ t View otherView = new () { X = 0, Y = 0, Width = 1, Height = 1, Id = "otherView", CanFocus = true }; Label label = new () { X = 0, Y = 1, Text = "_label" }; View nextView = new () { X = Pos.Right (label), Y = Pos.Top (label), Width = 1, Height = 1, Id = "nextView", CanFocus = true }; - Application.Current = new (); - Application.Current.Add (otherView, label, nextView); - Application.Current.Layout (); + Application.TopRunnable = new (); + Application.TopRunnable.Add (otherView, label, nextView); + Application.TopRunnable.Layout (); - Application.Current.SetFocus (); + Application.TopRunnable.SetFocus (); // click on label Application.RaiseMouseEvent (new () { ScreenPosition = label.Frame.Location, Flags = MouseFlags.Button1Clicked }); Assert.False (label.HasFocus); Assert.True (nextView.HasFocus); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (); } @@ -1281,8 +1281,8 @@ t CanFocus = true }; - Application.Current = new (); - Application.Current.Add (label, view); + Application.TopRunnable = new (); + Application.TopRunnable.Add (label, view); view.SetFocus (); Assert.True (label.CanFocus); @@ -1295,7 +1295,7 @@ t Assert.True (label.HasFocus); Assert.False (view.HasFocus); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (); } @@ -1320,14 +1320,14 @@ t CanFocus = true }; - Application.Current = new () + Application.TopRunnable = new () { Width = 10, Height = 10 }; - Application.Current.Add (label, otherView); - Application.Current.SetFocus (); - Application.Current.Layout (); + Application.TopRunnable.Add (label, otherView); + Application.TopRunnable.SetFocus (); + Application.TopRunnable.Layout (); Assert.True (label.CanFocus); Assert.True (label.HasFocus); @@ -1347,7 +1347,7 @@ t Assert.False (label.HasFocus); Assert.True (otherView.HasFocus); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (); } diff --git a/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs b/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs index 1241716ca..c51389bbc 100644 --- a/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs +++ b/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs @@ -698,7 +698,7 @@ public class MenuBarv1Tests (ITestOutputHelper output) Dialog.DefaultShadow = ShadowStyle.None; Button.DefaultShadow = ShadowStyle.None; - Assert.Equal (new (0, 0, 40, 15), Application.Current!.GetClip ()!.GetBounds ()); + Assert.Equal (new (0, 0, 40, 15), Application.TopRunnable!.GetClip ()!.GetBounds ()); DriverAssert.AssertDriverContentsWithFrameAre (@"", output); List items = new () @@ -1100,11 +1100,11 @@ wo Assert.False (copyAction); #if SUPPORT_ALT_TO_ACTIVATE_MENU - Assert.False (Application.Current.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); - Assert.False (Application.Current.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); - Assert.True (Application.Current.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); + Assert.False (Application.TopRunnable.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); + Assert.False (Application.TopRunnable.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); + Assert.True (Application.TopRunnable.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); Assert.True (menu.IsMenuOpen); - Application.Current.Draw (); + Application.TopRunnable.Draw (); string expected = @" File Edit @@ -1113,26 +1113,26 @@ wo var pos = DriverAsserts.AssertDriverContentsWithFrameAre (expected, output); Assert.Equal (new (1, 0, 11, 1), pos); - Assert.True (Application.Current.ProcessKeyDown (new KeyEventArgs (Key.N))); + Assert.True (Application.TopRunnable.ProcessKeyDown (new KeyEventArgs (Key.N))); AutoInitShutdownAttribute.RunIteration (); Assert.False (newAction); // not yet, hot keys don't work if the item is not visible - Assert.True (Application.Current.ProcessKeyDown (new KeyEventArgs (Key.F))); + Assert.True (Application.TopRunnable.ProcessKeyDown (new KeyEventArgs (Key.F))); AutoInitShutdownAttribute.RunIteration (); - Assert.True (Application.Current.ProcessKeyDown (new KeyEventArgs (Key.N))); + Assert.True (Application.TopRunnable.ProcessKeyDown (new KeyEventArgs (Key.N))); AutoInitShutdownAttribute.RunIteration (); Assert.True (newAction); - Application.Current.Draw (); + Application.TopRunnable.Draw (); expected = @" File Edit "; - Assert.False (Application.Current.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); - Assert.True (Application.Current.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); - Assert.True (Application.Current.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); + Assert.False (Application.TopRunnable.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); + Assert.True (Application.TopRunnable.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); + Assert.True (Application.TopRunnable.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); Assert.True (menu.IsMenuOpen); - Application.Current.Draw (); + Application.TopRunnable.Draw (); expected = @" File Edit @@ -1141,8 +1141,8 @@ wo pos = DriverAsserts.AssertDriverContentsWithFrameAre (expected, output); Assert.Equal (new (1, 0, 11, 1), pos); - Assert.True (Application.Current.ProcessKeyDown (new KeyEventArgs (Key.CursorRight))); - Assert.True (Application.Current.ProcessKeyDown (new KeyEventArgs (Key.C))); + Assert.True (Application.TopRunnable.ProcessKeyDown (new KeyEventArgs (Key.CursorRight))); + Assert.True (Application.TopRunnable.ProcessKeyDown (new KeyEventArgs (Key.C))); AutoInitShutdownAttribute.RunIteration (); Assert.True (copyAction); #endif @@ -1211,19 +1211,19 @@ wo Assert.True (menu.NewKeyDownEvent (Key.F.WithAlt)); Assert.True (menu.IsMenuOpen); - Application.Current.Draw (); + Application.TopRunnable.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - Assert.True (Application.Current.SubViews.ElementAt (1).NewKeyDownEvent (Key.N)); + Assert.True (Application.TopRunnable.SubViews.ElementAt (1).NewKeyDownEvent (Key.N)); AutoInitShutdownAttribute.RunIteration (); Assert.True (newAction); Assert.True (menu.NewKeyDownEvent (Key.E.WithAlt)); Assert.True (menu.IsMenuOpen); - Application.Current.Draw (); + Application.TopRunnable.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); - Assert.True (Application.Current.SubViews.ElementAt (1).NewKeyDownEvent (Key.C)); + Assert.True (Application.TopRunnable.SubViews.ElementAt (1).NewKeyDownEvent (Key.C)); AutoInitShutdownAttribute.RunIteration (); Assert.True (copyAction); top.Dispose (); @@ -1945,7 +1945,7 @@ wo Application.AddTimeout (TimeSpan.Zero, () => { - Toplevel top = Application.Current; + Toplevel top = Application.TopRunnable; AutoInitShutdownAttribute.RunIteration (); @@ -2090,7 +2090,7 @@ wo DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); // Open second - Assert.True (Application.Current.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorRight)); + Assert.True (Application.TopRunnable.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorRight)); Assert.True (menu.IsMenuOpen); top.SetClipToScreen (); top.Draw (); @@ -2141,7 +2141,7 @@ wo Assert.True (top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorRight)); Assert.True (menu.IsMenuOpen); top.SetClipToScreen (); - Application.Current.Draw (); + Application.TopRunnable.Draw (); DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); // Close menu @@ -3075,7 +3075,7 @@ Edit pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.True (Application.Current.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorDown)); + Assert.True (Application.TopRunnable.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorDown)); top.Draw (); expected = @" @@ -3090,7 +3090,7 @@ Edit pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.True (Application.Current.SubViews.ElementAt (2).NewKeyDownEvent (Key.CursorLeft)); + Assert.True (Application.TopRunnable.SubViews.ElementAt (2).NewKeyDownEvent (Key.CursorLeft)); top.Draw (); expected = @" @@ -3104,7 +3104,7 @@ Edit pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.True (Application.Current.SubViews.ElementAt (1).NewKeyDownEvent (Key.Esc)); + Assert.True (Application.TopRunnable.SubViews.ElementAt (1).NewKeyDownEvent (Key.Esc)); top.Draw (); expected = @" @@ -3186,7 +3186,7 @@ Edit menu.NewMouseEvent ( new () { - Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = Application.Current.SubViews.ElementAt (1) + Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = Application.TopRunnable.SubViews.ElementAt (1) } ); top.Draw (); @@ -3208,7 +3208,7 @@ Edit menu.NewMouseEvent ( new () { - Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = Application.Current.SubViews.ElementAt (1) + Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = Application.TopRunnable.SubViews.ElementAt (1) } ) ); @@ -3227,7 +3227,7 @@ Edit Assert.Equal (new (1, 0, 10, 6), pos); menu.NewMouseEvent ( - new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Current } + new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.TopRunnable } ); top.Draw (); diff --git a/Tests/UnitTests/Views/ShortcutTests.cs b/Tests/UnitTests/Views/ShortcutTests.cs index 79da2dbc6..df5dac215 100644 --- a/Tests/UnitTests/Views/ShortcutTests.cs +++ b/Tests/UnitTests/Views/ShortcutTests.cs @@ -23,7 +23,7 @@ public class ShortcutTests [InlineData (9, 0)] public void MouseClick_Raises_Accepted (int x, int expectedAccepted) { - Application.Current = new (); + Application.TopRunnable = new (); var shortcut = new Shortcut { @@ -31,8 +31,8 @@ public class ShortcutTests Text = "0", Title = "C" }; - Application.Current.Add (shortcut); - Application.Current.Layout (); + Application.TopRunnable.Add (shortcut); + Application.TopRunnable.Layout (); var accepted = 0; shortcut.Accepting += (s, e) => accepted++; @@ -46,7 +46,7 @@ public class ShortcutTests Assert.Equal (expectedAccepted, accepted); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -74,7 +74,7 @@ public class ShortcutTests int expectedShortcutSelected ) { - Application.Current = new (); + Application.TopRunnable = new (); var shortcut = new Shortcut { @@ -93,9 +93,9 @@ public class ShortcutTests var shortcutSelectCount = 0; shortcut.Selecting += (s, e) => { shortcutSelectCount++; }; - Application.Current.Add (shortcut); - Application.Current.SetRelativeLayout (new (100, 100)); - Application.Current.LayoutSubViews (); + Application.TopRunnable.Add (shortcut); + Application.TopRunnable.SetRelativeLayout (new (100, 100)); + Application.TopRunnable.LayoutSubViews (); Application.RaiseMouseEvent ( new () @@ -109,7 +109,7 @@ public class ShortcutTests Assert.Equal (expectedCommandViewAccepted, commandViewAcceptCount); Assert.Equal (expectedCommandViewSelected, commandViewSelectCount); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -130,7 +130,7 @@ public class ShortcutTests [InlineData (9, 0, 0)] public void MouseClick_Button_CommandView_Raises_Shortcut_Accepted (int mouseX, int expectedAccept, int expectedButtonAccept) { - Application.Current = new (); + Application.TopRunnable = new (); var shortcut = new Shortcut { @@ -147,9 +147,9 @@ public class ShortcutTests }; var buttonAccepted = 0; shortcut.CommandView.Accepting += (s, e) => { buttonAccepted++; }; - Application.Current.Add (shortcut); - Application.Current.SetRelativeLayout (new (100, 100)); - Application.Current.LayoutSubViews (); + Application.TopRunnable.Add (shortcut); + Application.TopRunnable.SetRelativeLayout (new (100, 100)); + Application.TopRunnable.LayoutSubViews (); var accepted = 0; shortcut.Accepting += (s, e) => { accepted++; }; @@ -164,7 +164,7 @@ public class ShortcutTests Assert.Equal (expectedAccept, accepted); Assert.Equal (expectedButtonAccept, buttonAccepted); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -186,7 +186,7 @@ public class ShortcutTests [InlineData (10, 1, 0)] public void MouseClick_CheckBox_CommandView_Raises_Shortcut_Accepted_Selected_Correctly (int mouseX, int expectedAccepted, int expectedCheckboxAccepted) { - Application.Current = new (); + Application.TopRunnable = new (); var shortcut = new Shortcut { @@ -212,9 +212,9 @@ public class ShortcutTests checkboxSelected++; }; - Application.Current.Add (shortcut); - Application.Current.SetRelativeLayout (new (100, 100)); - Application.Current.LayoutSubViews (); + Application.TopRunnable.Add (shortcut); + Application.TopRunnable.SetRelativeLayout (new (100, 100)); + Application.TopRunnable.LayoutSubViews (); var selected = 0; shortcut.Selecting += (s, e) => @@ -241,7 +241,7 @@ public class ShortcutTests Assert.Equal (expectedCheckboxAccepted, checkboxAccepted); Assert.Equal (expectedCheckboxAccepted, checkboxSelected); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -260,7 +260,7 @@ public class ShortcutTests [InlineData (false, KeyCode.F1, 0, 0)] public void KeyDown_Raises_Accepted_Selected (bool canFocus, KeyCode key, int expectedAccept, int expectedSelect) { - Application.Current = new (); + Application.TopRunnable = new (); var shortcut = new Shortcut { @@ -269,7 +269,7 @@ public class ShortcutTests Title = "_C", CanFocus = canFocus }; - Application.Current.Add (shortcut); + Application.TopRunnable.Add (shortcut); shortcut.SetFocus (); Assert.Equal (canFocus, shortcut.HasFocus); @@ -285,7 +285,7 @@ public class ShortcutTests Assert.Equal (expectedAccept, accepted); Assert.Equal (expectedSelect, selected); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -305,7 +305,7 @@ public class ShortcutTests [InlineData (false, KeyCode.F1, 0, 0)] public void KeyDown_CheckBox_Raises_Accepted_Selected (bool canFocus, KeyCode key, int expectedAccept, int expectedSelect) { - Application.Current = new (); + Application.TopRunnable = new (); var shortcut = new Shortcut { @@ -317,7 +317,7 @@ public class ShortcutTests }, CanFocus = canFocus }; - Application.Current.Add (shortcut); + Application.TopRunnable.Add (shortcut); shortcut.SetFocus (); Assert.Equal (canFocus, shortcut.HasFocus); @@ -337,7 +337,7 @@ public class ShortcutTests Assert.Equal (expectedAccept, accepted); Assert.Equal (expectedSelect, selected); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } [Theory] @@ -349,7 +349,7 @@ public class ShortcutTests [InlineData (KeyCode.F1, 0)] public void KeyDown_App_Scope_Invokes_Accept (KeyCode key, int expectedAccept) { - Application.Current = new () { App = Application.Create () }; + Application.TopRunnable = new () { App = Application.Create () }; var shortcut = new Shortcut { @@ -357,9 +357,9 @@ public class ShortcutTests Text = "0", Title = "_C" }; - Application.Current.Add (shortcut); + Application.TopRunnable.Add (shortcut); shortcut.BindKeyToApplication = true; - Application.Current.SetFocus (); + Application.TopRunnable.SetFocus (); var accepted = 0; shortcut.Accepting += (s, e) => accepted++; @@ -368,7 +368,7 @@ public class ShortcutTests Assert.Equal (expectedAccept, accepted); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -427,7 +427,7 @@ public class ShortcutTests [InlineData (false, KeyCode.F1, 0)] public void KeyDown_App_Scope_Invokes_Action (bool canFocus, KeyCode key, int expectedAction) { - Application.Current = new (); + Application.TopRunnable = new (); var shortcut = new Shortcut { @@ -438,13 +438,13 @@ public class ShortcutTests CanFocus = canFocus }; - Application.Current.Add (shortcut); + Application.TopRunnable.Add (shortcut); // Shortcut requires Init for App scoped hotkeys to work - Application.Current.BeginInit (); - Application.Current.EndInit (); + Application.TopRunnable.BeginInit (); + Application.TopRunnable.EndInit (); - Application.Current.SetFocus (); + Application.TopRunnable.SetFocus (); var action = 0; shortcut.Action += () => action++; @@ -453,7 +453,7 @@ public class ShortcutTests Assert.Equal (expectedAction, action); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (true); } @@ -461,11 +461,11 @@ public class ShortcutTests [Fact] public void Scheme_SetScheme_Does_Not_Fault_3664 () { - Application.Current = new (); + Application.TopRunnable = new (); var shortcut = new Shortcut (); - Application.Current.SetScheme (null); + Application.TopRunnable.SetScheme (null); Assert.False (shortcut.HasScheme); Assert.NotNull (shortcut.GetScheme ()); @@ -475,7 +475,7 @@ public class ShortcutTests Assert.False (shortcut.HasScheme); Assert.NotNull (shortcut.GetScheme ()); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); Application.ResetState (); } } diff --git a/Tests/UnitTests/Views/SpinnerViewTests.cs b/Tests/UnitTests/Views/SpinnerViewTests.cs index 35b25e039..1a74d5514 100644 --- a/Tests/UnitTests/Views/SpinnerViewTests.cs +++ b/Tests/UnitTests/Views/SpinnerViewTests.cs @@ -40,7 +40,7 @@ public class SpinnerViewTests (ITestOutputHelper output) // Dispose clears timeout view.Dispose (); Assert.Empty (Application.TimedEvents.Timeouts); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Fact] @@ -62,7 +62,7 @@ public class SpinnerViewTests (ITestOutputHelper output) expected = "/"; DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Fact] @@ -95,7 +95,7 @@ public class SpinnerViewTests (ITestOutputHelper output) //expected = "|"; //DriverAsserts.AssertDriverContentsWithFrameAre (expected, output); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } private SpinnerView GetSpinnerView () diff --git a/Tests/UnitTests/Views/TableViewTests.cs b/Tests/UnitTests/Views/TableViewTests.cs index 4c162a037..7e103b591 100644 --- a/Tests/UnitTests/Views/TableViewTests.cs +++ b/Tests/UnitTests/Views/TableViewTests.cs @@ -3257,19 +3257,19 @@ A B C // Pressing left should move us to the first column without changing focus Application.RaiseKeyDownEvent (Key.CursorLeft); - Assert.Same (tableView, Application.Current!.MostFocused); + Assert.Same (tableView, Application.TopRunnable!.MostFocused); Assert.True (tableView.HasFocus); // Because we are now on the leftmost cell a further left press should move focus Application.RaiseKeyDownEvent (Key.CursorLeft); - Assert.NotSame (tableView, Application.Current.MostFocused); + Assert.NotSame (tableView, Application.TopRunnable.MostFocused); Assert.False (tableView.HasFocus); - Assert.Same (tf1, Application.Current.MostFocused); + Assert.Same (tf1, Application.TopRunnable.MostFocused); Assert.True (tf1.HasFocus); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Fact] @@ -3282,19 +3282,19 @@ A B C // First press should move us up Application.RaiseKeyDownEvent (Key.CursorUp); - Assert.Same (tableView, Application.Current!.MostFocused); + Assert.Same (tableView, Application.TopRunnable!.MostFocused); Assert.True (tableView.HasFocus); // Because we are now on the top row a further press should move focus Application.RaiseKeyDownEvent (Key.CursorUp); - Assert.NotSame (tableView, Application.Current.MostFocused); + Assert.NotSame (tableView, Application.TopRunnable.MostFocused); Assert.False (tableView.HasFocus); - Assert.Same (tf1, Application.Current.MostFocused); + Assert.Same (tf1, Application.TopRunnable.MostFocused); Assert.True (tf1.HasFocus); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Fact] @@ -3307,19 +3307,19 @@ A B C // First press should move us to the rightmost column without changing focus Application.RaiseKeyDownEvent (Key.CursorRight); - Assert.Same (tableView, Application.Current!.MostFocused); + Assert.Same (tableView, Application.TopRunnable!.MostFocused); Assert.True (tableView.HasFocus); // Because we are now on the rightmost cell, a further right press should move focus Application.RaiseKeyDownEvent (Key.CursorRight); - Assert.NotSame (tableView, Application.Current.MostFocused); + Assert.NotSame (tableView, Application.TopRunnable.MostFocused); Assert.False (tableView.HasFocus); - Assert.Same (tf2, Application.Current.MostFocused); + Assert.Same (tf2, Application.TopRunnable.MostFocused); Assert.True (tf2.HasFocus); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Fact] @@ -3332,19 +3332,19 @@ A B C // First press should move us to the bottommost row without changing focus Application.RaiseKeyDownEvent (Key.CursorDown); - Assert.Same (tableView, Application.Current!.MostFocused); + Assert.Same (tableView, Application.TopRunnable!.MostFocused); Assert.True (tableView.HasFocus); // Because we are now on the bottommost cell, a further down press should move focus Application.RaiseKeyDownEvent (Key.CursorDown); - Assert.NotSame (tableView, Application.Current.MostFocused); + Assert.NotSame (tableView, Application.TopRunnable.MostFocused); Assert.False (tableView.HasFocus); - Assert.Same (tf2, Application.Current.MostFocused); + Assert.Same (tf2, Application.TopRunnable.MostFocused); Assert.True (tf2.HasFocus); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Fact] @@ -3357,7 +3357,7 @@ A B C // Pressing shift-left should give us a multi selection Application.RaiseKeyDownEvent (Key.CursorLeft.WithShift); - Assert.Same (tableView, Application.Current!.MostFocused); + Assert.Same (tableView, Application.TopRunnable!.MostFocused); Assert.True (tableView.HasFocus); Assert.Equal (2, tableView.GetAllSelectedCells ().Count ()); @@ -3368,19 +3368,19 @@ A B C // Selection 'clears' just to the single cell and we remain focused Assert.Single (tableView.GetAllSelectedCells ()); - Assert.Same (tableView, Application.Current.MostFocused); + Assert.Same (tableView, Application.TopRunnable.MostFocused); Assert.True (tableView.HasFocus); // A further left will switch focus Application.RaiseKeyDownEvent (Key.CursorLeft); - Assert.NotSame (tableView, Application.Current.MostFocused); + Assert.NotSame (tableView, Application.TopRunnable.MostFocused); Assert.False (tableView.HasFocus); - Assert.Same (tf1, Application.Current.MostFocused); + Assert.Same (tf1, Application.TopRunnable.MostFocused); Assert.True (tf1.HasFocus); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Theory] @@ -3407,7 +3407,7 @@ A B C } /// - /// Creates 3 views on with the focus in the + /// Creates 3 views on with the focus in the /// . This is a helper method to setup tests that want to /// explore moving input focus out of a tableview. /// @@ -3421,16 +3421,16 @@ A B C tableView.EndInit (); - Application.Current = new (); + Application.TopRunnable = new (); tf1 = new (); tf2 = new (); - Application.Current.Add (tf1); - Application.Current.Add (tableView); - Application.Current.Add (tf2); + Application.TopRunnable.Add (tf1); + Application.TopRunnable.Add (tableView); + Application.TopRunnable.Add (tf2); tableView.SetFocus (); - Assert.Same (tableView, Application.Current.MostFocused); + Assert.Same (tableView, Application.TopRunnable.MostFocused); Assert.True (tableView.HasFocus); // Set big table diff --git a/Tests/UnitTests/Views/TextFieldTests.cs b/Tests/UnitTests/Views/TextFieldTests.cs index d41de3152..66ad798bd 100644 --- a/Tests/UnitTests/Views/TextFieldTests.cs +++ b/Tests/UnitTests/Views/TextFieldTests.cs @@ -84,7 +84,7 @@ public class TextFieldTests (ITestOutputHelper output) tf.Draw (); DriverAssert.AssertDriverContentsAre (expectedRender, output); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Fact] @@ -105,7 +105,7 @@ public class TextFieldTests (ITestOutputHelper output) tf.Draw (); DriverAssert.AssertDriverContentsAre ("Misérables", output); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Theory (Skip = "Broke with ContextMenuv2")] @@ -133,7 +133,7 @@ public class TextFieldTests (ITestOutputHelper output) tf.SetClipToScreen (); tf.Draw (); DriverAssert.AssertDriverContentsAre (content, output); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Fact] @@ -158,7 +158,7 @@ public class TextFieldTests (ITestOutputHelper output) tf.SetClipToScreen (); tf.Draw (); DriverAssert.AssertDriverContentsAre ("Enter txt", output); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Fact] @@ -188,7 +188,7 @@ public class TextFieldTests (ITestOutputHelper output) // All characters in "Enter text" should have the caption attribute DriverAssert.AssertDriverAttributesAre ("0000000000", output, Application.Driver, captionAttr); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Fact] @@ -222,7 +222,7 @@ public class TextFieldTests (ITestOutputHelper output) // F is underlined (index 1), remaining characters use normal caption attribute (index 0) DriverAssert.AssertDriverAttributesAre ("1000", output, Application.Driver, captionAttr, hotkeyAttr); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Fact] @@ -256,7 +256,7 @@ public class TextFieldTests (ITestOutputHelper output) // "Enter " (6 chars) + "T" (underlined) + "ext" (3 chars) DriverAssert.AssertDriverAttributesAre ("0000001000", output, Application.Driver, captionAttr, hotkeyAttr); - Application.Current.Dispose (); + Application.TopRunnable.Dispose (); } [Fact] @@ -1691,7 +1691,7 @@ Les Miśerables", { base.Before (methodUnderTest); - //Application.Current.Scheme = Colors.Schemes ["Base"]; + //Application.TopRunnable.Scheme = Colors.Schemes ["Base"]; _textField = new () { // 1 2 3 diff --git a/Tests/UnitTests/Views/TextViewTests.cs b/Tests/UnitTests/Views/TextViewTests.cs index 80f185fb4..10764cf4c 100644 --- a/Tests/UnitTests/Views/TextViewTests.cs +++ b/Tests/UnitTests/Views/TextViewTests.cs @@ -109,7 +109,7 @@ public class TextViewTests Assert.Equal (leftCol, _textView.LeftColumn); } - Application.Current.Remove (_textView); + Application.TopRunnable.Remove (_textView); Application.RequestStop (); } } diff --git a/Tests/UnitTests/Views/ToplevelTests.cs b/Tests/UnitTests/Views/ToplevelTests.cs index 365ebd8bf..f01ad988e 100644 --- a/Tests/UnitTests/Views/ToplevelTests.cs +++ b/Tests/UnitTests/Views/ToplevelTests.cs @@ -69,11 +69,11 @@ public class ToplevelTests #endif Application.Begin (top); - Assert.Equal (top, Application.Current); + Assert.Equal (top, Application.TopRunnable); - // Application.Current without menu and status bar. + // Application.TopRunnable without menu and status bar. View supView = View.GetLocationEnsuringFullVisibility (top, 2, 2, out int nx, out int ny /*, out StatusBar sb*/); - Assert.Equal (Application.Current, supView); + Assert.Equal (Application.TopRunnable, supView); Assert.Equal (0, nx); Assert.Equal (0, ny); @@ -82,7 +82,7 @@ public class ToplevelTests top.Add (new MenuBar ()); Assert.NotNull (top.MenuBar); - // Application.Current with a menu and without status bar. + // Application.TopRunnable with a menu and without status bar. View.GetLocationEnsuringFullVisibility (top, 2, 2, out nx, out ny /*, out sb*/); Assert.Equal (0, nx); Assert.Equal (1, ny); @@ -92,11 +92,11 @@ public class ToplevelTests //top.Add (new StatusBar ()); //Assert.NotNull (top.StatusBar); - // Application.Current with a menu and status bar. + // Application.TopRunnable with a menu and status bar. View.GetLocationEnsuringFullVisibility (top, 2, 2, out nx, out ny /*, out sb*/); Assert.Equal (0, nx); - // The available height is lower than the Application.Current height minus + // The available height is lower than the Application.TopRunnable height minus // the menu bar and status bar, then the top can go beyond the bottom // Assert.Equal (2, ny); //Assert.NotNull (sb); @@ -106,11 +106,11 @@ public class ToplevelTests Assert.Null (top.MenuBar); Assert.NotNull (menuBar); - // Application.Current without a menu and with a status bar. + // Application.TopRunnable without a menu and with a status bar. View.GetLocationEnsuringFullVisibility (top, 2, 2, out nx, out ny /*, out sb*/); Assert.Equal (0, nx); - // The available height is lower than the Application.Current height minus + // The available height is lower than the Application.TopRunnable height minus // the status bar, then the top can go beyond the bottom // Assert.Equal (2, ny); //Assert.NotNull (sb); @@ -127,11 +127,11 @@ public class ToplevelTests // The SuperView is always the same regardless of the caller. supView = View.GetLocationEnsuringFullVisibility (win, 0, 0, out nx, out ny /*, out sb*/); - Assert.Equal (Application.Current, supView); + Assert.Equal (Application.TopRunnable, supView); supView = View.GetLocationEnsuringFullVisibility (win, 0, 0, out nx, out ny /*, out sb*/); - Assert.Equal (Application.Current, supView); + Assert.Equal (Application.TopRunnable, supView); - // Application.Current without menu and status bar. + // Application.TopRunnable without menu and status bar. View.GetLocationEnsuringFullVisibility (win, 0, 0, out nx, out ny /*, out sb*/); Assert.Equal (0, nx); Assert.Equal (0, ny); @@ -141,7 +141,7 @@ public class ToplevelTests top.Add (new MenuBar ()); Assert.NotNull (top.MenuBar); - // Application.Current with a menu and without status bar. + // Application.TopRunnable with a menu and without status bar. View.GetLocationEnsuringFullVisibility (win, 2, 2, out nx, out ny /*, out sb*/); Assert.Equal (0, nx); Assert.Equal (1, ny); @@ -152,11 +152,11 @@ public class ToplevelTests //Assert.NotNull (top.StatusBar); - // Application.Current with a menu and status bar. + // Application.TopRunnable with a menu and status bar. View.GetLocationEnsuringFullVisibility (win, 30, 20, out nx, out ny /*, out sb*/); Assert.Equal (0, nx); - // The available height is lower than the Application.Current height minus + // The available height is lower than the Application.TopRunnable height minus // the menu bar and status bar, then the top can go beyond the bottom //Assert.Equal (20, ny); //Assert.NotNull (sb); @@ -177,7 +177,7 @@ public class ToplevelTests win = new () { Width = 60, Height = 15 }; top.Add (win); - // Application.Current without menu and status bar. + // Application.TopRunnable without menu and status bar. View.GetLocationEnsuringFullVisibility (win, 0, 0, out nx, out ny /*, out sb*/); Assert.Equal (0, nx); Assert.Equal (0, ny); @@ -187,7 +187,7 @@ public class ToplevelTests top.Add (new MenuBar ()); Assert.NotNull (top.MenuBar); - // Application.Current with a menu and without status bar. + // Application.TopRunnable with a menu and without status bar. View.GetLocationEnsuringFullVisibility (win, 2, 2, out nx, out ny /*, out sb*/); Assert.Equal (2, nx); Assert.Equal (2, ny); @@ -198,7 +198,7 @@ public class ToplevelTests //Assert.NotNull (top.StatusBar); - // Application.Current with a menu and status bar. + // Application.TopRunnable with a menu and status bar. View.GetLocationEnsuringFullVisibility (win, 30, 20, out nx, out ny /*, out sb*/); Assert.Equal (20, nx); // 20+60=80 @@ -307,7 +307,7 @@ public class ToplevelTests } else if (iterations == 1) { - Assert.Equal (new (2, 2), Application.Current!.Frame.Location); + Assert.Equal (new (2, 2), Application.TopRunnable!.Frame.Location); } else if (iterations == 2) { @@ -316,12 +316,12 @@ public class ToplevelTests // Grab the mouse Application.RaiseMouseEvent (new () { ScreenPosition = new (3, 2), Flags = MouseFlags.Button1Pressed }); - Assert.Equal (Application.Current!.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (2, 2, 10, 3), Application.Current.Frame); + Assert.Equal (Application.TopRunnable!.Border, Application.Mouse.MouseGrabView); + Assert.Equal (new (2, 2, 10, 3), Application.TopRunnable.Frame); } else if (iterations == 3) { - Assert.Equal (Application.Current!.Border, Application.Mouse.MouseGrabView); + Assert.Equal (Application.TopRunnable!.Border, Application.Mouse.MouseGrabView); // Drag to left Application.RaiseMouseEvent ( @@ -333,38 +333,38 @@ public class ToplevelTests }); AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (Application.Current.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (1, 2, 10, 3), Application.Current.Frame); + Assert.Equal (Application.TopRunnable.Border, Application.Mouse.MouseGrabView); + Assert.Equal (new (1, 2, 10, 3), Application.TopRunnable.Frame); } else if (iterations == 4) { - Assert.Equal (Application.Current!.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (1, 2), Application.Current.Frame.Location); + Assert.Equal (Application.TopRunnable!.Border, Application.Mouse.MouseGrabView); + Assert.Equal (new (1, 2), Application.TopRunnable.Frame.Location); - Assert.Equal (Application.Current.Border, Application.Mouse.MouseGrabView); + Assert.Equal (Application.TopRunnable.Border, Application.Mouse.MouseGrabView); } else if (iterations == 5) { - Assert.Equal (Application.Current!.Border, Application.Mouse.MouseGrabView); + Assert.Equal (Application.TopRunnable!.Border, Application.Mouse.MouseGrabView); // Drag up Application.RaiseMouseEvent (new () { ScreenPosition = new (2, 1), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition }); AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (Application.Current!.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (1, 1, 10, 3), Application.Current.Frame); + Assert.Equal (Application.TopRunnable!.Border, Application.Mouse.MouseGrabView); + Assert.Equal (new (1, 1, 10, 3), Application.TopRunnable.Frame); } else if (iterations == 6) { - Assert.Equal (Application.Current!.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (1, 1), Application.Current.Frame.Location); + Assert.Equal (Application.TopRunnable!.Border, Application.Mouse.MouseGrabView); + Assert.Equal (new (1, 1), Application.TopRunnable.Frame.Location); - Assert.Equal (Application.Current.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (1, 1, 10, 3), Application.Current.Frame); + Assert.Equal (Application.TopRunnable.Border, Application.Mouse.MouseGrabView); + Assert.Equal (new (1, 1, 10, 3), Application.TopRunnable.Frame); } else if (iterations == 7) { - Assert.Equal (Application.Current!.Border, Application.Mouse.MouseGrabView); + Assert.Equal (Application.TopRunnable!.Border, Application.Mouse.MouseGrabView); // Ungrab the mouse Application.RaiseMouseEvent (new () { ScreenPosition = new (2, 1), Flags = MouseFlags.Button1Released }); diff --git a/Tests/UnitTestsParallelizable/TestSetup.cs b/Tests/UnitTestsParallelizable/TestSetup.cs index 178a0d4de..47fdeeb25 100644 --- a/Tests/UnitTestsParallelizable/TestSetup.cs +++ b/Tests/UnitTestsParallelizable/TestSetup.cs @@ -39,7 +39,7 @@ public class GlobalTestSetup : IDisposable // Check that all Application fields and properties are set to their default values // Public Properties - Assert.Null (Application.Current); + Assert.Null (Application.TopRunnable); Assert.Null (Application.Mouse.MouseGrabView); // Don't check Application.ForceDriver diff --git a/docfx/docs/application.md b/docfx/docs/application.md index 298cf6ff5..530f53dc5 100644 --- a/docfx/docs/application.md +++ b/docfx/docs/application.md @@ -192,22 +192,22 @@ public interface IApplication Terminal.Gui v2 modernized its terminology for clarity: -### Application.Current (formerly "Top") +### Application.TopRunnable (formerly "Current", and before that "Top") -The `Current` property represents the currently running Toplevel (the active session): +The `TopRunnable` property represents the Toplevel on the top of the session stack (the active runnable session): ```csharp -// Access the current session -Toplevel? current = app.Current; +// Access the top runnable session +Toplevel? topRunnable = app.TopRunnable; // From within a view -Toplevel? current = App?.Current; +Toplevel? topRunnable = App?.TopRunnable; ``` -**Why "Current" instead of "Top"?** -- Follows .NET patterns (`Thread.CurrentThread`, `HttpContext.Current`) -- Self-documenting: immediately clear it's the "current" active view -- Less confusing than "Top" which could mean "topmost in Z-order" +**Why "TopRunnable"?** +- Clearly indicates it's the top of the runnable session stack +- Aligns with the IRunnable architecture proposal +- Distinguishes from other concepts like "Current" which could be ambiguous ### Application.SessionStack (formerly "TopLevels") @@ -256,7 +256,7 @@ public static partial class Application // OLD: void MyMethod() { - Application.Current?.SetNeedsDraw(); + Application.TopRunnable?.SetNeedsDraw(); } // NEW: @@ -467,7 +467,7 @@ public void Refresh() ❌ AVOID: public void Refresh() { - Application.Current?.SetNeedsDraw(); // Obsolete! + Application.TopRunnable?.SetNeedsDraw(); // Obsolete! } ``` @@ -487,7 +487,7 @@ public class Service ❌ AVOID (obsolete pattern): public void Refresh() { - Application.Current?.SetNeedsDraw(); // Obsolete static access + Application.TopRunnable?.SetNeedsDraw(); // Obsolete static access } ✅ PREFERRED: diff --git a/docfx/docs/migratingfromv1.md b/docfx/docs/migratingfromv1.md index d7fd2c0b0..527301a73 100644 --- a/docfx/docs/migratingfromv1.md +++ b/docfx/docs/migratingfromv1.md @@ -85,12 +85,12 @@ When measuring the screen space taken up by a `string` you can use the extension In v1, @Terminal.Gui.View was derived from `Responder` which supported `IDisposable`. In v2, `Responder` has been removed and @Terminal.Gui.View is the base-class supporting `IDisposable`. -In v1, @Terminal.Gui./Terminal.Gui.Application.Init) automatically created a toplevel view and set [Application.Current](~/api/Terminal.Gui.Application.Current. In v2, @Terminal.Gui.App.Application.Init no longer automatically creates a toplevel or sets @Terminal.Gui.App.Application.Current; app developers must explicitly create the toplevel view and pass it to @Terminal.Gui.App.Application.Run (or use `Application.Run`). Developers are responsible for calling `Dispose` on any toplevel they create before exiting. +In v1, @Terminal.Gui./Terminal.Gui.Application.Init) automatically created a toplevel view and set [Application.TopRunnable](~/api/Terminal.Gui.Application.TopRunnable. In v2, @Terminal.Gui.App.Application.Init no longer automatically creates a toplevel or sets @Terminal.Gui.App.Application.TopRunnable; app developers must explicitly create the toplevel view and pass it to @Terminal.Gui.App.Application.Run (or use `Application.Run`). Developers are responsible for calling `Dispose` on any toplevel they create before exiting. ### How to Fix * Replace `Responder` with @Terminal.Gui.View -* Update any code that assumes `Application.Init` automatically created a toplevel view and set `Application.Current`. +* Update any code that assumes `Application.Init` automatically created a toplevel view and set `Application.TopRunnable`. * Update any code that assumes `Application.Init` automatically disposed of the toplevel view when the application exited. ## Instance-Based Application Architecture @@ -144,7 +144,7 @@ When accessing application services from within views, use the `App` property in // OLD (v1 / obsolete static): public void Refresh() { - Application.Current?.SetNeedsDraw(); + Application.TopRunnable?.SetNeedsDraw(); } // NEW (v2 - use View.App): @@ -591,6 +591,6 @@ new ( * To simplify programming, any `View` added as a SubView another `View` will have it's lifecycle owned by the Superview; when a `View` is disposed, it will call `Dispose` on all the items in the `SubViews` property. Note this behavior is the same as it was in v1, just clarified. -* In v1, `Application.End` called `Dispose ()` on @Terminal.Gui.App.Application.Current (via `Runstate.Toplevel`). This was incorrect as it meant that after `Application.Run` returned, `Application.Current` had been disposed, and any code that wanted to interrogate the results of `Run` by accessing `Application.Current` only worked by accident. This is because GC had not actually happened; if it had the application would have crashed. In v2 `Application.End` does NOT call `Dispose`, and it is the caller to `Application.Run` who is responsible for disposing the `Toplevel` that was either passed to `Application.Run (View)` or created by `Application.Run ()`. +* In v1, `Application.End` called `Dispose ()` on @Terminal.Gui.App.Application.TopRunnable (via `Runstate.Toplevel`). This was incorrect as it meant that after `Application.Run` returned, `Application.TopRunnable` had been disposed, and any code that wanted to interrogate the results of `Run` by accessing `Application.TopRunnable` only worked by accident. This is because GC had not actually happened; if it had the application would have crashed. In v2 `Application.End` does NOT call `Dispose`, and it is the caller to `Application.Run` who is responsible for disposing the `Toplevel` that was either passed to `Application.Run (View)` or created by `Application.Run ()`. * Any code that creates a `Toplevel`, either by using `top = new()` or by calling either `top = Application.Run ()` or `top = ApplicationRun()` must call `top.Dispose` when complete. The exception to this is if `top` is passed to `myView.Add(top)` making it a subview of `myView`. This is because the semantics of `Add` are that the `myView` takes over responsibility for the subviews lifetimes. Of course, if someone calls `myView.Remove(top)` to remove said subview, they then re-take responsbility for `top`'s lifetime and they must call `top.Dispose`. diff --git a/docfx/docs/navigation.md b/docfx/docs/navigation.md index 2c5c2391a..f217ace2c 100644 --- a/docfx/docs/navigation.md +++ b/docfx/docs/navigation.md @@ -181,7 +181,7 @@ return app.Current?.AdvanceFocus (direction, behavior); This method is called from the `Command` handlers bound to the application-scoped keybindings created during `app.Init()`. It is `public` as a convenience. -**Note:** When accessing from within a View, use `App?.Current` instead of `Application.Current` (which is obsolete). +**Note:** When accessing from within a View, use `App?.Current` instead of `Application.TopRunnable` (which is obsolete). This method replaces about a dozen functions in v1 (scattered across `Application` and `Toplevel`). @@ -379,7 +379,7 @@ In v1 `View` had `MostFocused` property that traversed up the view-hierarchy ret var focused = Application.Navigation.GetFocused(); // This replaces the v1 pattern: -// var focused = Application.Current.MostFocused; +// var focused = Application.TopRunnable.MostFocused; ``` ## How Does `View.Add/Remove` Work? diff --git a/docfx/docs/runnable-architecture-proposal.md b/docfx/docs/runnable-architecture-proposal.md new file mode 100644 index 000000000..e9ee14663 --- /dev/null +++ b/docfx/docs/runnable-architecture-proposal.md @@ -0,0 +1,95 @@ +# IRunnable Architecture Proposal + +**Status**: Proposal + +**Version**: 1.6 (Property-Based Architecture) + +**Date**: 2025-01-20 + +## Summary + +This proposal recommends decoupling Terminal.Gui's "Runnable" concept from `Toplevel` and `ViewArrangement.Overlapped`, elevating it to a first-class interface-based abstraction. + +**Key Insight**: Analysis of the codebase reveals that **all runnable sessions are effectively modal** - they block in `Application.Run()` until stopped and capture input. The distinction between "modal" and "non-modal" in the current design is artificial: + +- The `Modal` property only affects input propagation and Z-order, not the fundamental run loop behavior +- All `Toplevel`s block in `Run()` - there's no "background" runnable concept +- Non-modal `Toplevel`s (like `WizardAsView`) are just embedded views with `Modal = false`, not true sessions +- Overlapped windows are managed by `ViewArrangement.Overlapped`, not runnability + +By introducing `IRunnable`, we create a clean separation where: + +- **Runnable** = Can be run as a **UI**-blocking session with `Application.Run()` and returns a result +- **Overlapped** = `ViewArrangement.Overlapped` for window management (orthogonal to runnability) +- **Embedded** = Just views, not runnable at all + +## Terminology + +This proposal introduces new terminology to clarify the architecture: + +| Term | Definition | +|------|------------| +| **`IRunnable`** | Base interface for Views capable of running as an independent session with `Application.Run()` without returning a result. Replaces `Toplevel` as the contract for runnable views. When an `IRunnable` is passed to `IApplication.Run`, `Run` blocks until the `IRunnable` `Stops`. | +| **`IRunnable`** | Generic interface derived from `IRunnable` that can return a typed result. | +| **`Runnable`** | Optional base class that implements `IRunnable` and derives from `View`, providing default lifecycle behavior. Views can derive from this or implement `IRunnable` directly. | +| **`TResult`** | Type parameter specifying the type of result data returned when the runnable completes (e.g., `int` for button index, `string` for file path, enum, or other complex type). `Result` is `null` if the runnable stopped without the user explicitly accepting it (ESC pressed, window closed, etc.). | +| **`Result`** | Property on `IRunnable` that holds the typed result data. Should be set in `IsRunningChanging` handler (when `newValue = false`) **before** the runnable is popped from `RunnableSessionStack`. This allows subscribers to inspect results and optionally cancel the stop. Available after `IApplication.Run` returns. `null` indicates cancellation/non-acceptance. | +| **RunnableSession** | A running instance of an `IRunnable`. Managed by `IApplication` via `Begin()`, `Run()`, `RequestStop()`, and `End()` methods. Represented by a `RunnableSessionToken` on the `RunnableSessionStack`. | +| **`RunnableSessionToken`** | Object returned by `Begin()` that represents a running session. Wraps an `IRunnable` instance (via a `Runnable` property) and is stored in `RunnableSessionStack`. Disposed when session ends. | +| **`RunnableSessionStack`** | A stack of `RunnableSessionToken` instances, each wrapping an `IRunnable`. Tracks all running runnables in the application. Literally a `ConcurrentStack`. Replaces `SessionStack` (formerly `Toplevels`). | +| **`IsRunning`** | Boolean property on `IRunnable` indicating whether the runnable is currently on the `RunnableSessionStack` (i.e., `RunnableSessionStack.Any(token => token.Runnable == this)`). Read-only, derived from stack state. Runnables are added during `IApplication.Begin` and removed in `IApplication.End`. Replaces `Toplevel.Running`. | +| **`IsRunningChanging`** | Cancellable event raised **before** an `IRunnable` is added to or removed from `RunnableSessionStack`. When transitioning to `IsRunning = true`, can be canceled to prevent starting. When transitioning to `IsRunning = false`, allows code to prevent closure (e.g., prompt to save changes) AND is the ideal place to extract `Result` before the runnable is removed from the stack. Event args (`CancelEventArgs`) provide the new state in `NewValue`. Replaces `Toplevel.Closing` and partially `Toplevel.Activate`. | +| **`IsRunningChanged`** | Non-cancellable event raised **after** a runnable has been added to or removed from `RunnableSessionStack`. Fired after `IsRunning` has changed to the new value (true = started, false = stopped). For post-state-change logic (e.g., setting focus after start, cleanup after stop). Replaces `Toplevel.Activated` and `Toplevel.Closed`. | +| **`IsInitialized`** (`View` property) | Boolean property (on `View`) indicating whether a view has completed two-phase initialization (`View.BeginInit/View.EndInit`). From .NET's `ISupportInitialize` pattern. If the `IRunnable.IsInitialized == false`, `BeginInit` is called from `IApplication.Begin` after `IsRunning` has changed to `true`. `EndInit` is called immediately after `BeginInit`. | +| **`Initialized`** (`View` event) | Non-cancellable event raised as `View.EndInit()` completes. | +| **`TopRunnable`** (`IApplication` property) | The `IRunnable` that is on the top of the `RunnableSessionStack` stack. By definition and per-implementation, this `IRunnable` is capturing all mouse and keyboard input and is thus "Modal". Note: any other `IRunnable` instances on `RunnableSessionStack` continue to be laid out, drawn, and receive iteration events; they just don't get any user input. **Renamed from `Current`** to better reflect its purpose as the top runnable in the stack. Synonymous with the runnable having `IsModal = true`. | +| **`IsModal`** | Boolean property on `IRunnable` indicating whether the `IRunnable` is at the top of the `RunnableSessionStack` (i.e., `this == app.TopRunnable` or `app.RunnableSessionStack.Peek().Runnable == this`). The `IRunnable` at the top of the stack gets all mouse/keyboard input and thus is running "modally". Read-only, derived from stack state. `IsModal` represents the concept from the end-user's perspective. | +| **`IsModalChanging`** | Cancellable event raised **before** an `IRunnable` transitions to/from the top of the `RunnableSessionStack`. When becoming modal (`newValue = true`), can be canceled to prevent activation. Event args (`CancelEventArgs`) provide the new state. Replaces `Toplevel.Activate` and `Toplevel.Deactivate`. | +| **`IsModalChanged`** | Non-cancellable event raised **after** an `IRunnable` has transitioned to/from the top of the `RunnableSessionStack`. Fired after `IsModal` has changed to the new value (true = became modal, false = no longer modal). For post-activation logic (e.g., setting focus, updating UI state). Replaces `Toplevel.Activated` and `Toplevel.Deactivated`. | +| **`End`** (`IApplication` method) | Ends a running `IRunnable` instance by removing its `RunnableSessionToken` from the `RunnableSessionStack`. `IsRunningChanging` with `newValue = false` is raised **before** the token is popped from the stack (allowing result extraction and cancellation). `IsRunningChanged` is raised **after** the `Pop` operation. Then, `RunnableSessionStack.Peek()` is called to see if another `IRunnable` instance can transition to `IApplication.TopRunnable`/`IRunnable.IsModal = true`. | +| **`ViewArrangement.Overlapped`** | Layout mode for windows that can overlap with Z-order management. Orthogonal to runnability - overlapped windows can be embedded views (not runnable) or runnable sessions. | + +**Key Architectural Changes:** +- **Simplified**: One interface `IRunnable` replaces both `Toplevel` and the artificial `Modal` property distinction +- **All sessions block**: No concept of "non-modal runnable" - if it's runnable, `Run()` blocks until `RequestStop()` +- **Type-safe results**: Generic `TResult` parameter provides compile-time type safety +- **Decoupled from layout**: Being runnable is independent of `ViewArrangement.Overlapped` +- **Consistent patterns**: All lifecycle events follow Terminal.Gui's Cancellable Work Pattern +- **Result extraction in `Stopping`**: `OnStopping()` is the correct place to extract `Result` before disposal + +## Implementation Status + +### Completed Work + +- [x] **2025-11-20**: Renamed `IApplication.Current` to `IApplication.TopRunnable` to better reflect its role as the top runnable in the session stack + - Updated interface definition in `IApplication.cs` + - Updated implementation in `ApplicationImpl.cs` + - Updated static property in `Application.Current.cs` + - Updated all references in library code (28 occurrences) + - Updated all references in examples (50+ occurrences) + - Updated all references in tests (607 occurrences) + - Updated `View.IsCurrentTop` to use the renamed property + - Updated API documentation comments + - All tests pass + - No new warnings introduced + +### Remaining Work + +The following items from the original proposal are still pending: + +- [ ] Implement `IRunnable` non-generic base interface +- [ ] Implement `IRunnable` generic interface +- [ ] Create optional `Runnable` base class +- [ ] Replace `SessionToken` with `RunnableSessionToken` +- [ ] Replace `SessionStack` (ConcurrentStack) with `RunnableSessionStack` (ConcurrentStack) +- [ ] Add lifecycle events: `IsRunningChanging`, `IsRunningChanged`, `IsModalChanging`, `IsModalChanged` +- [ ] Migrate `Toplevel` to implement `IRunnable` +- [ ] Update all view classes to use new pattern +- [ ] Add comprehensive tests for new architecture +- [ ] Update all documentation + +## See Also + +- [Original Issue #4148](https://github.com/gui-cs/Terminal.Gui/issues/4148) +- [Application Lifecycle Documentation](application.md) +- [View Documentation](View.md) From a229f4e3e96ff7d7f71c443acc5a0b0755e60506 Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 20 Nov 2025 15:03:59 -0700 Subject: [PATCH 08/16] Force update .md doc Somehow it got truncated. --- docfx/docs/runnable-architecture-proposal.md | 1664 +++++++++++++++++- 1 file changed, 1629 insertions(+), 35 deletions(-) diff --git a/docfx/docs/runnable-architecture-proposal.md b/docfx/docs/runnable-architecture-proposal.md index e9ee14663..854095521 100644 --- a/docfx/docs/runnable-architecture-proposal.md +++ b/docfx/docs/runnable-architecture-proposal.md @@ -2,7 +2,7 @@ **Status**: Proposal -**Version**: 1.6 (Property-Based Architecture) +**Version**: 1.7 - Approved - Implementing **Date**: 2025-01-20 @@ -17,7 +17,7 @@ This proposal recommends decoupling Terminal.Gui's "Runnable" concept from `Topl - Non-modal `Toplevel`s (like `WizardAsView`) are just embedded views with `Modal = false`, not true sessions - Overlapped windows are managed by `ViewArrangement.Overlapped`, not runnability -By introducing `IRunnable`, we create a clean separation where: +By introducing `IRunnable`, we create a clean separation where: - **Runnable** = Can be run as a **UI**-blocking session with `Application.Run()` and returns a result - **Overlapped** = `ViewArrangement.Overlapped` for window management (orthogonal to runnability) @@ -31,18 +31,18 @@ This proposal introduces new terminology to clarify the architecture: |------|------------| | **`IRunnable`** | Base interface for Views capable of running as an independent session with `Application.Run()` without returning a result. Replaces `Toplevel` as the contract for runnable views. When an `IRunnable` is passed to `IApplication.Run`, `Run` blocks until the `IRunnable` `Stops`. | | **`IRunnable`** | Generic interface derived from `IRunnable` that can return a typed result. | -| **`Runnable`** | Optional base class that implements `IRunnable` and derives from `View`, providing default lifecycle behavior. Views can derive from this or implement `IRunnable` directly. | +| **`Runnable`** | Optional base class that implements `IRunnable` and derives from `View`, providing default lifecycle behavior. Views can derive from this or implement `IRunnable` directly. | | **`TResult`** | Type parameter specifying the type of result data returned when the runnable completes (e.g., `int` for button index, `string` for file path, enum, or other complex type). `Result` is `null` if the runnable stopped without the user explicitly accepting it (ESC pressed, window closed, etc.). | | **`Result`** | Property on `IRunnable` that holds the typed result data. Should be set in `IsRunningChanging` handler (when `newValue = false`) **before** the runnable is popped from `RunnableSessionStack`. This allows subscribers to inspect results and optionally cancel the stop. Available after `IApplication.Run` returns. `null` indicates cancellation/non-acceptance. | | **RunnableSession** | A running instance of an `IRunnable`. Managed by `IApplication` via `Begin()`, `Run()`, `RequestStop()`, and `End()` methods. Represented by a `RunnableSessionToken` on the `RunnableSessionStack`. | | **`RunnableSessionToken`** | Object returned by `Begin()` that represents a running session. Wraps an `IRunnable` instance (via a `Runnable` property) and is stored in `RunnableSessionStack`. Disposed when session ends. | -| **`RunnableSessionStack`** | A stack of `RunnableSessionToken` instances, each wrapping an `IRunnable`. Tracks all running runnables in the application. Literally a `ConcurrentStack`. Replaces `SessionStack` (formerly `Toplevels`). | +| **`RunnableSessionStack`** | A stack of `RunnableSessionToken` instances, each wrapping an `IRunnable`. Tracks all running runnables in the application. Literally a `ConcurrentStack`. Replaces `SessionStack` (formerly `Toplevels`). | **`IsRunning`** | Boolean property on `IRunnable` indicating whether the runnable is currently on the `RunnableSessionStack` (i.e., `RunnableSessionStack.Any(token => token.Runnable == this)`). Read-only, derived from stack state. Runnables are added during `IApplication.Begin` and removed in `IApplication.End`. Replaces `Toplevel.Running`. | | **`IsRunningChanging`** | Cancellable event raised **before** an `IRunnable` is added to or removed from `RunnableSessionStack`. When transitioning to `IsRunning = true`, can be canceled to prevent starting. When transitioning to `IsRunning = false`, allows code to prevent closure (e.g., prompt to save changes) AND is the ideal place to extract `Result` before the runnable is removed from the stack. Event args (`CancelEventArgs`) provide the new state in `NewValue`. Replaces `Toplevel.Closing` and partially `Toplevel.Activate`. | | **`IsRunningChanged`** | Non-cancellable event raised **after** a runnable has been added to or removed from `RunnableSessionStack`. Fired after `IsRunning` has changed to the new value (true = started, false = stopped). For post-state-change logic (e.g., setting focus after start, cleanup after stop). Replaces `Toplevel.Activated` and `Toplevel.Closed`. | | **`IsInitialized`** (`View` property) | Boolean property (on `View`) indicating whether a view has completed two-phase initialization (`View.BeginInit/View.EndInit`). From .NET's `ISupportInitialize` pattern. If the `IRunnable.IsInitialized == false`, `BeginInit` is called from `IApplication.Begin` after `IsRunning` has changed to `true`. `EndInit` is called immediately after `BeginInit`. | | **`Initialized`** (`View` event) | Non-cancellable event raised as `View.EndInit()` completes. | -| **`TopRunnable`** (`IApplication` property) | The `IRunnable` that is on the top of the `RunnableSessionStack` stack. By definition and per-implementation, this `IRunnable` is capturing all mouse and keyboard input and is thus "Modal". Note: any other `IRunnable` instances on `RunnableSessionStack` continue to be laid out, drawn, and receive iteration events; they just don't get any user input. **Renamed from `Current`** to better reflect its purpose as the top runnable in the stack. Synonymous with the runnable having `IsModal = true`. | +| **`TopRunnable`** (`IApplication` property) | Used to be `Top`, but was recently renamed to `Current` because it was confusing relative to `Toplevel`. It's precise definition in this proposal is "The `IRunnable` that is on the top of the `RunnableSessionStack` stack. And by definition, and per-implementation, this `IRunnable` is capturing all mouse and keyboard input and is thus "Modal". Note, any other `IRunnable` instances on `RunnableSessionStack` continue to be laid out, drawn, and receive iteration events; they just don't get any user input. Another interesting note: No code in the solution other than ./App, ./ViewBase, and tests reference `IApplication.Current` (an indication the previous de-coupling was successful). It also means the name of this property is not that important because it's just an implementation detail, primarily used to enable tests to not have to actually call `Run`. View has `public bool IsCurrentTop => App?.Current == this;`; thus we rename `IApplication.Current` to `IApplication.TopRunnable` and it's synonymous with `IRunnable.IsModal`. | | **`IsModal`** | Boolean property on `IRunnable` indicating whether the `IRunnable` is at the top of the `RunnableSessionStack` (i.e., `this == app.TopRunnable` or `app.RunnableSessionStack.Peek().Runnable == this`). The `IRunnable` at the top of the stack gets all mouse/keyboard input and thus is running "modally". Read-only, derived from stack state. `IsModal` represents the concept from the end-user's perspective. | | **`IsModalChanging`** | Cancellable event raised **before** an `IRunnable` transitions to/from the top of the `RunnableSessionStack`. When becoming modal (`newValue = true`), can be canceled to prevent activation. Event args (`CancelEventArgs`) provide the new state. Replaces `Toplevel.Activate` and `Toplevel.Deactivate`. | | **`IsModalChanged`** | Non-cancellable event raised **after** an `IRunnable` has transitioned to/from the top of the `RunnableSessionStack`. Fired after `IsModal` has changed to the new value (true = became modal, false = no longer modal). For post-activation logic (e.g., setting focus, updating UI state). Replaces `Toplevel.Activated` and `Toplevel.Deactivated`. | @@ -50,46 +50,1640 @@ This proposal introduces new terminology to clarify the architecture: | **`ViewArrangement.Overlapped`** | Layout mode for windows that can overlap with Z-order management. Orthogonal to runnability - overlapped windows can be embedded views (not runnable) or runnable sessions. | **Key Architectural Changes:** -- **Simplified**: One interface `IRunnable` replaces both `Toplevel` and the artificial `Modal` property distinction +- **Simplified**: One interface `IRunnable` replaces both `Toplevel` and the artificial `Modal` property distinction - **All sessions block**: No concept of "non-modal runnable" - if it's runnable, `Run()` blocks until `RequestStop()` - **Type-safe results**: Generic `TResult` parameter provides compile-time type safety - **Decoupled from layout**: Being runnable is independent of `ViewArrangement.Overlapped` - **Consistent patterns**: All lifecycle events follow Terminal.Gui's Cancellable Work Pattern - **Result extraction in `Stopping`**: `OnStopping()` is the correct place to extract `Result` before disposal -## Implementation Status +## Table of Contents -### Completed Work +- [Background](#background) +- [Problems with Current Design](#problems-with-current-design) +- [Proposed Architecture](#proposed-architecture) +- [Detailed API Design](#detailed-api-design) +- [Migration Path](#migration-path) +- [Implementation Strategy](#implementation-strategy) +- [Benefits](#benefits) +- [Open Questions](#open-questions) -- [x] **2025-11-20**: Renamed `IApplication.Current` to `IApplication.TopRunnable` to better reflect its role as the top runnable in the session stack - - Updated interface definition in `IApplication.cs` - - Updated implementation in `ApplicationImpl.cs` - - Updated static property in `Application.Current.cs` - - Updated all references in library code (28 occurrences) - - Updated all references in examples (50+ occurrences) - - Updated all references in tests (607 occurrences) - - Updated `View.IsCurrentTop` to use the renamed property - - Updated API documentation comments - - All tests pass - - No new warnings introduced +## Background -### Remaining Work +### Current State -The following items from the original proposal are still pending: +In Terminal.Gui v2, the concept of a "runnable" view is embedded in the `Toplevel` class: -- [ ] Implement `IRunnable` non-generic base interface -- [ ] Implement `IRunnable` generic interface -- [ ] Create optional `Runnable` base class -- [ ] Replace `SessionToken` with `RunnableSessionToken` -- [ ] Replace `SessionStack` (ConcurrentStack) with `RunnableSessionStack` (ConcurrentStack) -- [ ] Add lifecycle events: `IsRunningChanging`, `IsRunningChanged`, `IsModalChanging`, `IsModalChanged` -- [ ] Migrate `Toplevel` to implement `IRunnable` -- [ ] Update all view classes to use new pattern -- [ ] Add comprehensive tests for new architecture -- [ ] Update all documentation +```csharp +public partial class Toplevel : View +{ + public bool Running { get; set; } + public bool Modal { get; set; } + public bool IsLoaded { get; private set; } + + // Lifecycle events + public event EventHandler? Activate; + public event EventHandler? Deactivate; + public event EventHandler? Loaded; + public event EventHandler? Ready; + public event EventHandler? Closing; + public event EventHandler? Closed; + public event EventHandler? Unloaded; +} +``` -## See Also +To run a view, it must derive from `Toplevel`: -- [Original Issue #4148](https://github.com/gui-cs/Terminal.Gui/issues/4148) -- [Application Lifecycle Documentation](application.md) -- [View Documentation](View.md) +```csharp +// Current pattern +var dialog = new Dialog(); // Dialog -> Window -> Toplevel +Application.Run(dialog); +``` + +`Toplevel` serves multiple purposes: + +1. **Session Management**: Manages the running session lifecycle +2. **Full-Screen Container**: By default sizes to fill the screen +3. **Overlapped Support**: Sets `Arrangement = ViewArrangement.Overlapped` +4. **Modal Support**: Has a `Modal` property + +This creates unnecessary coupling: + +- Only `Toplevel` derivatives can be run +- `Toplevel` always implies overlapped arrangement +- Modal behavior is a property, not a characteristic of the session +- The `SessionStack` contains `Toplevel` objects, coupling session management to the view hierarchy + + +## Problems with Current Design + +### 1. Tight Coupling + +**Problem**: Runnable behavior is hardcoded into `Toplevel`, creating artificial constraints. + +**Consequence**: +- Cannot run arbitrary `View` subclasses (e.g., a `FrameView` or custom `View`) +- Forces inheritance hierarchy: must derive from `Toplevel` even when full-screen/overlapped behavior isn't needed +- Code that manages sessions is scattered between `Application`, `ApplicationImpl`, `Toplevel`, and session management code + +**Example Limitation**: +```csharp +// Want to run a specialized view as a session +var customView = new MyCustomView(); +// Cannot do: Application.Run(customView); +// Must do: wrap in Toplevel or derive from Toplevel +``` + +### 2. Overlapped Coupling + +**Problem**: `Toplevel` constructor sets `Arrangement = ViewArrangement.Overlapped`, conflating "runnable" with "overlapped". + +**Consequence**: +- Cannot have a runnable tiled view without explicitly unsetting `Overlapped` +- Unclear separation between layout mode (overlapped vs. tiled) and execution mode (runnable) +- Architecture implies overlapped views must be runnable, which isn't necessarily true + +```csharp +// Toplevel constructor +public Toplevel () +{ + Arrangement = ViewArrangement.Overlapped; // Why is this hardcoded? + Width = Dim.Fill (); + Height = Dim.Fill (); +} +``` + +### 3. Modal as Property - Actually Not a Distinction + +**Problem**: `Modal` is a boolean property on `Toplevel` that creates an **artificial distinction**. + +**Reality Check**: All `Toplevel`s are effectively "modal" in that they: +1. Block in `Application.Run()` until `RequestStop()` is called +2. Have exclusive access to the run loop while running +3. Must complete before control returns to the caller + +**What `Modal = false` Actually Does:** +- Allows keyboard events to propagate to the SuperView +- Doesn't enforce Z-order "topmost" behavior +- That's it - it's just input routing, not a fundamental session characteristic + +**Evidence from codebase:** + +```csharp +// WizardAsView.cs - "Non-modal" is actually just an embedded View +var wizard = new Wizard { /* ... */ }; +wizard.Modal = false; // Just affects input propagation and border + +// NOTE: The wizard is NOT run separately! +topLevel.Add (wizard); // Added as a subview (embedded) +Application.Run (topLevel); // Only the topLevel is run + +// The distinction is artificial: +// - "Modal" Wizard = Application.Run(wizard) - BLOCKS until stopped +// - "Non-Modal" Wizard = topLevel.Add(wizard) - NOT runnable, just a View +// Both named "Wizard" but completely different usage patterns! +``` + +The confusion arises because **`Modal` is a property that affects behavior whether the Toplevel is runnable OR embedded**: +- If run with `Application.Run()`: controls input capture and Z-order +- If embedded with `superView.Add()`: still affects input propagation, but it's not a session + +**The real distinction**: +- **Runnable** (call `Application.Run(x)`) - Always blocks, has session lifecycle +- **Embedded** (call `superView.Add(x)`) - Just a view in the hierarchy, no session + +**Consequence**: +- Confusing semantics: "non-modal runnable" is an oxymoron +- Modal behavior is scattered across the codebase in conditional checks +- Session management has complex logic for Modal state transitions + +```csharp +// ApplicationImpl.Run.cs:98-101 - Complex conditional +if ((Current?.Modal == false && toplevel.Modal) + || (Current?.Modal == false && !toplevel.Modal) + || (Current?.Modal == true && toplevel.Modal)) +{ + // All this complexity for input routing! +} +``` + +**Better Model**: Remove the `Modal` property. If you want embedded Wizard-like behavior, just add it as a View (don't make it runnable). + +### 4. Session Management Complexity + +**Problem**: The `RunnableSessionStack` manages `Toplevel` instances, coupling session lifecycle to view hierarchy. + +**Consequence**: +- `SessionToken` stores a `Toplevel`, not a more abstract "runnable session" +- Complex logic for managing the relationship between `RunnableSessionStack`, `Current`, and `CachedSessionTokenToplevel` +- Unclear ownership: who owns the `Toplevel` lifecycle? + +```csharp +public class SessionToken : IDisposable +{ + public Toplevel? Toplevel { get; internal set; } // Tight coupling +} +``` + +### 5. Lifecycle Events Are Misnamed and Hacky + +**Problem**: Events like `Activate`, `Deactivate`, `Loaded`, `Ready`, `Closing`, `Closed`, `Unloaded` are on `Toplevel` but conceptually belong to the runnable session, not the view. + +**Consequence**: +- Events fire on the view object, mixing view lifecycle with session lifecycle +- Cannot easily monitor session changes independently of view state +- Event args reference `Toplevel` specifically +- **Hacky `ToplevelTransitionManager`**: The `Ready` event requires a separate manager class to track which toplevels have been "readied" across session transitions + +**Why is this hacky?** The `Ready` event is fired during the first `RunIteration()` (in the main loop), not during `Begin()` like other lifecycle events. This requires tracking state externally and checking every iteration. With proper CWP-aligned lifecycle, this complexity disappears - `Started` fires after `Begin()` completes, no tracking needed. + +### 6. Unclear Responsibilities + +**Problem**: It's unclear what `Toplevel` is responsible for. + +Is `Toplevel`: +- A full-screen container view? +- The base class for runnable views? +- The representation of a running session? +- A view with special overlapped arrangement? + +**Consequence**: Confused codebase where responsibilities blur. + +### 7. Violates Cancellable Work Pattern + +**Problem**: `Toplevel`'s lifecycle methods don't follow Terminal.Gui's **Cancellable Work Pattern** (CWP), which is used throughout the framework for `View.Draw`, `View.Keyboard`, `View.Command`, and property changes. + +**Consequence**: + +Current `Toplevel.OnClosing` implementation: +```csharp +internal virtual bool OnClosing(ToplevelClosingEventArgs ev) +{ + Closing?.Invoke(this, ev); // ? Event fired INSIDE virtual method + return ev.Cancel; +} +``` + +**What's wrong:** +1. **Wrong Order**: Event is raised inside the virtual method, not after +2. **No Pre-Check**: Virtual method doesn't return bool to cancel before event +3. **Inconsistent Naming**: Should be `OnStopping`/`Stopping` (cancellable) and `OnStopped`/`Stopped` (non-cancellable) +4. **Manual Checking**: `Application.RequestStop` manually checks `ev.Cancel` instead of relying on method return value + +**Impact**: +- Developers familiar with CWP from other Terminal.Gui components are confused by inconsistent patterns +- Cannot properly override lifecycle methods following the standard pattern +- Event subscription doesn't work as expected compared to other Terminal.Gui events +- Testing is harder because flow is non-standard + +### 8. View Initialization Doesn't Follow CWP + +**Problem**: `View.BeginInit/EndInit/Initialized` doesn't follow the Cancellable Work Pattern, creating inconsistency with the rest of Terminal.Gui. + +**What's Wrong:** +1. **No Pre-Notification Virtual**: No `OnInitializing()` virtual method before initialization +2. **No Cancellation**: Cannot cancel initialization +3. **Event After Work**: `Initialized` event fires after all work is done, no chance to participate +4. **Inconsistent with CWP**: Doesn't match the pattern used elsewhere in Terminal.Gui + +**Impact**: +- Inconsistent with rest of Terminal.Gui's event model +- Cannot hook into initialization at the right point in the lifecycle +- Subclasses cannot easily customize initialization behavior +- Makes the IRunnable lifecycle confusing since `Initialized` event doesn't follow CWP + +**Proposed Fix**: Add `Initializing` (cancellable) event and `OnInitializing`/`OnInitialized` virtual methods to match CWP pattern used throughout Terminal.Gui. + +## Proposed Architecture + +### Core Concept: Simplify and Clarify + +**Key Insight**: After analyzing the codebase, there's no valid use case for "non-modal runnables". Every `Toplevel` that calls `Application.Run()` blocks until `RequestStop()`. The `Modal` property only controls input routing, not the fundamental session behavior. + +**Simplified Model:** + +1. **`IRunnable`** - Interface for views that can run as **blocking** sessions with typed results +2. **`ViewArrangement.Overlapped`** - Layout mode for window management (orthogonal to runnability) +3. **Embedded Views** - Views that aren't runnable at all (e.g., `Wizard` with `Modal = false` is just a view) + +### Architecture Tenets + +1. **Interface-Based**: Use `IRunnable` interface to define runnable behavior, not inheritance +2. **Composition Over Inheritance**: Views can implement `IRunnable` without inheriting from `Toplevel` +3. **All Sessions Block**: `Application.Run()` blocks until `RequestStop()` is called (no background/non-blocking sessions) +4. **Type-Safe Results**: Generic `TResult` parameter provides compile-time type safety for return values +5. **Clean Separation**: View hierarchy (SuperView/SubViews) is independent of session hierarchy (RunnableSessionStack) +6. **Cancellable Work Pattern**: All lifecycle phases follow Terminal.Gui's Cancellable Work Pattern for consistency +7. **Result Extraction in `Stopping`**: `OnStopping()` is called before disposal, perfect for extracting `Result` + +### Result Extraction Pattern + +The `Result` property is a nullable generic (`public TResult? Result`) to represent the outcome of the runnable operation, allowing for rich result data and context. + +**Critical Timing**: `Result` must be extracted in `RaiseIsRunningChanging()` when the new value is `false`, which is called by `RequestStop()` before the run loop exits and before disposal. This ensures the data is captured while views are still accessible. + +```csharp +protected override bool RaiseIsRunningChanging () +{ + // Extract Result BEFORE disposal + // At this point views are still alive and accessible + Result = ExtractResultFromViews(); + + return base.OnStopping(); // Allow cancellation +} +``` + +--- + +## Detailed API Design + +### 1. `IRunnable` Non-Generic Base Interface + +The non-generic base interface provides common members for all runnables, enabling type-safe heterogeneous collections: + +```csharp +namespace Terminal.Gui.App; + +/// +/// Non-generic base interface for runnable views. Provides common members without type parameter. +/// +/// +/// +/// This interface enables storing heterogeneous runnables in collections (e.g., ) +/// while preserving type safety at usage sites via . +/// +/// +/// Most code should use directly. This base interface is primarily +/// for framework infrastructure (session management, stacking, etc.). +/// +/// +public interface IRunnable +{ + #region Running or not (added to/removed from RunnableSessionStack) + + // TODO: Determine if this should support set for testing purposes. + // TODO: If IApplication.RunnableSessionStack should be public/internal or wrapped. + /// + /// Gets whether this runnable session is currently running. + /// + bool IsRunning { get => App?.RunnableSessionStack.Contains(this); } + + /// Called when IsRunning is changing; raises IsRunningChanging. + /// True if the change was canceled; otherwise false. + bool RaiseIsRunningChanging(bool oldIsRunning, bool newIsRunning); + + /// + /// Raised when IsRunning is changing (e.g when or is called). + /// Can be canceled by setting to true. + /// + event EventHandler>? IsRunningChanging; + + /// Called after IsRunning has changed. + /// The new value of IsRunning (true = started, false = stopped). + void RaiseIsRunningChangedEvent(bool newIsRunning); + + /// + /// Raised after the session has started or stopped (IsRunning has changed). + /// + /// + /// Subscribe to perform post-state-change logic. When newValue is false (stopped), + /// this is the ideal place to extract before views are disposed. + /// + event EventHandler>? IsRunningChanged; + + + #endregion Running or not (added to/removed from RunnableSessionStack) + + #region Modal or not (top of RunnableSessionStack or not) + + // TODO: Determine if this should support set for testing purposes. + /// + /// Gets whether this runnable session is a the top of the Runnable Stack and thus + /// exclusively receiving mouse and keyboard input. + /// + bool IsModal { get => App?.TopRunnable == this; } + + /// Called when IsModal is changing; raises IsModalChanging. + /// True if the change was canceled; otherwise false. + bool RaiseIsModalChanging(bool oldIsModal, bool newIsModal); + + /// + /// Called when the user does something to cause this runnable to be put at the top + /// of the Runnable Stack or not. This is typically because `Run` was called or `RequestStop` + /// was called. + /// Can be canceled by setting to true. + /// + event EventHandler>? IsModalChanging; + + /// Called after IsModal has changed. + /// The new value of IsModal (true = became modal/top, false = no longer modal). + void RaiseIsModalChangedEvent(bool newIsModal); + + /// + /// Raised after the session has become modal (top of stack) or ceased being modal. + /// + /// + /// Subscribe to perform post-activation logic (e.g., setting focus, updating UI state). + /// + event EventHandler>? IsModalChanged; + + #endregion Modal or not (top of RunnableSessionStack or not) + +} +``` + +### 2. `IRunnable` Generic Interface + +The generic interface extends the base with typed result support: + +```csharp +namespace Terminal.Gui.App; + +/// +/// Defines a view that can be run as an independent blocking session with , +/// returning a typed result. +/// +/// +/// The type of result data returned when the session completes. +/// Common types: for button indices, for file paths, +/// custom types for complex form data. +/// +/// +/// +/// A runnable view executes as a self-contained blocking session with its own lifecycle, +/// event loop iteration, and focus management. blocks until +/// is called. +/// +/// +/// When is null, the session was stopped without being accepted +/// (e.g., ESC key pressed, window closed). When non-null, it contains the result data +/// extracted in before views are disposed. +/// +/// +/// Implementing does not require deriving from any specific +/// base class or using . These are orthogonal concerns. +/// +/// +/// This interface follows the Terminal.Gui Cancellable Work Pattern for all lifecycle events. +/// +/// +public interface IRunnable : IRunnable +{ + /// + /// Gets the result data extracted when the session was accepted, or null if not accepted. + /// + /// + /// + /// Implementations should set this in by extracting data from + /// views before they are disposed. + /// + /// + /// null indicates the session was stopped without accepting (ESC key, close without action). + /// Non-null contains the type-safe result data. + /// + /// + TResult? Result { get; set; } +} +``` + +**Design Rationale:** +- **Non-generic base**: Enables `RunnableSessionStack` to store `ConcurrentStack` without type erasure +- **Generic extension**: Preserves type safety at usage sites: `var dialog = new Dialog(); int? result = dialog.Result;` +- **Common lifecycle**: Both interfaces share the same lifecycle events via the base + +**Note**: The `Initialized` event is already defined on `View` via `ISupportInitializeNotification` and does not need to be redefined here. + +### Why This Model Works + +1. **Natural nesting**: Each `Run()` call creates a nested blocking context +2. **Automatic cleanup**: When a session ends, previous session automatically becomes modal again +3. **Z-order enforcement**: Topmost session (IsModal=true) is always visually on top +4. **Input capture**: Only `TopRunnable` (IsModal=true) receives keyboard/mouse input +5. **All sessions active**: All sessions on stack (IsRunning=true) continue to be laid out and drawn +6. **No race conditions**: Serial call stack eliminates concurrency issues + +### Code Example + +```csharp +public class MainWindow : Runnable +{ + private void OpenFile() + { + var fileDialog = new FileDialog(); + + // This blocks until fileDialog closes + Application.Run(fileDialog); + + // FileDialog has stopped, we're back here + if (fileDialog.Result is string path) + { + LoadFile(path); + } + + fileDialog.Dispose(); + + // MainWindow's Run() loop continues + } +} + +public class FileDialog : Runnable +{ + protected override bool OnIsRunningChanging(bool oldIsRunning, bool newIsRunning) + { + if (!newIsRunning) // Stopping + { + if (SelectedPath == null) + { + // Confirm cancellation with nested modal + int result = MessageBox.Query( + "Confirm", + "Cancel file selection?", + "Yes", "No"); + + if (result == 1) // No + { + return true; // Cancel stopping + } + } + + Result = SelectedPath; + } + + return base.OnIsRunningChanging(oldIsRunning, newIsRunning); + } +} +``` + +### RunnableSessionStack Implementation + +```csharp +public interface IApplication +{ + /// + /// Gets the stack of active runnable session tokens. + /// Sessions execute serially - the top of stack is the currently modal session. + /// + /// + /// + /// Session tokens are pushed onto the stack when is called and popped when + /// completes. The stack grows during nested modal calls and + /// shrinks as they complete. + /// + /// + /// Only the top session () has exclusive keyboard/mouse input (IsModal=true). + /// All other sessions on the stack continue to be laid out, drawn, and receive iteration events (IsRunning=true), + /// but they don't receive user input. + /// + /// + /// Stack during nested modals: + /// + /// RunnableSessionStack (top to bottom): + /// - MessageBox (TopRunnable, IsModal=true, IsRunning=true, has input) + /// - FileDialog (IsModal=false, IsRunning=true, continues to update/draw) + /// - MainWindow (IsModal=false, IsRunning=true, continues to update/draw) + /// + /// + /// + ConcurrentStack RunnableSessionStack { get; } + + /// + /// Gets or sets the topmost runnable session (the one capturing input). + /// + /// + /// + /// Always equals RunnableSessionStack.Peek().Runnable when stack is non-empty. + /// + /// + /// This is the runnable with = true. + /// + /// + IRunnable? TopRunnable { get; set; } +} +``` + +### Why Not Parallel Sessions? + +**Question**: Why not allow multiple non-modal sessions running in parallel (like tiled window managers)? + +**Answer**: This adds enormous complexity with little benefit: + +1. **Input routing**: Which session gets keyboard/mouse events? +2. **Focus management**: How does focus move between parallel sessions? +3. **Z-order**: How are overlapping sessions drawn? +4. **Coordination**: How do sessions communicate? +5. **Thread safety**: Concurrent access to Application state + +**Alternative**: Use embedded views with `ViewArrangement.Overlapped`: + +```csharp +// Instead of parallel runnables, use embedded overlapped windows +var mainView = new Runnable(); + +var window1 = new Window +{ + X = 0, + Y = 0, + Width = 40, + Height = 20, + Arrangement = ViewArrangement.Overlapped +}; + +var window2 = new Window +{ + X = 10, + Y = 5, + Width = 40, + Height = 20, + Arrangement = ViewArrangement.Overlapped +}; + +mainView.Add(window1); +mainView.Add(window2); + +// Only mainView is runnable, windows are embedded +Application.Run(mainView); +mainView.Dispose(); +``` + +**Benefits of serial-only model:** +- **Simplicity**: Clear execution flow +- **Predictability**: One active session at a time +- **Composability**: Overlapped windows via `ViewArrangement`, runnability via `IRunnable` +- **Testability**: Easier to test serial workflows + +### 2. `Runnable` Base Class (Complete Implementation) + +Provides a default implementation for convenience: + +```csharp +namespace Terminal.Gui.ViewBase; + +/// +/// Base implementation of for views that can be run as blocking sessions. +/// +/// The type of result data returned when the session completes. +/// +/// Views can derive from this class or implement directly. +/// +public class Runnable : View, IRunnable +{ + /// + public TResult? Result { get; set; } + + #region IRunnable Implementation - IsRunning (from base interface) + + /// + public bool RaiseIsRunningChanging(bool oldIsRunning, bool newIsRunning) + { + // Clear previous result when starting + if (newIsRunning) + { + Result = default; + } + + // CWP Phase 1: Virtual method (pre-notification) + if (OnIsRunningChanging(oldIsRunning, newIsRunning)) + { + return true; // Canceled + } + + // CWP Phase 2: Event notification + var args = new CancelEventArgs { CurrentValue = oldIsRunning, NewValue = newIsRunning }; + IsRunningChanging?.Invoke(this, args); + + return args.Cancel; + } + + /// + public event EventHandler>? IsRunningChanging; + + /// + public void RaiseIsRunningChangedEvent(bool newIsRunning) + { + // CWP Phase 3: Post-notification (work already done by Application.Begin/End) + OnIsRunningChanged(newIsRunning); + + var args = new EventArgs { CurrentValue = newIsRunning }; + IsRunningChanged?.Invoke(this, args); + } + + /// + public event EventHandler>? IsRunningChanged; + + /// + /// Called before event. Override to cancel state change or extract . + /// + /// The current value of IsRunning. + /// The new value of IsRunning (true = starting, false = stopping). + /// True to cancel; false to proceed. + /// + /// + /// Default implementation returns false (allow change). + /// + /// + /// IMPORTANT: When is false (stopping), this is the ideal place + /// to extract from views before the runnable is removed from the stack. + /// At this point, all views are still alive and accessible, and subscribers can inspect the result + /// and optionally cancel the stop. + /// + /// + /// + /// protected override bool OnIsRunningChanging(bool oldIsRunning, bool newIsRunning) + /// { + /// if (!newIsRunning) // Stopping + /// { + /// // Extract result before removal from stack + /// Result = _textField.Text; + /// + /// // Or check if user wants to save first + /// if (HasUnsavedChanges()) + /// { + /// var result = MessageBox.Query("Save?", "Save changes?", "Yes", "No", "Cancel"); + /// if (result == 2) return true; // Cancel stopping + /// if (result == 0) Save(); + /// } + /// } + /// + /// return base.OnIsRunningChanging(oldIsRunning, newIsRunning); + /// } + /// + /// + /// + protected virtual bool OnIsRunningChanging(bool oldIsRunning, bool newIsRunning) => false; + + /// + /// Called after has changed. Override for post-state-change logic. + /// + /// The new value of IsRunning (true = started, false = stopped). + /// + /// Default implementation does nothing. Overrides should call base to ensure extensibility. + /// + protected virtual void OnIsRunningChanged(bool newIsRunning) + { + // Default: no-op + } + + #endregion + + #region IRunnable Implementation - IsModal (from base interface) + + /// + public bool RaiseIsModalChanging(bool oldIsModal, bool newIsModal) + { + // CWP Phase 1: Virtual method (pre-notification) + if (OnIsModalChanging(oldIsModal, newIsModal)) + { + return true; // Canceled + } + + // CWP Phase 2: Event notification + var args = new CancelEventArgs { CurrentValue = oldIsModal, NewValue = newIsModal }; + IsModalChanging?.Invoke(this, args); + + return args.Cancel; + } + + /// + public event EventHandler>? IsModalChanging; + + /// + public void RaiseIsModalChangedEvent(bool newIsModal) + { + // CWP Phase 3: Post-notification (work already done by Application) + OnIsModalChanged(newIsModal); + + var args = new EventArgs { CurrentValue = newIsModal }; + IsModalChanged?.Invoke(this, args); + } + + /// + public event EventHandler>? IsModalChanged; + + /// + /// Called before event. Override to cancel activation/deactivation. + /// + /// The current value of IsModal. + /// The new value of IsModal (true = becoming modal/top, false = no longer modal). + /// True to cancel; false to proceed. + /// + /// Default implementation returns false (allow change). + /// + protected virtual bool OnIsModalChanging(bool oldIsModal, bool newIsModal) => false; + + /// + /// Called after has changed. Override for post-activation logic. + /// + /// The new value of IsModal (true = became modal, false = no longer modal). + /// + /// + /// Default implementation does nothing. Overrides should call base to ensure extensibility. + /// + /// + /// Common uses: setting focus when becoming modal, updating UI state. + /// + /// + protected virtual void OnIsModalChanged(bool newIsModal) + { + // Default: no-op + } + + #endregion + + /// + /// Requests that this runnable session stop. + /// + public virtual void RequestStop() + { + Application.RequestStop(this); + } +} +``` + +**Key Design Point**: `OnStopping()` is called **before** the run loop exits and **before** disposal, making it the perfect place to extract `Result` while views are still accessible. + +### 3. Event Args + +Terminal.Gui's existing event args types are used: + +- **`EventArgs`** - For non-cancellable events that need to pass data +- **`CancelEventArgs`** - For cancellable events that need to pass data +- **`CancelEventArgs`** - For cancellable events without additional data +- **`ResultEventArgs`** - For events that produce a result + +### 4. Updated `IApplication` Interface + +Modified methods to work with `IRunnable`: + +```csharp +namespace Terminal.Gui.App; + +public interface IApplication +{ + /// + /// Gets or sets the topmost runnable session (the one capturing keyboard/mouse input). + /// + /// + /// + /// null when no session is running. + /// + /// + /// This is the runnable with = true. + /// Always equals RunnableSessionStack.Peek().Runnable when stack is non-empty. + /// + /// + IRunnable? TopRunnable { get; set; } + + /// + /// Gets the stack of all runnable session tokens. + /// + /// + /// The top of the stack (Peek().Runnable) is the session (IsModal=true). + /// All sessions on the stack have IsRunning=true and continue to receive layout, draw, and iteration events. + /// + ConcurrentStack RunnableSessionStack { get; } + + /// + /// Prepares the provided runnable for execution and creates a session token. + /// + /// The runnable to begin executing. + /// A RunnableSessionToken that must be passed to when the session completes. + RunnableSessionToken Begin(IRunnable runnable); + + // Three forms of Run(): + + /// + /// Runs a new session with the provided runnable view. + /// + /// The runnable to execute. + /// Optional handler for unhandled exceptions. + void Run(IRunnable runnable, Func? errorHandler = null); + + /// + /// Creates and runs a new session with a runnable of the specified type. + /// + /// The type of runnable to create and run. Must have a parameterless constructor. + /// Optional handler for unhandled exceptions. + /// The runnable instance that was created and run. + /// + /// This is a convenience method that creates an instance of and runs it. + /// Equivalent to: var r = new TRunnable(); Run(r); return r; + /// + TRunnable Run(Func? errorHandler = null) where TRunnable : IRunnable, new(); + + /// + /// Creates and runs a default container runnable (e.g., or ). + /// + /// Optional handler for unhandled exceptions. + /// The default runnable that was created and run. + /// + /// + /// This is a convenience method for the common use case where the developer just wants a default + /// container view without specifying a type. It creates a instance + /// and runs it, allowing the developer to populate it via the event. + /// + /// + /// + /// var app = Application.Create(); + /// app.Init(); + /// + /// IRunnable? mainRunnable = null; + /// + /// // Listen for when the default runnable starts + /// app.IsRunningChanged += (s, e) => + /// { + /// if (e.CurrentValue && app.TopRunnable != null) + /// { + /// // Populate app.TopRunnable with views + /// app.TopRunnable.Add(new MenuBar { /* ... */ }); + /// app.TopRunnable.Add(new StatusBar { /* ... */ }); + /// // ... + /// } + /// }; + /// + /// mainRunnable = app.Run(); // Creates default Runnable{object} and runs it + /// app.Shutdown(); + /// + /// + /// + IRunnable Run(Func? errorHandler = null); + + /// + /// Requests that the specified runnable session stop. + /// + /// The runnable to stop. If null, stops . + void RequestStop(IRunnable? runnable = null); + + /// + /// Ends the session associated with the token. + /// + /// The token returned by . + void End(RunnableSessionToken sessionToken); +} +``` + +### 5. Updated `RunnableSessionToken` + +Wraps an `IRunnable` instance: + +```csharp +namespace Terminal.Gui.App; + +/// +/// Represents a running session created by . +/// Wraps an instance and is stored in . +/// +public class RunnableSessionToken : IDisposable +{ + internal RunnableSessionToken(IRunnable runnable) + { + Runnable = runnable; + } + + /// + /// Gets or sets the runnable associated with this session. + /// Set to null by when the session completes. + /// + public IRunnable? Runnable { get; internal set; } + + public void Dispose() + { + if (Runnable != null) + { + throw new InvalidOperationException( + "RunnableSessionToken.Dispose called but Runnable is not null. " + + "Call IApplication.End(sessionToken) before disposing."); + } + } +} +``` + +### 6. `ApplicationImpl.Run` Implementation + +Here's how the three forms of `Run()` work with `IRunnable`: + +```csharp +namespace Terminal.Gui.App; + +public partial class ApplicationImpl +{ + // Form 1: Run with provided runnable + public void Run(IRunnable runnable, Func? errorHandler = null) + { + if (runnable is null) + { + throw new ArgumentNullException(nameof(runnable)); + } + + if (!Initialized) + { + throw new NotInitializedException(nameof(Run)); + } + + // Begin the session (adds to stack, raises IsRunningChanging/IsRunningChanged) + RunnableSessionToken token = Begin(runnable); + + try + { + // All runnables block until RequestStop() is called + RunLoop(runnable, errorHandler); + } + finally + { + // End the session (raises IsRunningChanging/IsRunningChanged, pops from stack) + End(token); + } + } + + // Form 2: Run with type parameter (convenience) + public TRunnable Run(Func? errorHandler = null) + where TRunnable : IRunnable, new() + { + if (!Initialized) + { + throw new NotInitializedException(nameof(Run)); + } + + TRunnable runnable = new(); + Run(runnable, errorHandler); + return runnable; + } + + // Form 3: Run with default container (convenience) + public IRunnable Run(Func? errorHandler = null) + { + if (!Initialized) + { + throw new NotInitializedException(nameof(Run)); + } + + // Create a default container runnable + // Using Runnable as a generic container (result not meaningful) + var runnable = new Runnable + { + Width = Dim.Fill(), + Height = Dim.Fill() + }; + + Run(runnable, errorHandler); + return runnable; + } + + private void RunLoop(IRunnable runnable, Func? errorHandler) + { + // Main loop - blocks until RequestStop() is called + // Note: IsRunning is a derived property (stack.Contains), so we check it each iteration + while (runnable.IsRunning && !_isDisposed) + { + try + { + // Process one iteration of the event loop + Coordinator.RunIteration(); + } + catch (Exception ex) + { + if (errorHandler is null || !errorHandler(ex)) + { + throw; + } + } + } + } + + private RunnableSessionToken Begin(IRunnable runnable) + { + // Create token wrapping the runnable + var token = new RunnableSessionToken(runnable); + + // Raise IsRunningChanging (false -> true) - can be canceled + if (runnable.RaiseIsRunningChanging(false, true)) + { + // Starting was canceled + return token; // Don't add to stack + } + + // Push token onto Runnable Stack (IsRunning becomes true) + RunnableSessionStack.Push(token); + + // Update TopRunnable to the new top of stack + IRunnable? previousTop = TopRunnable; + TopRunnable = runnable; + + // Raise IsRunningChanged (now true) + runnable.RaiseIsRunningChangedEvent(true); + + // If there was a previous top, it's no longer modal + if (previousTop != null) + { + // Raise IsModalChanging (true -> false) + previousTop.RaiseIsModalChanging(true, false); + // IsModal is now false (derived property) + previousTop.RaiseIsModalChangedEvent(false); + } + + // New runnable becomes modal + // Raise IsModalChanging (false -> true) + runnable.RaiseIsModalChanging(false, true); + // IsModal is now true (derived property) + runnable.RaiseIsModalChangedEvent(true); + + // Initialize if needed + if (runnable is View view && !view.IsInitialized) + { + view.BeginInit(); + view.EndInit(); + // Initialized event is raised by View.EndInit() + } + + // Initial Layout and draw + LayoutAndDraw(true); + + // Set focus + if (runnable is View viewToFocus && !viewToFocus.HasFocus) + { + viewToFocus.SetFocus(); + } + + if (PositionCursor()) + { + Driver?.UpdateCursor(); + } + + return token; + } + + private void End(RunnableSessionToken token) + { + if (token.Runnable is null) + { + return; // Already ended + } + + IRunnable runnable = token.Runnable; + + // Raise IsRunningChanging (true -> false) - can be canceled + // This is where Result should be extracted! + if (runnable.RaiseIsRunningChanging(true, false)) + { + // Stopping was canceled + return; + } + + // Current runnable is no longer modal + // Raise IsModalChanging (true -> false) + runnable.RaiseIsModalChanging(true, false); + // IsModal is now false (will be false after pop) + runnable.RaiseIsModalChangedEvent(false); + + // Pop token from Runnable Stack (IsRunning becomes false) + if (RunnableSessionStack.TryPop(out RunnableSessionToken? popped) && popped == token) + { + // Restore previous top runnable + if (RunnableSessionStack.TryPeek(out RunnableSessionToken? previousToken)) + { + TopRunnable = previousToken.Runnable; + + // Previous runnable becomes modal again + if (TopRunnable != null) + { + // Raise IsModalChanging (false -> true) + TopRunnable.RaiseIsModalChanging(false, true); + // IsModal is now true (derived property) + TopRunnable.RaiseIsModalChangedEvent(true); + } + } + else + { + TopRunnable = null; + } + } + + // Raise IsRunningChanged (now false) + runnable.RaiseIsRunningChangedEvent(false); + + // Set focus to new TopRunnable if exists + if (TopRunnable is View viewToFocus && !viewToFocus.HasFocus) + { + viewToFocus.SetFocus(); + } + + // Clear the token + token.Runnable = null; + } + + public void RequestStop(IRunnable? runnable = null) + { + runnable ??= TopRunnable; + + if (runnable is null) + { + return; + } + + // Trigger the run loop to exit + // The End() method will be called from the finally block in Run() + // and that's where IsRunningChanging/IsRunningChanged will be raised + _stopRequested = runnable; + } +} + +``` + +### 8. Updated View Hierarchy + +```csharp +// Old hierarchy +View + ├─ Toplevel (runnable, overlapped, modal property) + │ ├─ Window (overlapped) + │ │ ├─ Dialog (modal, centered) + │ │ │ └─ MessageBox + │ │ └─ Wizard (modal, multi-step) + │ └─ (other Toplevel derivatives) + └─ (all other views) + +// New hierarchy +View + ├─ Runnable (implements IRunnable) + │ ├─ Window (can be run, overlapped by default) + │ ├─ Dialog (implements IModalRunnable, centered) + │ │ └─ MessageBox + │ └─ Wizard (implements IModalRunnable, multi-step) + └─ (all other views, can optionally implement IRunnable) +``` + +### 9. Usage Examples + +#### Three Forms of Run() + +**Form 1: Run with provided runnable** + +```csharp +Application app = Application.Create (); +app.Init (); + +Runnable myView = new () +{ + Width = Dim.Fill (), + Height = Dim.Fill () +}; +myView.Add (new MenuBar { /* ... */ }); +myView.Add (new StatusBar { /* ... */ }); + +app.Run (myView); // Run the specific runnable +myView.Dispose (); +app.Shutdown (); +``` + +**Form 2: Run with type parameter (generic convenience)** + +```csharp +Application app = Application.Create (); +app.Init (); + +Dialog dialog = app.Run (); // Creates and runs Dialog +// dialog.Result contains the result after it closes + +dialog.Dispose (); +app.Shutdown (); +``` + +**Form 3: Run with default container (parameterless convenience)** + +```csharp +Application app = Application.Create (); +app.Init (); + +// Subscribe to application-level event to populate the default runnable +app.IsRunningChanged += (s, e) => +{ + if (e.CurrentValue && app.TopRunnable != null) + { + // Populate app.TopRunnable with views when it starts + app.TopRunnable.Add (new MenuBar { /* ... */ }); + app.TopRunnable.Add (new Label + { + Text = "Hello World!", + X = Pos.Center (), + Y = Pos.Center () + }); + app.TopRunnable.Add (new StatusBar { /* ... */ }); + } +}; + +IRunnable mainRunnable = app.Run (); // Creates default Runnable and runs it +mainRunnable.Dispose (); + +app.Shutdown (); +``` + +**Why three forms?** +- **Form 1**: Most control, you create and configure the runnable +- **Form 2**: Convenience for creating typed runnables with default constructors +- **Form 3**: Simplest for quick apps, populate via `Starting` event + +#### Using IsRunningChanged Event + +```csharp +Runnable runnable = new (); + +// Listen for when it starts running +runnable.IsRunningChanged += (s, e) => +{ + if (e.CurrentValue) // Started running + { + // View is on the stack, initialized, and laid out + // Safe to perform post-start logic + SetupDataBindings (); + LoadInitialData (); + } + else // Stopped running + { + // Clean up resources + SaveState (); + } +}; + +app.Run (runnable); +runnable.Dispose (); +``` + +#### Override Pattern (Canceling Stop with Cleanup and Result Extraction) + +```csharp +public class MyRunnable : Runnable +{ + private TextField? _textField; + + protected override bool OnIsRunningChanging(bool oldIsRunning, bool newIsRunning) + { + if (!newIsRunning) // Stopping + { + // Extract Result BEFORE being removed from stack + if (HasUnsavedChanges ()) + { + var result = MessageBox.Query ("Unsaved Changes", + "Save before closing?", "Yes", "No", "Cancel"); + + if (result == 2) // Cancel + { + return true; // Cancel stopping + } + else if (result == 0) // Yes + { + SaveChanges (); + Result = _textField?.Text; // Extract result + } + else // No + { + Result = null; // Explicitly null (canceled) + } + } + else + { + Result = _textField?.Text; // Extract result + } + } + else // Starting + { + // Clear previous result + Result = default; + + // Can prevent starting if needed + if (!CanStart ()) + { + return true; // Cancel starting + } + } + + return base.OnIsRunningChanging (oldIsRunning, newIsRunning); + } + + protected override void OnIsRunningChanged (bool newIsRunning) + { + if (newIsRunning) // Started + { + // Post-start initialization + SetFocus (); + StartBackgroundWork (); + } + else // Stopped + { + // Cleanup after successful stop + DisconnectFromServer (); + SaveState (); + } + + base.OnIsRunningChanged (newIsRunning); + } + + protected override void OnIsModalChanged (bool newIsModal) + { + if (newIsModal) // Became modal (top of stack) + { + // Set focus, update UI for being active + UpdateTitle ("Active"); + } + else // No longer modal (another runnable on top) + { + // Dim UI, show as inactive + UpdateTitle ("Inactive"); + } + + base.OnIsModalChanged (newIsModal); + } +} +``` + +#### Modal Dialog with Automatic Result Capture + +`Dialog` implements `IRunnable` and overrides `OnIsRunningChanging` to extract result before disposal: + +```csharp +public class Dialog : Runnable +{ + private Button[]? _buttons; + + protected override bool OnIsRunningChanging(bool oldIsRunning, bool newIsRunning) + { + if (!newIsRunning) // Stopping + { + // Extract Result BEFORE views are disposed + // Find which button was clicked + Result = _buttons?.Select((b, i) => (button: b, index: i)) + .FirstOrDefault(x => x.button.HasFocus) + .index ?? -1; + } + + return base.OnIsRunningChanging(oldIsRunning, newIsRunning); + } +} + +// Usage +Dialog dialog = new (); +Application.Run (dialog); + +// Type-safe result - no casting needed +var result = dialog.Result ?? -1; + +// Pattern matching +if (dialog.Result is int buttonIndex) +{ + switch (buttonIndex) + { + case 0: + // First button clicked + break; + case 1: + // Second button clicked + break; + case -1: + // Canceled (ESC, closed) + break; + } +} +dialog.Dispose (); +``` + +This works seamlessly with buttons calling `Application.RequestStop()` in their handlers. + +#### MessageBox Example - Type-Safe and Simple + +With `Dialog` implementing `IRunnable`, MessageBox is beautifully simple: + +```csharp +// MessageBox.Query implementation (simplified) +private static int QueryFull (string title, string message, params string [] buttons) +{ + using Dialog d = new () { Title = title, Text = message }; + + // Create buttons with handlers that call RequestStop + for (var i = 0; i < buttons.Length; i++) + { + var buttonIndex = i; // Capture for closure + d.AddButton (new Button + { + Text = buttons [i], + IsDefault = (i == 0), // First button is default + Accept = (s, e) => + { + // Store which button was clicked + d.Result = buttonIndex; + Application.RequestStop (); + } + }); + } + + // Run modal - blocks until RequestStop() + Application.Run (d); + + // Type-safe result - no casting needed! + return d.Result ?? -1; // null = canceled (ESC pressed, etc.) +} +``` + +**Pattern**: Buttons set `Result` in their handlers, then call `RequestStop()`. The `OnIsRunningChanging` override can extract additional data if needed. + +#### OptionSelector Example - Type-Safe Pattern + +Custom dialog that returns a typed enum: + +```csharp +// Custom dialog that returns an Alignment enum +public class AlignmentDialog : Runnable +{ + private RadioGroup? _selector; + + public AlignmentDialog () + { + Title = "Choose Alignment"; + + _selector = new () + { + RadioLabels = new [] { "Start", "Center", "End" } + }; + + Add (_selector); + + Button okButton = new () { Text = "OK", IsDefault = true }; + okButton.Accept += (s, e) => + { + Application.RequestStop (); + }; + AddButton (okButton); + } + + protected override bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning) + { + if (!newIsRunning) // Stopping + { + // Extract the selected value BEFORE disposal + Result = _selector?.SelectedItem switch + { + 0 => Alignment.Start, + 1 => Alignment.Center, + 2 => Alignment.End, + _ => (Alignment?)null + }; + } + + return base.OnIsRunningChanging (oldIsRunning, newIsRunning); + } +} + +// Usage - type-safe! +AlignmentDialog dialog = new (); +Application.Run (dialog); + +if (dialog.Result is Alignment alignment) +{ + ApplyAlignment (alignment); // No casting needed! +} +dialog.Dispose (); +``` + +#### FileDialog Example + +```csharp +public class FileDialog : Runnable +{ + private TextField? _pathField; + + public FileDialog () + { + Title = "Open File"; + + _pathField = new () { Width = Dim.Fill () }; + Add (_pathField); + + Button okButton = new () { Text = "OK", IsDefault = true }; + okButton.Accept += (s, e) => + { + Application.RequestStop (); + }; + + AddButton (okButton); + } + + protected override bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning) + { + if (!newIsRunning) // Stopping + { + // Extract result BEFORE disposal + Result = _pathField?.Text; + } + + return base.OnIsRunningChanging (oldIsRunning, newIsRunning); + } +} + +// Usage - type-safe! +FileDialog fileDialog = new (); +Application.Run (fileDialog); + +if (fileDialog.Result is { } path) +{ + OpenFile (path); // string, no cast needed! +} +fileDialog.Dispose (); +``` + +#### Key Benefits + +1. **Zero boilerplate** - No manual Accepting handlers in MessageBox +2. **Fully type-safe** - No casting, compile-time type checking +3. **Natural C# idioms** - `null` = not accepted, pattern matching for accepted +4. **Safe disposal** - Data extracted before views are disposed +5. **Extensible** - Works with any type: `int`, `string`, enums, custom objects +6. **Clean separation** - Dialog captures data, controls handle their own Accept logic +7. **Consistent** - All lifecycle events follow Terminal.Gui's Cancellable Work Pattern + +--- + +## Migration Path + +### Phase 0: Rename `Current` to `TopRunnable` **DONE** + +- Issue #4148 +- This is a minor rename for clarity. Can be done after Phase 1 is complete. +- Rename `IApplication.Current` → `IApplication.TopRunnable` +- Update `View.IsCurrentTop` → `View.IsTopRunnable` + +### Phase 1: Add IRunnable Support + +- Issue #4400 + +1. Add `IRunnable` (non-generic) interface alongside existing `Toplevel` +2. Add `IRunnable` (generic) interface +3. Add `Runnable` base class +4. Add `RunnableSessionToken` class +5. Update `IApplication.RunnableSessionStack` to hold `RunnableSessionToken` instead of `Toplevel` +6. Update `IApplication` to support both `Toplevel` and `IRunnable` +7. Implement CWP-based `IsRunningChanging`/`IsRunningChanged` events +8. Implement CWP-based `IsModalChanging`/`IsModalChanged` events +9. Update `Begin()`, `End()`, `RequestStop()` to raise these events +10. Add three `Run()` overloads: `Run(IRunnable)`, `Run()`, `Run()` + +### Phase 2: Migrate Existing Views + +- Issue (TBD) + +1. Make `Toplevel` implement `IRunnable` (adapter pattern for compatibility) +2. Update `Dialog` to inherit from `Runnable` instead of `Window` +3. Update `MessageBox` to use `Dialog.Result` +4. Update `Wizard` to inherit from `Runnable` +5. Update all examples to use new `IRunnable` pattern + +### Phase 3: Deprecate and Remove Toplevel + +- Issue (TBD) + +1. Mark `Toplevel` as `[Obsolete]` +2. Update all internal code to use `IRunnable`/`Runnable` +3. Remove `Toplevel` class entirely (breaking change for v3) + +### Phase 4: Upgrade View Initialization (Optional Enhancement) + +- Issue (TBD) + +1. Refactor `View.BeginInit()`/`View.EndInit()`/`Initialized` to be CWP compliant +2. This is independent of the runnable architecture but would improve consistency From 171a26a350fe73488b7af22e68732cd3af24f98d Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 21 Nov 2025 06:51:56 -0700 Subject: [PATCH 09/16] Removes the v1 `Menu` stuff. Preps for #4148 (#4402) --- Examples/UICatalog/Resources/config.json | 4 +- Examples/UICatalog/Scenario.cs | 2 + Examples/UICatalog/Scenarios/Bars.cs | 6 +- .../Scenarios/CharacterMap/CharacterMap.cs | 10 +- Examples/UICatalog/Scenarios/ClassExplorer.cs | 392 +- .../Scenarios/CollectionNavigatorTester.cs | 277 +- Examples/UICatalog/Scenarios/ContextMenus.cs | 24 +- Examples/UICatalog/Scenarios/CsvEditor.cs | 451 ++- .../UICatalog/Scenarios/DynamicMenuBar.cs | 1413 ------- Examples/UICatalog/Scenarios/Editor.cs | 1609 ++++---- .../UICatalog/Scenarios/GraphViewExample.cs | 632 ++- Examples/UICatalog/Scenarios/HexEditor.cs | 10 +- .../UICatalog/Scenarios/InteractiveTree.cs | 112 +- Examples/UICatalog/Scenarios/ListColumns.cs | 575 +-- Examples/UICatalog/Scenarios/Localization.cs | 165 +- .../UICatalog/Scenarios/MenuBarScenario.cs | 135 - Examples/UICatalog/Scenarios/Menus.cs | 6 +- .../UICatalog/Scenarios/MultiColouredTable.cs | 145 +- Examples/UICatalog/Scenarios/Notepad.cs | 245 +- .../Scenarios/RuneWidthGreaterThanOne.cs | 253 +- .../Scenarios/SingleBackgroundWorker.cs | 234 +- .../UICatalog/Scenarios/SyntaxHighlighting.cs | 114 +- .../UICatalog/Scenarios/TabViewExample.cs | 253 +- Examples/UICatalog/Scenarios/TableEditor.cs | 762 ++-- .../Scenarios/TextViewAutocompletePopup.cs | 285 +- Examples/UICatalog/Scenarios/TreeUseCases.cs | 197 +- .../UICatalog/Scenarios/TreeViewFileSystem.cs | 587 +-- Examples/UICatalog/Scenarios/Unicode.cs | 188 +- Examples/UICatalog/Scenarios/WizardAsView.cs | 136 +- Examples/UICatalog/UICatalogTop.cs | 28 +- Scripts/Run-LocalCoverage.ps1 | 3 +- Terminal.Gui/App/ApplicationImpl.Run.cs | 14 +- Terminal.Gui/App/IApplication.cs | 2 + .../App/MainLoop/ApplicationMainLoop.cs | 5 + .../Toplevel/IToplevelTransitionManager.cs | 6 + .../App/Toplevel/ToplevelTransitionManager.cs | 5 + Terminal.Gui/Resources/config.json | 6 +- Terminal.Gui/Text/TextFormatter.cs | 2 +- Terminal.Gui/ViewBase/View.Layout.cs | 43 +- Terminal.Gui/Views/FileDialogs/FileDialog.cs | 6 + Terminal.Gui/Views/Line.cs | 5 +- .../Views/Menu/{Menuv2.cs => Menu.cs} | 28 +- .../Views/Menu/{MenuBarv2.cs => MenuBar.cs} | 102 +- .../Menu/{MenuBarItemv2.cs => MenuBarItem.cs} | 24 +- .../Views/Menu/{MenuItemv2.cs => MenuItem.cs} | 24 +- Terminal.Gui/Views/Menu/PopoverMenu.cs | 90 +- Terminal.Gui/Views/Menuv1/Menu.cs | 1009 ----- Terminal.Gui/Views/Menuv1/MenuBar.cs | 1854 --------- Terminal.Gui/Views/Menuv1/MenuBarItem.cs | 265 -- .../Views/Menuv1/MenuClosingEventArgs.cs | 34 - Terminal.Gui/Views/Menuv1/MenuItem.cs | 387 -- .../Views/Menuv1/MenuItemCheckStyle.cs | 16 - .../Views/Menuv1/MenuOpenedEventArgs.cs | 22 - .../Views/Menuv1/MenuOpeningEventArgs.cs | 27 - Terminal.Gui/Views/TextInput/TextField.cs | 2 +- Terminal.Gui/Views/TextInput/TextView.cs | 14 +- Terminal.Gui/Views/Toplevel.cs | 12 +- .../{MenuBarv2Tests.cs => MenuBarvTests.cs} | 134 +- .../FluentTests/PopverMenuTests.cs | 14 +- .../UnitTests/Application/ApplicationTests.cs | 2 +- Tests/UnitTests/View/SubviewTests.cs | 156 +- Tests/UnitTests/Views/MenuBarTests.cs | 154 +- .../UnitTests/Views/Menuv1/MenuBarv1Tests.cs | 3410 ----------------- Tests/UnitTests/Views/Menuv1/Menuv1Tests.cs | 111 - Tests/UnitTests/Views/ToplevelTests.cs | 214 +- Tests/UnitTests/Views/WindowTests.cs | 111 +- .../UnitTestsParallelizable/Views/BarTests.cs | 2 +- .../Views/MenuBarItemTests.cs | 4 +- .../Views/MenuItemTests.cs | 14 - .../Views/MenuTests.cs | 2 +- docfx/docs/Popovers.md | 2 +- docfx/docs/cancellable-work-pattern.md | 2 +- docfx/docs/command.md | 112 +- docfx/docs/events.md | 6 +- docfx/docs/newinv2.md | 4 +- docfx/schemas/tui-config-schema.json | 8 +- 76 files changed, 4614 insertions(+), 13105 deletions(-) delete mode 100644 Examples/UICatalog/Scenarios/DynamicMenuBar.cs delete mode 100644 Examples/UICatalog/Scenarios/MenuBarScenario.cs rename Terminal.Gui/Views/Menu/{Menuv2.cs => Menu.cs} (90%) rename Terminal.Gui/Views/Menu/{MenuBarv2.cs => MenuBar.cs} (88%) rename Terminal.Gui/Views/Menu/{MenuBarItemv2.cs => MenuBarItem.cs} (86%) rename Terminal.Gui/Views/Menu/{MenuItemv2.cs => MenuItem.cs} (90%) delete mode 100644 Terminal.Gui/Views/Menuv1/Menu.cs delete mode 100644 Terminal.Gui/Views/Menuv1/MenuBar.cs delete mode 100644 Terminal.Gui/Views/Menuv1/MenuBarItem.cs delete mode 100644 Terminal.Gui/Views/Menuv1/MenuClosingEventArgs.cs delete mode 100644 Terminal.Gui/Views/Menuv1/MenuItem.cs delete mode 100644 Terminal.Gui/Views/Menuv1/MenuItemCheckStyle.cs delete mode 100644 Terminal.Gui/Views/Menuv1/MenuOpenedEventArgs.cs delete mode 100644 Terminal.Gui/Views/Menuv1/MenuOpeningEventArgs.cs rename Tests/IntegrationTests/FluentTests/{MenuBarv2Tests.cs => MenuBarvTests.cs} (89%) delete mode 100644 Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs delete mode 100644 Tests/UnitTests/Views/Menuv1/Menuv1Tests.cs delete mode 100644 Tests/UnitTestsParallelizable/Views/MenuItemTests.cs diff --git a/Examples/UICatalog/Resources/config.json b/Examples/UICatalog/Resources/config.json index e47ea567c..17d6edcf5 100644 --- a/Examples/UICatalog/Resources/config.json +++ b/Examples/UICatalog/Resources/config.json @@ -149,8 +149,8 @@ "FrameView.DefaultBorderStyle": "Double", "MessageBox.DefaultMinimumHeight": 0, "Button.DefaultHighlightStates": "In, Pressed", - "Menuv2.DefaultBorderStyle": "Heavy", - "MenuBarv2.DefaultBorderStyle": "Heavy", + "Menu.DefaultBorderStyle": "Heavy", + "MenuBar.DefaultBorderStyle": "Heavy", "Schemes": [ { "UI Catalog Scheme": { diff --git a/Examples/UICatalog/Scenario.cs b/Examples/UICatalog/Scenario.cs index 0fa13e6db..4d9f0c759 100644 --- a/Examples/UICatalog/Scenario.cs +++ b/Examples/UICatalog/Scenario.cs @@ -219,6 +219,8 @@ public class Scenario : IDisposable } } + // BUGBUG: This is incompatible with modals. This should be using the new equivalent of Toplevel.Ready + // BUGBUG: which will be IsRunningChanged with newIsRunning == true private void OnApplicationSessionBegun (object? sender, SessionTokenEventArgs e) { SubscribeAllSubViews (Application.TopRunnable!); diff --git a/Examples/UICatalog/Scenarios/Bars.cs b/Examples/UICatalog/Scenarios/Bars.cs index 730ddf969..669f4e9c0 100644 --- a/Examples/UICatalog/Scenarios/Bars.cs +++ b/Examples/UICatalog/Scenarios/Bars.cs @@ -80,7 +80,7 @@ public class Bars : Scenario }; menuBarLikeExamples.Add (label); - //bar = new MenuBarv2 + //bar = new MenuBar //{ // Id = "menuBar", // X = Pos.Right (label), @@ -128,7 +128,7 @@ public class Bars : Scenario }; menuLikeExamples.Add (label); - bar = new Menuv2 + bar = new Menu { Id = "menu", X = Pos.Left (label), @@ -147,7 +147,7 @@ public class Bars : Scenario }; menuLikeExamples.Add (label); - Menuv2 popOverMenu = new Menuv2 + Menu popOverMenu = new Menu { Id = "popupMenu", X = Pos.Left (label), diff --git a/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs b/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs index 3028eb4ee..fba9130e3 100644 --- a/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs +++ b/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs @@ -176,13 +176,13 @@ public class CharacterMap : Scenario top.Add (_categoryList); - var menu = new MenuBarv2 + var menu = new MenuBar { Menus = [ new ( "_File", - new MenuItemv2 [] + new MenuItem [] { new ( "_Quit", @@ -337,14 +337,14 @@ public class CharacterMap : Scenario ); } - private MenuItemv2 CreateMenuShowWidth () + private MenuItem CreateMenuShowWidth () { CheckBox cb = new () { Title = "_Show Glyph Width", CheckedState = _charMap!.ShowGlyphWidths ? CheckState.Checked : CheckState.None }; - var item = new MenuItemv2 { CommandView = cb }; + var item = new MenuItem { CommandView = cb }; item.Action += () => { @@ -357,7 +357,7 @@ public class CharacterMap : Scenario return item; } - private MenuItemv2 CreateMenuUnicodeCategorySelector () + private MenuItem CreateMenuUnicodeCategorySelector () { // First option is "All" (no filter), followed by all UnicodeCategory names string [] allCategoryNames = Enum.GetNames (); diff --git a/Examples/UICatalog/Scenarios/ClassExplorer.cs b/Examples/UICatalog/Scenarios/ClassExplorer.cs index efcb0ceeb..14c87e387 100644 --- a/Examples/UICatalog/Scenarios/ClassExplorer.cs +++ b/Examples/UICatalog/Scenarios/ClassExplorer.cs @@ -1,6 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; +#nullable enable + using System.Reflection; using System.Text; @@ -11,63 +10,45 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("TreeView")] public class ClassExplorer : Scenario { - private MenuItem _highlightModelTextOnly; - private MenuItem _miShowPrivate; - private TextView _textView; - private TreeView _treeView; + private CheckBox? _highlightModelTextOnlyCheckBox; + private CheckBox? _showPrivateCheckBox; + private TextView? _textView; + private TreeView? _treeView; public override void Main () { Application.Init (); - var top = new Toplevel (); - var menu = new MenuBar - { - Menus = - [ - new MenuBarItem ("_File", new MenuItem [] { new ("_Quit", "", () => Quit ()) }), - new MenuBarItem ( - "_View", - new [] - { - _miShowPrivate = - new MenuItem ( - "_Include Private", - "", - () => ShowPrivate () - ) { Checked = false, CheckType = MenuItemCheckStyle.Checked }, - new ("_Expand All", "", () => _treeView.ExpandAll ()), - new ("_Collapse All", "", () => _treeView.CollapseAll ()) - } - ), - new MenuBarItem ( - "_Style", - new [] - { - _highlightModelTextOnly = new MenuItem ( - "_Highlight Model Text Only", - "", - () => OnCheckHighlightModelTextOnly () - ) { CheckType = MenuItemCheckStyle.Checked } - } - ) - ] - }; - top.Add (menu); - - var win = new Window + Window win = new () { Title = GetName (), - Y = Pos.Bottom (menu) + BorderStyle = LineStyle.None }; - _treeView = new TreeView { X = 0, Y = 1, Width = Dim.Percent (50), Height = Dim.Fill () }; + // MenuBar + MenuBar menuBar = new (); - var lblSearch = new Label { Text = "Search" }; - var tfSearch = new TextField { Width = 20, X = Pos.Right (lblSearch) }; + // Search controls + Label lblSearch = new () + { + Y = Pos.Bottom (menuBar), + Title = "Search:" + }; - win.Add (lblSearch); - win.Add (tfSearch); + TextField tfSearch = new () + { + Y = Pos.Top (lblSearch), + X = Pos.Right (lblSearch) + 1, + Width = 20 + }; + + // TreeView + _treeView = new () + { + Y = Pos.Bottom (lblSearch), + Width = Dim.Percent (50), + Height = Dim.Fill () + }; TreeViewTextFilter filter = new (_treeView); _treeView.Filter = filter; @@ -76,7 +57,7 @@ public class ClassExplorer : Scenario { filter.Text = tfSearch.Text; - if (_treeView.SelectedObject != null) + if (_treeView.SelectedObject is { }) { _treeView.EnsureVisible (_treeView.SelectedObject); } @@ -87,111 +68,146 @@ public class ClassExplorer : Scenario _treeView.TreeBuilder = new DelegateTreeBuilder (ChildGetter, CanExpand); _treeView.SelectionChanged += TreeView_SelectionChanged; - win.Add (_treeView); + // TextView for details + _textView = new () + { + X = Pos.Right (_treeView), + Y = Pos.Top (_treeView), + Width = Dim.Fill (), + Height = Dim.Fill (), + ReadOnly = true, + }; - _textView = new TextView { X = Pos.Right (_treeView), Y = 0, Width = Dim.Fill (), Height = Dim.Fill () }; + // Menu setup + _showPrivateCheckBox = new () + { + Title = "_Include Private" + }; + _showPrivateCheckBox.CheckedStateChanged += (s, e) => ShowPrivate (); - win.Add (_textView); + _highlightModelTextOnlyCheckBox = new () + { + Title = "_Highlight Model Text Only" + }; + _highlightModelTextOnlyCheckBox.CheckedStateChanged += (s, e) => OnCheckHighlightModelTextOnly (); - top.Add (win); + menuBar.Add ( + new MenuBarItem ( + "_File", + [ + new MenuItem + { + Title = "_Quit", + Action = Quit + } + ] + ) + ); - Application.Run (top); - top.Dispose (); + menuBar.Add ( + new MenuBarItem ( + "_View", + [ + new MenuItem + { + CommandView = _showPrivateCheckBox + }, + new MenuItem + { + Title = "_Expand All", + Action = () => _treeView?.ExpandAll () + }, + new MenuItem + { + Title = "_Collapse All", + Action = () => _treeView?.CollapseAll () + } + ] + ) + ); + + menuBar.Add ( + new MenuBarItem ( + "_Style", + [ + new MenuItem + { + CommandView = _highlightModelTextOnlyCheckBox + } + ] + ) + ); + + // Add views in order of visual appearance + win.Add (menuBar, lblSearch, tfSearch, _treeView, _textView); + + Application.Run (win); + win.Dispose (); Application.Shutdown (); } - private bool CanExpand (object arg) { return arg is Assembly || arg is Type || arg is ShowForType; } + private bool CanExpand (object arg) => arg is Assembly or Type or ShowForType; private IEnumerable ChildGetter (object arg) { try { - if (arg is Assembly a) - { - return a.GetTypes (); - } - - if (arg is Type t) - { - // Note that here we cannot simply return the enum values as the same object cannot appear under multiple branches - return Enum.GetValues (typeof (Showable)) - .Cast () - - // Although we new the Type every time the delegate is called state is preserved because the class has appropriate equality members - .Select (v => new ShowForType (v, t)); - } - - if (arg is ShowForType show) - { - switch (show.ToShow) - { - case Showable.Properties: - return show.Type.GetProperties (GetFlags ()); - case Showable.Constructors: - return show.Type.GetConstructors (GetFlags ()); - case Showable.Events: - return show.Type.GetEvents (GetFlags ()); - case Showable.Fields: - return show.Type.GetFields (GetFlags ()); - case Showable.Methods: - return show.Type.GetMethods (GetFlags ()); - } - } + return arg switch + { + Assembly assembly => assembly.GetTypes (), + Type type => Enum.GetValues (typeof (Showable)) + .Cast () + .Select (v => new ShowForType (v, type)), + ShowForType show => show.ToShow switch + { + Showable.Properties => show.Type.GetProperties (GetFlags ()), + Showable.Constructors => show.Type.GetConstructors (GetFlags ()), + Showable.Events => show.Type.GetEvents (GetFlags ()), + Showable.Fields => show.Type.GetFields (GetFlags ()), + Showable.Methods => show.Type.GetMethods (GetFlags ()), + _ => Enumerable.Empty () + }, + _ => Enumerable.Empty () + }; } catch (Exception) { return Enumerable.Empty (); } - - return Enumerable.Empty (); } - private BindingFlags GetFlags () - { - if (_miShowPrivate.Checked == true) - { - return BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; - } - - return BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public; - } + private BindingFlags GetFlags () => + _showPrivateCheckBox?.CheckedState == CheckState.Checked + ? BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic + : BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public; private string GetRepresentation (object model) { try { - if (model is Assembly ass) - { - return ass.GetName ().Name; - } - - if (model is PropertyInfo p) - { - return p.Name; - } - - if (model is FieldInfo f) - { - return f.Name; - } - - if (model is EventInfo ei) - { - return ei.Name; - } + return model switch + { + Assembly assembly => assembly.GetName ().Name ?? string.Empty, + PropertyInfo propertyInfo => propertyInfo.Name, + FieldInfo fieldInfo => fieldInfo.Name, + EventInfo eventInfo => eventInfo.Name, + _ => model.ToString () ?? string.Empty + }; } catch (Exception ex) { return ex.Message; } - - return model.ToString (); } private void OnCheckHighlightModelTextOnly () { - _treeView.Style.HighlightModelTextOnly = !_treeView.Style.HighlightModelTextOnly; - _highlightModelTextOnly.Checked = _treeView.Style.HighlightModelTextOnly; + if (_treeView is null) + { + return; + } + + _treeView.Style.HighlightModelTextOnly = _highlightModelTextOnlyCheckBox?.CheckedState == CheckState.Checked; _treeView.SetNeedsDraw (); } @@ -199,17 +215,21 @@ public class ClassExplorer : Scenario private void ShowPrivate () { - _miShowPrivate.Checked = !_miShowPrivate.Checked; - _treeView.RebuildTree (); - _treeView.SetFocus (); + _treeView?.RebuildTree (); + _treeView?.SetFocus (); } - private void TreeView_SelectionChanged (object sender, SelectionChangedEventArgs e) + private void TreeView_SelectionChanged (object? sender, SelectionChangedEventArgs e) { - object val = e.NewValue; + if (_treeView is null || _textView is null) + { + return; + } + + object? val = e.NewValue; object [] all = _treeView.GetAllSelectedObjects ().ToArray (); - if (val == null || val is ShowForType) + if (val is null or ShowForType) { return; } @@ -218,69 +238,73 @@ public class ClassExplorer : Scenario { if (all.Length > 1) { - _textView.Text = all.Length + " Objects"; + _textView.Text = $"{all.Length} Objects"; } else { - var sb = new StringBuilder (); + StringBuilder sb = new (); - // tell the user about the currently selected tree node - sb.AppendLine (e.NewValue.GetType ().Name); + sb.AppendLine (e.NewValue?.GetType ().Name ?? string.Empty); - if (val is Assembly ass) + switch (val) { - sb.AppendLine ($"Location:{ass.Location}"); - sb.AppendLine ($"FullName:{ass.FullName}"); - } + case Assembly assembly: + sb.AppendLine ($"Location:{assembly.Location}"); + sb.AppendLine ($"FullName:{assembly.FullName}"); - if (val is PropertyInfo p) - { - sb.AppendLine ($"Name:{p.Name}"); - sb.AppendLine ($"Type:{p.PropertyType}"); - sb.AppendLine ($"CanWrite:{p.CanWrite}"); - sb.AppendLine ($"CanRead:{p.CanRead}"); - } + break; - if (val is FieldInfo f) - { - sb.AppendLine ($"Name:{f.Name}"); - sb.AppendLine ($"Type:{f.FieldType}"); - } + case PropertyInfo propertyInfo: + sb.AppendLine ($"Name:{propertyInfo.Name}"); + sb.AppendLine ($"Type:{propertyInfo.PropertyType}"); + sb.AppendLine ($"CanWrite:{propertyInfo.CanWrite}"); + sb.AppendLine ($"CanRead:{propertyInfo.CanRead}"); - if (val is EventInfo ev) - { - sb.AppendLine ($"Name:{ev.Name}"); - sb.AppendLine ("Parameters:"); + break; - foreach (ParameterInfo parameter in ev.EventHandlerType.GetMethod ("Invoke") - .GetParameters ()) - { - sb.AppendLine ($" {parameter.ParameterType} {parameter.Name}"); - } - } + case FieldInfo fieldInfo: + sb.AppendLine ($"Name:{fieldInfo.Name}"); + sb.AppendLine ($"Type:{fieldInfo.FieldType}"); - if (val is MethodInfo method) - { - sb.AppendLine ($"Name:{method.Name}"); - sb.AppendLine ($"IsPublic:{method.IsPublic}"); - sb.AppendLine ($"IsStatic:{method.IsStatic}"); - sb.AppendLine ($"Parameters:{(method.GetParameters ().Any () ? "" : "None")}"); + break; - foreach (ParameterInfo parameter in method.GetParameters ()) - { - sb.AppendLine ($" {parameter.ParameterType} {parameter.Name}"); - } - } + case EventInfo eventInfo: + sb.AppendLine ($"Name:{eventInfo.Name}"); + sb.AppendLine ("Parameters:"); - if (val is ConstructorInfo ctor) - { - sb.AppendLine ($"Name:{ctor.Name}"); - sb.AppendLine ($"Parameters:{(ctor.GetParameters ().Any () ? "" : "None")}"); + if (eventInfo.EventHandlerType?.GetMethod ("Invoke") is { } invokeMethod) + { + foreach (ParameterInfo parameter in invokeMethod.GetParameters ()) + { + sb.AppendLine ($" {parameter.ParameterType} {parameter.Name}"); + } + } - foreach (ParameterInfo parameter in ctor.GetParameters ()) - { - sb.AppendLine ($" {parameter.ParameterType} {parameter.Name}"); - } + break; + + case MethodInfo methodInfo: + sb.AppendLine ($"Name:{methodInfo.Name}"); + sb.AppendLine ($"IsPublic:{methodInfo.IsPublic}"); + sb.AppendLine ($"IsStatic:{methodInfo.IsStatic}"); + sb.AppendLine ($"Parameters:{(methodInfo.GetParameters ().Length > 0 ? string.Empty : "None")}"); + + foreach (ParameterInfo parameter in methodInfo.GetParameters ()) + { + sb.AppendLine ($" {parameter.ParameterType} {parameter.Name}"); + } + + break; + + case ConstructorInfo constructorInfo: + sb.AppendLine ($"Name:{constructorInfo.Name}"); + sb.AppendLine ($"Parameters:{(constructorInfo.GetParameters ().Length > 0 ? string.Empty : "None")}"); + + foreach (ParameterInfo parameter in constructorInfo.GetParameters ()) + { + sb.AppendLine ($" {parameter.ParameterType} {parameter.Name}"); + } + + break; } _textView.Text = sb.ToString ().Replace ("\r\n", "\n"); @@ -303,7 +327,7 @@ public class ClassExplorer : Scenario Methods } - private class ShowForType + private sealed class ShowForType { public ShowForType (Showable toShow, Type type) { @@ -314,13 +338,11 @@ public class ClassExplorer : Scenario public Showable ToShow { get; } public Type Type { get; } - // Make sure to implement Equals methods on your objects if you intend to return new instances every time in ChildGetter - public override bool Equals (object obj) - { - return obj is ShowForType type && EqualityComparer.Default.Equals (Type, type.Type) && ToShow == type.ToShow; - } + public override bool Equals (object? obj) => + obj is ShowForType type && EqualityComparer.Default.Equals (Type, type.Type) && ToShow == type.ToShow; - public override int GetHashCode () { return HashCode.Combine (Type, ToShow); } - public override string ToString () { return ToShow.ToString (); } + public override int GetHashCode () => HashCode.Combine (Type, ToShow); + + public override string ToString () => ToShow.ToString (); } } diff --git a/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs b/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs index 2c2dc89ed..1516647d9 100644 --- a/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs +++ b/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs @@ -1,14 +1,13 @@ -using System; -using System.Collections.Generic; +#nullable enable + using System.Collections.ObjectModel; -using System.Linq; namespace UICatalog.Scenarios; [ScenarioMetadata ( - "Collection Navigator", - "Demonstrates keyboard navigation in ListView & TreeView (CollectionNavigator)." - )] + "Collection Navigator", + "Demonstrates keyboard navigation in ListView & TreeView (CollectionNavigator)." + )] [ScenarioCategory ("Controls")] [ScenarioCategory ("ListView")] [ScenarioCategory ("TreeView")] @@ -16,120 +15,165 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Mouse and Keyboard")] public class CollectionNavigatorTester : Scenario { - private ObservableCollection _items = new ObservableCollection (new ObservableCollection () - { - "a", - "b", - "bb", - "c", - "ccc", - "ccc", - "cccc", - "ddd", - "dddd", - "dddd", - "ddddd", - "dddddd", - "ddddddd", - "this", - "this is a test", - "this was a test", - "this and", - "that and that", - "the", - "think", - "thunk", - "thunks", - "zip", - "zap", - "zoo", - "@jack", - "@sign", - "@at", - "@ateme", - "n@", - "n@brown", - ".net", - "$100.00", - "$101.00", - "$101.10", - "$101.11", - "$200.00", - "$210.99", - "$$", - "apricot", - "arm", - "丗丙业丞", - "丗丙丛", - "text", - "egg", - "candle", - " <- space", - "\t<- tab", - "\n<- newline", - "\r<- formfeed", - "q", - "quit", - "quitter" - }.ToList ()); + private ObservableCollection _items = new ( + [ + "a", + "b", + "bb", + "c", + "ccc", + "ccc", + "cccc", + "ddd", + "dddd", + "dddd", + "ddddd", + "dddddd", + "ddddddd", + "this", + "this is a test", + "this was a test", + "this and", + "that and that", + "the", + "think", + "thunk", + "thunks", + "zip", + "zap", + "zoo", + "@jack", + "@sign", + "@at", + "@ateme", + "n@", + "n@brown", + ".net", + "$100.00", + "$101.00", + "$101.10", + "$101.11", + "$200.00", + "$210.99", + "$$", + "apricot", + "arm", + "丗丙业丞", + "丗丙丛", + "text", + "egg", + "candle", + " <- space", + "\t<- tab", + "\n<- newline", + "\r<- formfeed", + "q", + "quit", + "quitter" + ] + ); - private Toplevel top; - private ListView _listView; - private TreeView _treeView; + private Window? _top; + private ListView? _listView; + private TreeView? _treeView; + private CheckBox? _allowMarkingCheckBox; + private CheckBox? _allowMultiSelectionCheckBox; - // Don't create a Window, just return the top-level view public override void Main () { Application.Init (); - top = new Toplevel { SchemeName = "Base" }; - var allowMarking = new MenuItem ("Allow _Marking", "", null) + Window top = new () { - CheckType = MenuItemCheckStyle.Checked, Checked = false + SchemeName = "Base" }; - allowMarking.Action = () => allowMarking.Checked = _listView.AllowsMarking = !_listView.AllowsMarking; + _top = top; - var allowMultiSelection = new MenuItem ("Allow Multi _Selection", "", null) + // MenuBar + MenuBar menu = new (); + + _allowMarkingCheckBox = new () { - CheckType = MenuItemCheckStyle.Checked, Checked = false + Title = "Allow _Marking" }; - allowMultiSelection.Action = () => - allowMultiSelection.Checked = - _listView.AllowsMultipleSelection = !_listView.AllowsMultipleSelection; - allowMultiSelection.CanExecute = () => (bool)allowMarking.Checked; + _allowMarkingCheckBox.CheckedStateChanged += (s, e) => + { + if (_listView is { }) + { + _listView.AllowsMarking = _allowMarkingCheckBox.CheckedState == CheckState.Checked; + } - var menu = new MenuBar + if (_allowMultiSelectionCheckBox is { }) + { + _allowMultiSelectionCheckBox.Enabled = _allowMarkingCheckBox.CheckedState == CheckState.Checked; + } + }; + + _allowMultiSelectionCheckBox = new () { - Menus = - [ - new MenuBarItem ( - "_Configure", - new [] - { - allowMarking, - allowMultiSelection, - null, - new ( - "_Quit", - $"{Application.QuitKey}", - () => Quit (), - null, - null, - (KeyCode)Application.QuitKey - ) - } - ), - new MenuBarItem ("_Quit", $"{Application.QuitKey}", () => Quit ()) - ] + Title = "Allow Multi _Selection", + Enabled = false }; + _allowMultiSelectionCheckBox.CheckedStateChanged += (s, e) => + { + if (_listView is { }) + { + _listView.AllowsMultipleSelection = + _allowMultiSelectionCheckBox.CheckedState == CheckState.Checked; + } + }; + + menu.Add ( + new MenuBarItem ( + "_Configure", + [ + new MenuItem + { + CommandView = _allowMarkingCheckBox + }, + new MenuItem + { + CommandView = _allowMultiSelectionCheckBox + }, + new MenuItem + { + Title = "_Quit", + Key = Application.QuitKey, + Action = Quit + } + ] + ) + ); + + menu.Add ( + new MenuBarItem ( + "_Quit", + [ + new MenuItem + { + Title = "_Quit", + Key = Application.QuitKey, + Action = Quit + } + ] + ) + ); + top.Add (menu); _items = new (_items.OrderBy (i => i, StringComparer.OrdinalIgnoreCase)); CreateListView (); - var vsep = new Line { Orientation = Orientation.Vertical, X = Pos.Right (_listView), Y = 1, Height = Dim.Fill () }; + + Line vsep = new () + { + Orientation = Orientation.Vertical, + X = Pos.Right (_listView!), + Y = 1, + Height = Dim.Fill () + }; top.Add (vsep); CreateTreeView (); @@ -140,7 +184,12 @@ public class CollectionNavigatorTester : Scenario private void CreateListView () { - var label = new Label + if (_top is null) + { + return; + } + + Label label = new () { Text = "ListView", TextAlignment = Alignment.Center, @@ -149,9 +198,9 @@ public class CollectionNavigatorTester : Scenario Width = Dim.Percent (50), Height = 1 }; - top.Add (label); + _top.Add (label); - _listView = new ListView + _listView = new () { X = 0, Y = Pos.Bottom (label), @@ -160,7 +209,7 @@ public class CollectionNavigatorTester : Scenario AllowsMarking = false, AllowsMultipleSelection = false }; - top.Add (_listView); + _top.Add (_listView); _listView.SetSource (_items); @@ -169,7 +218,12 @@ public class CollectionNavigatorTester : Scenario private void CreateTreeView () { - var label = new Label + if (_top is null || _listView is null) + { + return; + } + + Label label = new () { Text = "TreeView", TextAlignment = Alignment.Center, @@ -178,23 +232,26 @@ public class CollectionNavigatorTester : Scenario Width = Dim.Percent (50), Height = 1 }; - top.Add (label); + _top.Add (label); - _treeView = new TreeView + _treeView = new () { - X = Pos.Right (_listView) + 1, Y = Pos.Bottom (label), Width = Dim.Fill (), Height = Dim.Fill () + X = Pos.Right (_listView) + 1, + Y = Pos.Bottom (label), + Width = Dim.Fill (), + Height = Dim.Fill () }; _treeView.Style.HighlightModelTextOnly = true; - top.Add (_treeView); + _top.Add (_treeView); - var root = new TreeNode ("IsLetterOrDigit examples"); + TreeNode root = new ("IsLetterOrDigit examples"); root.Children = _items.Where (i => char.IsLetterOrDigit (i [0])) .Select (i => new TreeNode (i)) .Cast () .ToList (); _treeView.AddObject (root); - root = new TreeNode ("Non-IsLetterOrDigit examples"); + root = new ("Non-IsLetterOrDigit examples"); root.Children = _items.Where (i => !char.IsLetterOrDigit (i [0])) .Select (i => new TreeNode (i)) diff --git a/Examples/UICatalog/Scenarios/ContextMenus.cs b/Examples/UICatalog/Scenarios/ContextMenus.cs index bb3a7b19e..5baaabded 100644 --- a/Examples/UICatalog/Scenarios/ContextMenus.cs +++ b/Examples/UICatalog/Scenarios/ContextMenus.cs @@ -112,13 +112,13 @@ public class ContextMenus : Scenario { _winContextMenu = new ( [ - new MenuItemv2 + new MenuItem { Title = "C_ultures", SubMenu = GetSupportedCultureMenu (), }, new Line (), - new MenuItemv2 + new MenuItem { Title = "_Configuration...", HelpText = "Show configuration", @@ -130,12 +130,12 @@ public class ContextMenus : Scenario "Ok" ) }, - new MenuItemv2 + new MenuItem { Title = "M_ore options", SubMenu = new ( [ - new MenuItemv2 + new MenuItem { Title = "_Setup...", HelpText = "Perform setup", @@ -149,7 +149,7 @@ public class ContextMenus : Scenario ), Key = Key.T.WithCtrl }, - new MenuItemv2 + new MenuItem { Title = "_Maintenance...", HelpText = "Maintenance mode", @@ -165,7 +165,7 @@ public class ContextMenus : Scenario ]) }, new Line (), - new MenuItemv2 + new MenuItem { Title = "_Quit", Action = () => Application.RequestStop () @@ -177,14 +177,14 @@ public class ContextMenus : Scenario Application.Popover?.Register (_winContextMenu); } - private Menuv2 GetSupportedCultureMenu () + private Menu GetSupportedCultureMenu () { - List supportedCultures = []; + List supportedCultures = []; int index = -1; foreach (CultureInfo c in _cultureInfos!) { - MenuItemv2 culture = new (); + MenuItem culture = new (); culture.CommandView = new CheckBox { CanFocus = false }; @@ -215,17 +215,17 @@ public class ContextMenus : Scenario supportedCultures.Add (culture); } - Menuv2 menu = new (supportedCultures.ToArray ()); + Menu menu = new (supportedCultures.ToArray ()); return menu; - void CreateAction (List cultures, MenuItemv2 culture) + void CreateAction (List cultures, MenuItem culture) { culture.Action += () => { Thread.CurrentThread.CurrentUICulture = new (culture.HelpText); - foreach (MenuItemv2 item in cultures) + foreach (MenuItem item in cultures) { ((CheckBox)item.CommandView).CheckedState = Thread.CurrentThread.CurrentUICulture.Name == item.HelpText ? CheckState.Checked : CheckState.UnChecked; diff --git a/Examples/UICatalog/Scenarios/CsvEditor.cs b/Examples/UICatalog/Scenarios/CsvEditor.cs index f61d03b14..5690a7509 100644 --- a/Examples/UICatalog/Scenarios/CsvEditor.cs +++ b/Examples/UICatalog/Scenarios/CsvEditor.cs @@ -1,8 +1,7 @@ -using System; +#nullable enable + using System.Data; using System.Globalization; -using System.IO; -using System.Linq; using System.Text.RegularExpressions; using CsvHelper; @@ -14,99 +13,37 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Controls")] [ScenarioCategory ("Dialogs")] [ScenarioCategory ("Text and Formatting")] -[ScenarioCategory ("Dialogs")] [ScenarioCategory ("Arrangement")] [ScenarioCategory ("Files and IO")] public class CsvEditor : Scenario { - private string _currentFile; - private DataTable _currentTable; - private MenuItem _miCentered; - private MenuItem _miLeft; - private MenuItem _miRight; - private TextField _selectedCellTextField; - private TableView _tableView; + private string? _currentFile; + private DataTable? _currentTable; + private CheckBox? _miCenteredCheckBox; + private CheckBox? _miLeftCheckBox; + private CheckBox? _miRightCheckBox; + private TextField? _selectedCellTextField; + private TableView? _tableView; public override void Main () { - // Init Application.Init (); - // Setup - Create a top-level application window and configure it. - Toplevel appWindow = new () + Window appWindow = new () { - Title = $"{GetName ()}" + Title = GetName () }; - //appWindow.Height = Dim.Fill (1); // status bar + // MenuBar + MenuBar menu = new (); - _tableView = new () { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (2) }; - - var fileMenu = new MenuBarItem ( - "_File", - new MenuItem [] - { - new ("_Open CSV", "", () => Open ()), - new ("_Save", "", () => Save ()), - new ("_Quit", "Quits The App", () => Quit ()) - } - ); - - //fileMenu.Help = "Help"; - var menu = new MenuBar + _tableView = new () { - Menus = - [ - fileMenu, - new ( - "_Edit", - new MenuItem [] - { - new ("_New Column", "", () => AddColumn ()), - new ("_New Row", "", () => AddRow ()), - new ( - "_Rename Column", - "", - () => RenameColumn () - ), - new ("_Delete Column", "", () => DeleteColum ()), - new ("_Move Column", "", () => MoveColumn ()), - new ("_Move Row", "", () => MoveRow ()), - new ("_Sort Asc", "", () => Sort (true)), - new ("_Sort Desc", "", () => Sort (false)) - } - ), - new ( - "_View", - new [] - { - _miLeft = new ( - "_Align Left", - "", - () => Align (Alignment.Start) - ), - _miRight = new ( - "_Align Right", - "", - () => Align (Alignment.End) - ), - _miCentered = new ( - "_Align Centered", - "", - () => Align (Alignment.Center) - ), - - // Format requires hard typed data table, when we read a CSV everything is untyped (string) so this only works for new columns in this demo - _miCentered = new ( - "_Set Format Pattern", - "", - () => SetFormat () - ) - } - ) - ] + X = 0, + Y = Pos.Bottom (menu), + Width = Dim.Fill (), + Height = Dim.Fill (1) }; - appWindow.Add (menu); _selectedCellTextField = new () { @@ -116,50 +53,162 @@ public class CsvEditor : Scenario }; _selectedCellTextField.TextChanged += SelectedCellLabel_TextChanged; - var statusBar = new StatusBar ( - [ - new (Application.QuitKey, "Quit", Quit, "Quit!"), - new (Key.O.WithCtrl, "Open", Open, "Open a file."), - new (Key.S.WithCtrl, "Save", Save, "Save current."), - new () - { - HelpText = "Cell:", - CommandView = _selectedCellTextField, - AlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast, - Enabled = false - } - ]) + // StatusBar + StatusBar statusBar = new ( + [ + new (Application.QuitKey, "Quit", Quit, "Quit!"), + new (Key.O.WithCtrl, "Open", Open, "Open a file."), + new (Key.S.WithCtrl, "Save", Save, "Save current."), + new () + { + HelpText = "Cell:", + CommandView = _selectedCellTextField, + AlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast, + Enabled = false + } + ] + ) { AlignmentModes = AlignmentModes.IgnoreFirstOrLast }; - appWindow.Add (statusBar); - appWindow.Add (_tableView); + // Setup menu checkboxes for alignment + _miLeftCheckBox = new () + { + Title = "_Align Left" + }; + _miLeftCheckBox.CheckedStateChanged += (s, e) => Align (Alignment.Start); + + _miRightCheckBox = new () + { + Title = "_Align Right" + }; + _miRightCheckBox.CheckedStateChanged += (s, e) => Align (Alignment.End); + + _miCenteredCheckBox = new () + { + Title = "_Align Centered" + }; + _miCenteredCheckBox.CheckedStateChanged += (s, e) => Align (Alignment.Center); + + MenuBarItem fileMenu = new ( + "_File", + [ + new MenuItem + { + Title = "_Open CSV", + Action = Open + }, + new MenuItem + { + Title = "_Save", + Action = Save + }, + new MenuItem + { + Title = "_Quit", + HelpText = "Quits The App", + Action = Quit + } + ] + ); + + menu.Add (fileMenu); + + menu.Add ( + new MenuBarItem ( + "_Edit", + [ + new MenuItem + { + Title = "_New Column", + Action = AddColumn + }, + new MenuItem + { + Title = "_New Row", + Action = AddRow + }, + new MenuItem + { + Title = "_Rename Column", + Action = RenameColumn + }, + new MenuItem + { + Title = "_Delete Column", + Action = DeleteColum + }, + new MenuItem + { + Title = "_Move Column", + Action = MoveColumn + }, + new MenuItem + { + Title = "_Move Row", + Action = MoveRow + }, + new MenuItem + { + Title = "_Sort Asc", + Action = () => Sort (true) + }, + new MenuItem + { + Title = "_Sort Desc", + Action = () => Sort (false) + } + ] + ) + ); + + menu.Add ( + new MenuBarItem ( + "_View", + [ + new MenuItem + { + CommandView = _miLeftCheckBox + }, + new MenuItem + { + CommandView = _miRightCheckBox + }, + new MenuItem + { + CommandView = _miCenteredCheckBox + }, + new MenuItem + { + Title = "_Set Format Pattern", + Action = SetFormat + } + ] + ) + ); + + appWindow.Add (menu, _tableView, statusBar); _tableView.SelectedCellChanged += OnSelectedCellChanged; _tableView.CellActivated += EditCurrentCell; _tableView.KeyDown += TableViewKeyPress; - //SetupScrollBar (); - - // Run - Start the application. Application.Run (appWindow); appWindow.Dispose (); - - // Shutdown - Calling Application.Shutdown is required. Application.Shutdown (); } private void AddColumn () { - if (NoTableLoaded ()) + if (NoTableLoaded () || _tableView is null || _currentTable is null) { return; } if (GetText ("Enter column name", "Name:", "", out string colName)) { - var col = new DataColumn (colName); + DataColumn col = new (colName); int newColIdx = Math.Min ( Math.Max (0, _tableView.SelectedColumn + 1), @@ -209,7 +258,7 @@ public class CsvEditor : Scenario private void AddRow () { - if (NoTableLoaded ()) + if (NoTableLoaded () || _currentTable is null || _tableView is null) { return; } @@ -224,7 +273,7 @@ public class CsvEditor : Scenario private void Align (Alignment newAlignment) { - if (NoTableLoaded ()) + if (NoTableLoaded () || _tableView is null) { return; } @@ -232,24 +281,27 @@ public class CsvEditor : Scenario ColumnStyle style = _tableView.Style.GetOrCreateColumnStyle (_tableView.SelectedColumn); style.Alignment = newAlignment; - _miLeft.Checked = style.Alignment == Alignment.Start; - _miRight.Checked = style.Alignment == Alignment.End; - _miCentered.Checked = style.Alignment == Alignment.Center; + if (_miLeftCheckBox is { }) + { + _miLeftCheckBox.CheckedState = style.Alignment == Alignment.Start ? CheckState.Checked : CheckState.UnChecked; + } + + if (_miRightCheckBox is { }) + { + _miRightCheckBox.CheckedState = style.Alignment == Alignment.End ? CheckState.Checked : CheckState.UnChecked; + } + + if (_miCenteredCheckBox is { }) + { + _miCenteredCheckBox.CheckedState = style.Alignment == Alignment.Center ? CheckState.Checked : CheckState.UnChecked; + } _tableView.Update (); } - private void ClearColumnStyles () - { - _tableView.Style.ColumnStyles.Clear (); - _tableView.Update (); - } - - private void CloseExample () { _tableView.Table = null; } - private void DeleteColum () { - if (NoTableLoaded ()) + if (NoTableLoaded () || _tableView is null || _currentTable is null) { return; } @@ -272,16 +324,16 @@ public class CsvEditor : Scenario } } - private void EditCurrentCell (object sender, CellActivatedEventArgs e) + private void EditCurrentCell (object? sender, CellActivatedEventArgs e) { - if (e.Table == null) + if (e.Table is null || _currentTable is null || _tableView is null) { return; } var oldValue = _currentTable.Rows [e.Row] [e.Col].ToString (); - if (GetText ("Enter new value", _currentTable.Columns [e.Col].ColumnName, oldValue, out string newText)) + if (GetText ("Enter new value", _currentTable.Columns [e.Col].ColumnName, oldValue ?? "", out string newText)) { try { @@ -301,20 +353,20 @@ public class CsvEditor : Scenario { var okPressed = false; - var ok = new Button { Text = "Ok", IsDefault = true }; + Button ok = new () { Text = "Ok", IsDefault = true }; ok.Accepting += (s, e) => - { - okPressed = true; - Application.RequestStop (); - }; - var cancel = new Button { Text = "Cancel" }; + { + okPressed = true; + Application.RequestStop (); + }; + Button cancel = new () { Text = "Cancel" }; cancel.Accepting += (s, e) => { Application.RequestStop (); }; - var d = new Dialog { Title = title, Buttons = [ok, cancel] }; + Dialog d = new () { Title = title, Buttons = [ok, cancel] }; - var lbl = new Label { X = 0, Y = 1, Text = label }; + Label lbl = new () { X = 0, Y = 1, Text = label }; - var tf = new TextField { Text = initialText, X = 0, Y = 2, Width = Dim.Fill () }; + TextField tf = new () { Text = initialText, X = 0, Y = 2, Width = Dim.Fill () }; d.Add (lbl, tf); tf.SetFocus (); @@ -322,14 +374,14 @@ public class CsvEditor : Scenario Application.Run (d); d.Dispose (); - enteredText = okPressed ? tf.Text : null; + enteredText = okPressed ? tf.Text : string.Empty; return okPressed; } private void MoveColumn () { - if (NoTableLoaded ()) + if (NoTableLoaded () || _currentTable is null || _tableView is null) { return; } @@ -367,7 +419,7 @@ public class CsvEditor : Scenario private void MoveRow () { - if (NoTableLoaded ()) + if (NoTableLoaded () || _currentTable is null || _tableView is null) { return; } @@ -394,7 +446,7 @@ public class CsvEditor : Scenario return; } - object [] arrayItems = currentRow.ItemArray; + object?[] arrayItems = currentRow.ItemArray; _currentTable.Rows.Remove (currentRow); // Removing and Inserting the same DataRow seems to result in it loosing its values so we have to create a new instance @@ -416,7 +468,7 @@ public class CsvEditor : Scenario private bool NoTableLoaded () { - if (_tableView.Table == null) + if (_tableView?.Table is null) { MessageBox.ErrorQuery ("No Table Loaded", "No table has currently be opened", "Ok"); @@ -426,31 +478,47 @@ public class CsvEditor : Scenario return false; } - private void OnSelectedCellChanged (object sender, SelectedCellChangedEventArgs e) + private void OnSelectedCellChanged (object? sender, SelectedCellChangedEventArgs e) { + if (_selectedCellTextField is null || _tableView is null) + { + return; + } + // only update the text box if the user is not manually editing it if (!_selectedCellTextField.HasFocus) { _selectedCellTextField.Text = $"{_tableView.SelectedRow},{_tableView.SelectedColumn}"; } - if (_tableView.Table == null || _tableView.SelectedColumn == -1) + if (_tableView.Table is null || _tableView.SelectedColumn == -1) { return; } - ColumnStyle style = _tableView.Style.GetColumnStyleIfAny (_tableView.SelectedColumn); + ColumnStyle? style = _tableView.Style.GetColumnStyleIfAny (_tableView.SelectedColumn); - _miLeft.Checked = style?.Alignment == Alignment.Start; - _miRight.Checked = style?.Alignment == Alignment.End; - _miCentered.Checked = style?.Alignment == Alignment.Center; + if (_miLeftCheckBox is { }) + { + _miLeftCheckBox.CheckedState = style?.Alignment == Alignment.Start ? CheckState.Checked : CheckState.UnChecked; + } + + if (_miRightCheckBox is { }) + { + _miRightCheckBox.CheckedState = style?.Alignment == Alignment.End ? CheckState.Checked : CheckState.UnChecked; + } + + if (_miCenteredCheckBox is { }) + { + _miCenteredCheckBox.CheckedState = style?.Alignment == Alignment.Center ? CheckState.Checked : CheckState.UnChecked; + } } private void Open () { - var ofd = new FileDialog + FileDialog ofd = new () { - AllowedTypes = new () { new AllowedType ("Comma Separated Values", ".csv") } + AllowedTypes = [new AllowedType ("Comma Separated Values", ".csv")] }; ofd.Style.OkButtonText = "Open"; @@ -471,13 +539,13 @@ public class CsvEditor : Scenario try { - using var reader = new CsvReader (File.OpenText (filename), CultureInfo.InvariantCulture); + using CsvReader reader = new (File.OpenText (filename), CultureInfo.InvariantCulture); - var dt = new DataTable (); + DataTable dt = new (); reader.Read (); - if (reader.ReadHeader ()) + if (reader.ReadHeader () && reader.HeaderRecord is { }) { foreach (string h in reader.HeaderRecord) { @@ -501,8 +569,16 @@ public class CsvEditor : Scenario // Only set the current filename if we successfully loaded the entire file _currentFile = filename; - _selectedCellTextField.SuperView.Enabled = true; - Application.TopRunnable.Title = $"{GetName ()} - {Path.GetFileName (_currentFile)}"; + + if (_selectedCellTextField?.SuperView is { }) + { + _selectedCellTextField.SuperView.Enabled = true; + } + + if (Application.TopRunnable is { }) + { + Application.TopRunnable.Title = $"{GetName ()} - {Path.GetFileName (_currentFile)}"; + } } catch (Exception ex) { @@ -518,7 +594,7 @@ public class CsvEditor : Scenario private void RenameColumn () { - if (NoTableLoaded ()) + if (NoTableLoaded () || _currentTable is null || _tableView is null) { return; } @@ -534,17 +610,17 @@ public class CsvEditor : Scenario private void Save () { - if (_tableView.Table == null || string.IsNullOrWhiteSpace (_currentFile)) + if (_tableView?.Table is null || string.IsNullOrWhiteSpace (_currentFile) || _currentTable is null) { MessageBox.ErrorQuery ("No file loaded", "No file is currently loaded", "Ok"); return; } - using var writer = new CsvWriter ( - new StreamWriter (File.OpenWrite (_currentFile)), - CultureInfo.InvariantCulture - ); + using CsvWriter writer = new ( + new StreamWriter (File.OpenWrite (_currentFile)), + CultureInfo.InvariantCulture + ); foreach (string col in _currentTable.Columns.Cast ().Select (c => c.ColumnName)) { @@ -555,7 +631,7 @@ public class CsvEditor : Scenario foreach (DataRow row in _currentTable.Rows) { - foreach (object item in row.ItemArray) + foreach (object? item in row.ItemArray) { writer.WriteField (item); } @@ -564,8 +640,13 @@ public class CsvEditor : Scenario } } - private void SelectedCellLabel_TextChanged (object sender, EventArgs e) + private void SelectedCellLabel_TextChanged (object? sender, EventArgs e) { + if (_selectedCellTextField is null || _tableView is null) + { + return; + } + // if user is in the text control and editing the selected cell if (!_selectedCellTextField.HasFocus) { @@ -577,14 +658,14 @@ public class CsvEditor : Scenario if (match.Success) { - _tableView.SelectedColumn = int.Parse (match.Groups [1].Value); - _tableView.SelectedRow = int.Parse (match.Groups [2].Value); + _tableView.SelectedColumn = int.Parse (match.Groups [2].Value); + _tableView.SelectedRow = int.Parse (match.Groups [1].Value); } } private void SetFormat () { - if (NoTableLoaded ()) + if (NoTableLoaded () || _currentTable is null || _tableView is null) { return; } @@ -611,46 +692,19 @@ public class CsvEditor : Scenario } } - private void SetTable (DataTable dataTable) { _tableView.Table = new DataTableSource (_currentTable = dataTable); } + private void SetTable (DataTable dataTable) + { + if (_tableView is null) + { + return; + } - //private void SetupScrollBar () - //{ - // var scrollBar = new ScrollBarView (_tableView, true); - - // scrollBar.ChangedPosition += (s, e) => - // { - // _tableView.RowOffset = scrollBar.Position; - - // if (_tableView.RowOffset != scrollBar.Position) - // { - // scrollBar.Position = _tableView.RowOffset; - // } - - // _tableView.SetNeedsDraw (); - // }; - // /* - // scrollBar.OtherScrollBarView.ChangedPosition += (s,e) => { - // tableView.LeftItem = scrollBar.OtherScrollBarView.Position; - // if (tableView.LeftItem != scrollBar.OtherScrollBarView.Position) { - // scrollBar.OtherScrollBarView.Position = tableView.LeftItem; - // } - // tableView.SetNeedsDraw (); - // };*/ - - // _tableView.DrawingContent += (s, e) => - // { - // scrollBar.Size = _tableView.Table?.Rows ?? 0; - // scrollBar.Position = _tableView.RowOffset; - - // //scrollBar.OtherScrollBarView.Size = tableView.Maxlength - 1; - // //scrollBar.OtherScrollBarView.Position = tableView.LeftItem; - // scrollBar.Refresh (); - // }; - //} + _tableView.Table = new DataTableSource (_currentTable = dataTable); + } private void Sort (bool asc) { - if (NoTableLoaded ()) + if (NoTableLoaded () || _currentTable is null || _tableView is null) { return; } @@ -668,9 +722,14 @@ public class CsvEditor : Scenario SetTable (_currentTable.DefaultView.ToTable ()); } - private void TableViewKeyPress (object sender, Key e) + private void TableViewKeyPress (object? sender, Key e) { - if (e.KeyCode == KeyCode.Delete) + if (_currentTable is null || _tableView is null) + { + return; + } + + if (e.KeyCode == Key.Delete) { if (_tableView.FullRowSelect) { diff --git a/Examples/UICatalog/Scenarios/DynamicMenuBar.cs b/Examples/UICatalog/Scenarios/DynamicMenuBar.cs deleted file mode 100644 index 28a13a916..000000000 --- a/Examples/UICatalog/Scenarios/DynamicMenuBar.cs +++ /dev/null @@ -1,1413 +0,0 @@ -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text; - -#pragma warning disable CS0618 // Type or member is obsolete - -namespace UICatalog.Scenarios; - -[ScenarioMetadata ("Dynamic MenuBar", "Demonstrates how to change a MenuBar dynamically.")] -[ScenarioCategory ("Arrangement")] -[ScenarioCategory ("Menus")] -public class DynamicMenuBar : Scenario -{ - public override void Main () - { - // Init - Application.Init (); - - // Setup - Create a top-level application window and configure it. - DynamicMenuBarSample appWindow = new () - { - Title = GetQuitKeyAndName () - }; - - // Run - Start the application. - Application.Run (appWindow); - appWindow.Dispose (); - - // Shutdown - Calling Application.Shutdown is required. - Application.Shutdown (); - } - - public class Binding - { - private readonly PropertyInfo _sourceBindingProperty; - private readonly object _sourceDataContext; - private readonly IValueConverter _valueConverter; - - public Binding ( - View source, - string sourcePropertyName, - View target, - string targetPropertyName, - IValueConverter valueConverter = null - ) - { - Target = target; - Source = source; - SourcePropertyName = sourcePropertyName; - TargetPropertyName = targetPropertyName; - _sourceDataContext = Source.GetType ().GetProperty ("DataContext").GetValue (Source); - _sourceBindingProperty = _sourceDataContext.GetType ().GetProperty (SourcePropertyName); - _valueConverter = valueConverter; - UpdateTarget (); - - var notifier = (INotifyPropertyChanged)_sourceDataContext; - - if (notifier != null) - { - notifier.PropertyChanged += (s, e) => - { - if (e.PropertyName == SourcePropertyName) - { - UpdateTarget (); - } - }; - } - } - - public View Source { get; } - public string SourcePropertyName { get; } - public View Target { get; } - public string TargetPropertyName { get; } - - private void UpdateTarget () - { - try - { - object sourceValue = _sourceBindingProperty.GetValue (_sourceDataContext); - - if (sourceValue == null) - { - return; - } - - object finalValue = _valueConverter?.Convert (sourceValue) ?? sourceValue; - - PropertyInfo targetProperty = Target.GetType ().GetProperty (TargetPropertyName); - targetProperty.SetValue (Target, finalValue); - } - catch (Exception ex) - { - MessageBox.ErrorQuery ("Binding Error", $"Binding failed: {ex}.", "Ok"); - } - } - } - - public class DynamicMenuBarDetails : FrameView - { - private bool _hasParent; - private MenuItem _menuItem; - - public DynamicMenuBarDetails (MenuItem menuItem = null, bool hasParent = false) : this () - { - _menuItem = menuItem; - _hasParent = hasParent; - Title = menuItem == null ? "Adding New Menu." : "Editing Menu."; - } - - public DynamicMenuBarDetails () - { - var lblTitle = new Label { Y = 1, Text = "Title:" }; - Add (lblTitle); - - TextTitle = new () { X = Pos.Right (lblTitle) + 2, Y = Pos.Top (lblTitle), Width = Dim.Fill () }; - Add (TextTitle); - - var lblHelp = new Label { X = Pos.Left (lblTitle), Y = Pos.Bottom (lblTitle) + 1, Text = "Help:" }; - Add (lblHelp); - - TextHelp = new () { X = Pos.Left (TextTitle), Y = Pos.Top (lblHelp), Width = Dim.Fill () }; - Add (TextHelp); - - var lblAction = new Label { X = Pos.Left (lblTitle), Y = Pos.Bottom (lblHelp) + 1, Text = "Action:" }; - Add (lblAction); - - TextAction = new () - { - X = Pos.Left (TextTitle), Y = Pos.Top (lblAction), Width = Dim.Fill (), Height = 5 - }; - Add (TextAction); - - var lblHotKey = new Label { X = Pos.Left (lblTitle), Y = Pos.Bottom (lblAction) + 5, Text = "HotKey:" }; - Add (lblHotKey); - - TextHotKey = new () - { - X = Pos.Left (TextTitle), Y = Pos.Bottom (lblAction) + 5, Width = 2, ReadOnly = true - }; - - TextHotKey.TextChanging += (s, e) => - { - if (!string.IsNullOrEmpty (e.Result) && char.IsLower (e.Result [0])) - { - e.Result = e.Result.ToUpper (); - } - }; - TextHotKey.TextChanged += (s, _) => TextHotKey.SelectAll (); - TextHotKey.SelectAll (); - Add (TextHotKey); - - CkbIsTopLevel = new () - { - X = Pos.Left (lblTitle), Y = Pos.Bottom (lblHotKey) + 1, Text = "IsTopLevel" - }; - Add (CkbIsTopLevel); - - CkbSubMenu = new () - { - X = Pos.Left (lblTitle), - Y = Pos.Bottom (CkbIsTopLevel), - CheckedState = (_menuItem == null ? !_hasParent : HasSubMenus (_menuItem)) ? CheckState.Checked : CheckState.UnChecked, - Text = "Has sub-menus" - }; - Add (CkbSubMenu); - - CkbNullCheck = new () - { - X = Pos.Left (lblTitle), Y = Pos.Bottom (CkbSubMenu), Text = "Allow null checked" - }; - Add (CkbNullCheck); - - var rChkLabels = new [] { "NoCheck", "Checked", "Radio" }; - - OsChkStyle = new () - { - X = Pos.Left (lblTitle), Y = Pos.Bottom (CkbSubMenu) + 1, Labels = rChkLabels - }; - Add (OsChkStyle); - - var lblShortcut = new Label - { - X = Pos.Right (CkbSubMenu) + 10, Y = Pos.Top (CkbSubMenu), Text = "Shortcut:" - }; - Add (lblShortcut); - - TextShortcutKey = new () - { - X = Pos.X (lblShortcut), Y = Pos.Bottom (lblShortcut), Width = Dim.Fill (), ReadOnly = true - }; - - TextShortcutKey.KeyDown += (s, e) => - { - TextShortcutKey.Text = e.ToString (); - - }; - - Add (TextShortcutKey); - - var btnShortcut = new Button - { - X = Pos.X (lblShortcut), Y = Pos.Bottom (TextShortcutKey) + 1, Text = "Clear Shortcut" - }; - btnShortcut.Accepting += (s, e) => { TextShortcutKey.Text = ""; }; - Add (btnShortcut); - - CkbIsTopLevel.CheckedStateChanging += (s, e) => - { - if ((_menuItem != null && _menuItem.Parent != null && e.Result == CheckState.Checked) - || (_menuItem == null && _hasParent && e.Result == CheckState.Checked)) - { - MessageBox.ErrorQuery ( - "Invalid IsTopLevel", - "Only menu bar can have top level menu item!", - "Ok" - ); - e.Handled = true; - - return; - } - - if (e.Result == CheckState.Checked) - { - CkbSubMenu.CheckedState = CheckState.UnChecked; - CkbSubMenu.SetNeedsDraw (); - TextHelp.Enabled = true; - TextAction.Enabled = true; - TextShortcutKey.Enabled = true; - } - else - { - if ((_menuItem == null && !_hasParent) || _menuItem.Parent == null) - { - CkbSubMenu.CheckedState = CheckState.Checked; - CkbSubMenu.SetNeedsDraw (); - TextShortcutKey.Enabled = false; - } - - TextHelp.Text = ""; - TextHelp.Enabled = false; - TextAction.Text = ""; - - TextShortcutKey.Enabled = - e.Result == CheckState.Checked && CkbSubMenu.CheckedState == CheckState.UnChecked; - } - }; - - CkbSubMenu.CheckedStateChanged += (s, e) => - { - if (e.Value == CheckState.Checked) - { - CkbIsTopLevel.CheckedState = CheckState.UnChecked; - CkbIsTopLevel.SetNeedsDraw (); - TextHelp.Text = ""; - TextHelp.Enabled = false; - TextAction.Text = ""; - TextAction.Enabled = false; - TextShortcutKey.Text = ""; - TextShortcutKey.Enabled = false; - } - else - { - if (!_hasParent) - { - CkbIsTopLevel.CheckedState = CheckState.Checked; - CkbIsTopLevel.SetNeedsDraw (); - TextShortcutKey.Enabled = true; - } - - TextHelp.Enabled = true; - TextAction.Enabled = true; - - if (_hasParent) - { - TextShortcutKey.Enabled = CkbIsTopLevel.CheckedState == CheckState.UnChecked - && e.Value == CheckState.UnChecked; - } - } - }; - - CkbNullCheck.CheckedStateChanged += (s, e) => - { - if (_menuItem != null) - { - _menuItem.AllowNullChecked = e.Value == CheckState.Checked; - } - }; - - //Add (_frmMenuDetails); - } - - public CheckBox CkbIsTopLevel { get; } - public CheckBox CkbNullCheck { get; } - public CheckBox CkbSubMenu { get; } - public OptionSelector OsChkStyle { get; } - public TextView TextAction { get; } - public TextField TextHelp { get; } - public TextField TextHotKey { get; } - public TextField TextShortcutKey { get; } - public TextField TextTitle { get; } - - public Action CreateAction (MenuItem menuItem, DynamicMenuItem item) - { - switch (menuItem.CheckType) - { - case MenuItemCheckStyle.NoCheck: - return () => MessageBox.ErrorQuery (item.Title, item.Action, "Ok"); - case MenuItemCheckStyle.Checked: - return menuItem.ToggleChecked; - case MenuItemCheckStyle.Radio: - break; - } - - return () => - { - menuItem.Checked = true; - var parent = menuItem?.Parent as MenuBarItem; - - if (parent != null) - { - MenuItem [] childrens = parent.Children; - - for (var i = 0; i < childrens.Length; i++) - { - MenuItem child = childrens [i]; - - if (child != menuItem) - { - child.Checked = false; - } - } - } - }; - } - - public void EditMenuBarItem (MenuItem menuItem) - { - if (menuItem == null) - { - _hasParent = false; - Enabled = false; - CleanEditMenuBarItem (); - - return; - } - - _hasParent = menuItem.Parent != null; - Enabled = true; - _menuItem = menuItem; - TextTitle.Text = menuItem?.Title ?? ""; - TextHelp.Text = menuItem?.Help ?? ""; - - TextAction.Text = menuItem.Action != null - ? GetTargetAction (menuItem.Action) - : string.Empty; - TextHotKey.Text = menuItem?.HotKey != Key.Empty ? menuItem.HotKey.ToString () : ""; - CkbIsTopLevel.CheckedState = IsTopLevel (menuItem) ? CheckState.Checked : CheckState.UnChecked; - CkbSubMenu.CheckedState = HasSubMenus (menuItem) ? CheckState.Checked : CheckState.UnChecked; - CkbNullCheck.CheckedState = menuItem.AllowNullChecked ? CheckState.Checked : CheckState.UnChecked; - TextHelp.Enabled = CkbSubMenu.CheckedState == CheckState.UnChecked; - TextAction.Enabled = CkbSubMenu.CheckedState == CheckState.UnChecked; - OsChkStyle.Value = (int)(menuItem?.CheckType ?? MenuItemCheckStyle.NoCheck); - TextShortcutKey.Text = menuItem?.ShortcutTag ?? ""; - - TextShortcutKey.Enabled = CkbIsTopLevel.CheckedState == CheckState.Checked && CkbSubMenu.CheckedState == CheckState.UnChecked - || CkbIsTopLevel.CheckedState == CheckState.UnChecked && CkbSubMenu.CheckedState == CheckState.UnChecked; - } - - public DynamicMenuItem EnterMenuItem () - { - var valid = false; - - if (_menuItem == null) - { - var m = new DynamicMenuItem (); - TextTitle.Text = m.Title; - TextHelp.Text = m.Help; - TextAction.Text = m.Action; - TextHotKey.Text = m.HotKey ?? string.Empty; - CkbIsTopLevel.CheckedState = CheckState.UnChecked; - CkbSubMenu.CheckedState = !_hasParent ? CheckState.Checked : CheckState.UnChecked; - CkbNullCheck.CheckedState = CheckState.UnChecked; - TextHelp.Enabled = _hasParent; - TextAction.Enabled = _hasParent; - TextShortcutKey.Enabled = _hasParent; - } - else - { - EditMenuBarItem (_menuItem); - } - - var btnOk = new Button { IsDefault = true, Text = "Ok" }; - - btnOk.Accepting += (s, e) => - { - if (string.IsNullOrEmpty (TextTitle.Text)) - { - MessageBox.ErrorQuery ("Invalid title", "Must enter a valid title!.", "Ok"); - } - else - { - valid = true; - Application.RequestStop (); - } - }; - var btnCancel = new Button { Text = "Cancel" }; - - btnCancel.Accepting += (s, e) => - { - TextTitle.Text = string.Empty; - Application.RequestStop (); - }; - - var dialog = new Dialog - { Title = "Enter the menu details.", Buttons = [btnOk, btnCancel], Height = Dim.Auto (DimAutoStyle.Content, 23, Application.Screen.Height) }; - - Width = Dim.Fill (); - Height = Dim.Fill () - 2; - dialog.Add (this); - TextTitle.SetFocus (); - TextTitle.CursorPosition = TextTitle.Text.Length; - Application.Run (dialog); - dialog.Dispose (); - - if (valid) - { - return new () - { - Title = TextTitle.Text, - Help = TextHelp.Text, - Action = TextAction.Text, - HotKey = TextHotKey.Text, - IsTopLevel = CkbIsTopLevel?.CheckedState == CheckState.Checked, - HasSubMenu = CkbSubMenu?.CheckedState == CheckState.Checked, - CheckStyle = OsChkStyle.Value == 0 ? MenuItemCheckStyle.NoCheck : - OsChkStyle.Value == 1 ? MenuItemCheckStyle.Checked : - MenuItemCheckStyle.Radio, - ShortcutKey = TextShortcutKey.Text, - AllowNullChecked = CkbNullCheck?.CheckedState == CheckState.Checked, - }; - } - - return null; - } - - public void UpdateParent (ref MenuItem menuItem) - { - var parent = menuItem.Parent as MenuBarItem; - int idx = parent.GetChildrenIndex (menuItem); - - if (!(menuItem is MenuBarItem)) - { - menuItem = new MenuBarItem (menuItem.Title, new MenuItem [] { }, menuItem.Parent); - - if (idx > -1) - { - parent.Children [idx] = menuItem; - } - } - else - { - menuItem = new ( - menuItem.Title, - menuItem.Help, - CreateAction (menuItem, new ()), - null, - menuItem.Parent - ); - - if (idx > -1) - { - parent.Children [idx] = menuItem; - } - } - } - - private void CleanEditMenuBarItem () - { - TextTitle.Text = ""; - TextHelp.Text = ""; - TextAction.Text = ""; - TextHotKey.Text = ""; - CkbIsTopLevel.CheckedState = CheckState.UnChecked; - CkbSubMenu.CheckedState = CheckState.UnChecked; - OsChkStyle.Value = (int)MenuItemCheckStyle.NoCheck; - TextShortcutKey.Text = ""; - } - - private string GetTargetAction (Action action) - { - object me = action.Target; - - if (me == null) - { - throw new ArgumentException (); - } - - var v = new object (); - - foreach (FieldInfo field in me.GetType ().GetFields ()) - { - if (field.Name == "item") - { - v = field.GetValue (me); - } - } - - return v == null || !(v is DynamicMenuItem item) ? string.Empty : item.Action; - } - - private bool HasSubMenus (MenuItem menuItem) - { - var menuBarItem = menuItem as MenuBarItem; - - if (menuBarItem != null && menuBarItem.Children != null && (menuBarItem.Children.Length > 0 || menuBarItem.Action == null)) - { - return true; - } - - return false; - } - - private bool IsTopLevel (MenuItem menuItem) - { - var topLevel = menuItem as MenuBarItem; - - if (topLevel != null && topLevel.Parent == null && (topLevel.Children == null || topLevel.Children.Length == 0) && topLevel.Action != null) - { - return true; - } - - return false; - } - } - - public class DynamicMenuBarSample : Window - { - private readonly ListView _lstMenus; - private MenuItem _currentEditMenuBarItem; - private MenuItem _currentMenuBarItem; - private int _currentSelectedMenuBar; - private MenuBar _menuBar; - - public DynamicMenuBarSample () - { - DataContext = new (); - - var frmDelimiter = new FrameView - { - X = Pos.Center (), - Y = 3, - Width = 25, - Height = 4, - Title = "Shortcut Delimiter:" - }; - - var txtDelimiter = new TextField - { - X = Pos.Center (), Width = 2, Text = Key.Separator.ToString () - }; - - - var frmMenu = new FrameView { Y = 7, Width = Dim.Percent (50), Height = Dim.Fill (), Title = "Menus:" }; - - var btnAddMenuBar = new Button { Y = 1, Text = "Add a MenuBar" }; - frmMenu.Add (btnAddMenuBar); - - var btnMenuBarUp = new Button { X = Pos.Center (), Text = Glyphs.UpArrow.ToString () }; - frmMenu.Add (btnMenuBarUp); - - var btnMenuBarDown = new Button { X = Pos.Center (), Y = Pos.Bottom (btnMenuBarUp), Text = Glyphs.DownArrow.ToString () }; - frmMenu.Add (btnMenuBarDown); - - var btnRemoveMenuBar = new Button { Y = 1, Text = "Remove a MenuBar" }; - - btnRemoveMenuBar.X = Pos.AnchorEnd (0) - (Pos.Right (btnRemoveMenuBar) - Pos.Left (btnRemoveMenuBar)); - frmMenu.Add (btnRemoveMenuBar); - - var btnPrevious = new Button - { - X = Pos.Left (btnAddMenuBar), Y = Pos.Top (btnAddMenuBar) + 2, Text = Glyphs.LeftArrow.ToString () - }; - frmMenu.Add (btnPrevious); - - var btnAdd = new Button { Y = Pos.Top (btnPrevious) + 2, Text = " Add " }; - btnAdd.X = Pos.AnchorEnd (); - frmMenu.Add (btnAdd); - - var btnNext = new Button { X = Pos.X (btnAdd), Y = Pos.Top (btnPrevious), Text = Glyphs.RightArrow.ToString () }; - frmMenu.Add (btnNext); - - var lblMenuBar = new Label - { - SchemeName = "Dialog", - TextAlignment = Alignment.Center, - X = Pos.Right (btnPrevious) + 1, - Y = Pos.Top (btnPrevious), - - Width = Dim.Fill () - Dim.Func (_ => btnAdd.Frame.Width + 1), - Height = 1 - }; - - lblMenuBar.TextChanged += (s, e) => - { - if (lblMenuBar.Text.Contains ("_")) - { - lblMenuBar.Text = lblMenuBar.Text.Replace ("_", ""); - } - }; - frmMenu.Add (lblMenuBar); - lblMenuBar.WantMousePositionReports = true; - lblMenuBar.CanFocus = true; - - var lblParent = new Label - { - TextAlignment = Alignment.Center, - X = Pos.Right (btnPrevious) + 1, - Y = Pos.Top (btnPrevious) + 1, - - Width = Dim.Fill () - Dim.Width (btnAdd) - 1 - }; - frmMenu.Add (lblParent); - - var btnPreviowsParent = new Button - { - X = Pos.Left (btnAddMenuBar), Y = Pos.Top (btnPrevious) + 1, Text = ".." - }; - frmMenu.Add (btnPreviowsParent); - - _lstMenus = new () - { - SchemeName = "Dialog", - X = Pos.Right (btnPrevious) + 1, - Y = Pos.Top (btnPrevious) + 2, - Width = lblMenuBar.Width, - Height = Dim.Fill (), - Source = new ListWrapper ([]) - }; - frmMenu.Add (_lstMenus); - - //lblMenuBar.TabIndex = btnPrevious.TabIndex + 1; - //_lstMenus.TabIndex = lblMenuBar.TabIndex + 1; - //btnNext.TabIndex = _lstMenus.TabIndex + 1; - //btnAdd.TabIndex = btnNext.TabIndex + 1; - - var btnRemove = new Button { X = Pos.Left (btnAdd), Y = Pos.Top (btnAdd) + 1, Text = "Remove" }; - frmMenu.Add (btnRemove); - - var btnUp = new Button { X = Pos.Right (_lstMenus) + 2, Y = Pos.Top (btnRemove) + 2, Text = Glyphs.UpArrow.ToString () }; - frmMenu.Add (btnUp); - - var btnDown = new Button { X = Pos.Right (_lstMenus) + 2, Y = Pos.Top (btnUp) + 1, Text = Glyphs.DownArrow.ToString () }; - frmMenu.Add (btnDown); - - Add (frmMenu); - - var frmMenuDetails = new DynamicMenuBarDetails - { - X = Pos.Right (frmMenu), - Y = Pos.Top (frmMenu), - Width = Dim.Fill (), - Height = Dim.Fill (2), - Title = "Menu Details:" - }; - Add (frmMenuDetails); - - btnMenuBarUp.Accepting += (s, e) => - { - int i = _currentSelectedMenuBar; - - MenuBarItem menuItem = _menuBar != null && _menuBar.Menus.Length > 0 - ? _menuBar.Menus [i] - : null; - - if (menuItem != null) - { - MenuBarItem [] menus = _menuBar.Menus; - - if (i > 0) - { - menus [i] = menus [i - 1]; - menus [i - 1] = menuItem; - _currentSelectedMenuBar = i - 1; - _menuBar.SetNeedsDraw (); - } - } - }; - - btnMenuBarDown.Accepting += (s, e) => - { - int i = _currentSelectedMenuBar; - - MenuBarItem menuItem = _menuBar != null && _menuBar.Menus.Length > 0 - ? _menuBar.Menus [i] - : null; - - if (menuItem != null) - { - MenuBarItem [] menus = _menuBar.Menus; - - if (i < menus.Length - 1) - { - menus [i] = menus [i + 1]; - menus [i + 1] = menuItem; - _currentSelectedMenuBar = i + 1; - _menuBar.SetNeedsDraw (); - } - } - }; - - btnUp.Accepting += (s, e) => - { - int i = _lstMenus.SelectedItem.Value; - MenuItem menuItem = DataContext.Menus.Count > 0 ? DataContext.Menus [i].MenuItem : null; - - if (menuItem != null) - { - MenuItem [] childrens = ((MenuBarItem)_currentMenuBarItem).Children; - - if (i > 0) - { - childrens [i] = childrens [i - 1]; - childrens [i - 1] = menuItem; - DataContext.Menus [i] = DataContext.Menus [i - 1]; - - DataContext.Menus [i - 1] = - new () { Title = menuItem.Title, MenuItem = menuItem }; - _lstMenus.SelectedItem = i - 1; - } - } - }; - - btnDown.Accepting += (s, e) => - { - int i = _lstMenus.SelectedItem.Value; - MenuItem menuItem = DataContext.Menus.Count > 0 ? DataContext.Menus [i].MenuItem : null; - - if (menuItem != null) - { - MenuItem [] childrens = ((MenuBarItem)_currentMenuBarItem).Children; - - if (i < childrens.Length - 1) - { - childrens [i] = childrens [i + 1]; - childrens [i + 1] = menuItem; - DataContext.Menus [i] = DataContext.Menus [i + 1]; - - DataContext.Menus [i + 1] = - new () { Title = menuItem.Title, MenuItem = menuItem }; - _lstMenus.SelectedItem = i + 1; - } - } - }; - - btnPreviowsParent.Accepting += (s, e) => - { - if (_currentMenuBarItem != null && _currentMenuBarItem.Parent != null) - { - MenuItem mi = _currentMenuBarItem; - _currentMenuBarItem = _currentMenuBarItem.Parent as MenuBarItem; - SetListViewSource (_currentMenuBarItem, true); - int i = ((MenuBarItem)_currentMenuBarItem).GetChildrenIndex (mi); - - if (i > -1) - { - _lstMenus.SelectedItem = i; - } - - if (_currentMenuBarItem.Parent != null) - { - DataContext.Parent = _currentMenuBarItem.Title; - } - else - { - DataContext.Parent = string.Empty; - } - } - else - { - DataContext.Parent = string.Empty; - } - }; - - var btnOk = new Button { X = Pos.Right (frmMenu) + 20, Y = Pos.Bottom (frmMenuDetails), Text = "Ok" }; - Add (btnOk); - - var btnCancel = new Button { X = Pos.Right (btnOk) + 3, Y = Pos.Top (btnOk), Text = "Cancel" }; - btnCancel.Accepting += (s, e) => { SetFrameDetails (_currentEditMenuBarItem); }; - Add (btnCancel); - - txtDelimiter.TextChanging += (s, e) => - { - if (!string.IsNullOrEmpty (e.Result)) - { - Key.Separator = e.Result.ToRunes () [0]; - } - else - { - e.Handled = true; - txtDelimiter.SelectAll (); - } - }; - txtDelimiter.TextChanged += (s, _) => - { - txtDelimiter.SelectAll (); - SetFrameDetails (); - }; - frmDelimiter.Add (txtDelimiter); - txtDelimiter.SelectAll (); - Add (frmDelimiter); - - _lstMenus.SelectedItemChanged += (s, e) => { SetFrameDetails (); }; - - btnOk.Accepting += (s, e) => - { - if (string.IsNullOrEmpty (frmMenuDetails.TextTitle.Text) && _currentEditMenuBarItem != null) - { - MessageBox.ErrorQuery ("Invalid title", "Must enter a valid title!.", "Ok"); - } - else if (_currentEditMenuBarItem != null) - { - var menuItem = new DynamicMenuItem - { - Title = frmMenuDetails.TextTitle.Text, - Help = frmMenuDetails.TextHelp.Text, - Action = frmMenuDetails.TextAction.Text, - HotKey = frmMenuDetails.TextHotKey.Text, - IsTopLevel = frmMenuDetails.CkbIsTopLevel?.CheckedState == CheckState.Checked, - HasSubMenu = frmMenuDetails.CkbSubMenu?.CheckedState == CheckState.Checked, - CheckStyle = frmMenuDetails.OsChkStyle.Value == 0 - ? MenuItemCheckStyle.NoCheck - : frmMenuDetails.OsChkStyle.Value == 1 - ? MenuItemCheckStyle.Checked - : MenuItemCheckStyle.Radio, - ShortcutKey = frmMenuDetails.TextShortcutKey.Text - }; - UpdateMenuItem (_currentEditMenuBarItem, menuItem, _lstMenus.SelectedItem.Value); - } - }; - - btnAdd.Accepting += (s, e) => - { - if (MenuBar == null) - { - MessageBox.ErrorQuery ("Menu Bar Error", "Must add a MenuBar first!", "Ok"); - btnAddMenuBar.SetFocus (); - - return; - } - - var frameDetails = new DynamicMenuBarDetails (null, _currentMenuBarItem != null); - DynamicMenuItem item = frameDetails.EnterMenuItem (); - - if (item == null) - { - return; - } - - if (_currentMenuBarItem is not MenuBarItem) - { - var parent = _currentMenuBarItem.Parent as MenuBarItem; - int idx = parent.GetChildrenIndex (_currentMenuBarItem); - - _currentMenuBarItem = new MenuBarItem ( - _currentMenuBarItem.Title, - new MenuItem [] { }, - _currentMenuBarItem.Parent - ); - _currentMenuBarItem.CheckType = item.CheckStyle; - parent.Children [idx] = _currentMenuBarItem; - } - else - { - MenuItem newMenu = CreateNewMenu (item, _currentMenuBarItem); - var menuBarItem = _currentMenuBarItem as MenuBarItem; - menuBarItem.AddMenuBarItem (MenuBar, newMenu); - - - DataContext.Menus.Add (new () { Title = newMenu.Title, MenuItem = newMenu }); - _lstMenus.MoveDown (); - } - }; - - btnRemove.Accepting += (s, e) => - { - MenuItem menuItem = (DataContext.Menus.Count > 0 && _lstMenus.SelectedItem is {} selectedItem - ? DataContext.Menus [selectedItem].MenuItem - : _currentEditMenuBarItem); - - if (menuItem != null) - { - menuItem.RemoveMenuItem (); - - if (_currentEditMenuBarItem == menuItem) - { - _currentEditMenuBarItem = null; - - if (menuItem.Parent is null) - { - _currentSelectedMenuBar = Math.Max (Math.Min (_currentSelectedMenuBar, _menuBar.Menus.Length - 1), 0); - } - - SelectCurrentMenuBarItem (); - } - - if (_lstMenus.SelectedItem is {} selected) - { - DataContext.Menus?.RemoveAt (selected); - } - - if (_lstMenus.Source.Count > 0 && _lstMenus.SelectedItem > _lstMenus.Source.Count - 1) - { - _lstMenus.SelectedItem = _lstMenus.Source.Count - 1; - } - - if (_menuBar.Menus.Length == 0) - { - RemoveMenuBar (); - } - - _lstMenus.SetNeedsDraw (); - SetFrameDetails (); - } - }; - - _lstMenus.OpenSelectedItem += (s, e) => - { - _currentMenuBarItem = DataContext.Menus [e.Item.Value].MenuItem; - - if (!(_currentMenuBarItem is MenuBarItem)) - { - MessageBox.ErrorQuery ("Menu Open Error", "Must allows sub menus first!", "Ok"); - - return; - } - - DataContext.Parent = _currentMenuBarItem.Title; - DataContext.Menus = new (); - SetListViewSource (_currentMenuBarItem, true); - MenuItem menuBarItem = DataContext.Menus.Count > 0 ? DataContext.Menus [0].MenuItem : null; - SetFrameDetails (menuBarItem); - }; - - _lstMenus.HasFocusChanging += (s, e) => - { - MenuItem menuBarItem = _lstMenus.SelectedItem is {} selectedItem && DataContext.Menus.Count > 0 - ? DataContext.Menus [selectedItem].MenuItem - : null; - SetFrameDetails (menuBarItem); - }; - - btnNext.Accepting += (s, e) => - { - if (_menuBar != null && _currentSelectedMenuBar + 1 < _menuBar.Menus.Length) - { - _currentSelectedMenuBar++; - } - - SelectCurrentMenuBarItem (); - }; - - btnPrevious.Accepting += (s, e) => - { - if (_currentSelectedMenuBar - 1 > -1) - { - _currentSelectedMenuBar--; - } - - SelectCurrentMenuBarItem (); - }; - - lblMenuBar.HasFocusChanging += (s, e) => - { - if (_menuBar?.Menus != null) - { - _currentMenuBarItem = _menuBar.Menus [_currentSelectedMenuBar]; - SetFrameDetails (_menuBar.Menus [_currentSelectedMenuBar]); - } - }; - - btnAddMenuBar.Accepting += (s, e) => - { - var frameDetails = new DynamicMenuBarDetails (null); - DynamicMenuItem item = frameDetails.EnterMenuItem (); - - if (item == null) - { - return; - } - - if (MenuBar == null) - { - _menuBar = new (); - Add (_menuBar); - } - - var newMenu = CreateNewMenu (item) as MenuBarItem; - newMenu.AddMenuBarItem (MenuBar); - - _currentMenuBarItem = newMenu; - _currentMenuBarItem.CheckType = item.CheckStyle; - - if (Key.TryParse (item.ShortcutKey, out Key key)) - { - _currentMenuBarItem.ShortcutKey = key; - } - - _currentSelectedMenuBar = _menuBar.Menus.Length - 1; - _menuBar.Menus [_currentSelectedMenuBar] = newMenu; - lblMenuBar.Text = newMenu.Title; - SetListViewSource (_currentMenuBarItem, true); - SetFrameDetails (_menuBar.Menus [_currentSelectedMenuBar]); - _menuBar.SetNeedsDraw (); - }; - - btnRemoveMenuBar.Accepting += (s, e) => - { - if (_menuBar == null) - { - return; - } - - if (_menuBar != null && _menuBar.Menus.Length > 0) - { - _currentMenuBarItem.RemoveMenuItem (); - - - - if (_currentSelectedMenuBar - 1 >= 0 && _menuBar.Menus.Length > 0) - { - _currentSelectedMenuBar--; - } - - _currentMenuBarItem = _menuBar.Menus?.Length > 0 - ? _menuBar.Menus [_currentSelectedMenuBar] - : null; - } - - RemoveMenuBar (); - - SetListViewSource (_currentMenuBarItem, true); - SetFrameDetails (); - }; - - void RemoveMenuBar () - { - if (MenuBar != null && _currentMenuBarItem == null && _menuBar.Menus.Length == 0) - { - Remove (_menuBar); - _menuBar.Dispose (); - _menuBar = null; - DataContext.Menus = new (); - _currentMenuBarItem = null; - _currentSelectedMenuBar = -1; - lblMenuBar.Text = string.Empty; - } - else - { - lblMenuBar.Text = _menuBar.Menus [_currentSelectedMenuBar].Title; - } - } - - SetFrameDetails (); - - var ustringConverter = new UStringValueConverter (); - ListWrapperConverter listWrapperConverter = new ListWrapperConverter (); - - var bdgMenuBar = new Binding (this, "MenuBar", lblMenuBar, "Text", ustringConverter); - var bdgParent = new Binding (this, "Parent", lblParent, "Text", ustringConverter); - var bdgMenus = new Binding (this, "Menus", _lstMenus, "Source", listWrapperConverter); - - void SetFrameDetails (MenuItem menuBarItem = null) - { - MenuItem menuItem; - - if (menuBarItem == null) - { - menuItem = _lstMenus.SelectedItem is {} selectedItem && DataContext.Menus.Count > 0 - ? DataContext.Menus [selectedItem].MenuItem - : _currentEditMenuBarItem; - } - else - { - menuItem = menuBarItem; - } - - _currentEditMenuBarItem = menuItem; - frmMenuDetails.EditMenuBarItem (menuItem); - bool f = btnOk.Enabled == frmMenuDetails.Enabled; - - if (!f) - { - btnOk.Enabled = frmMenuDetails.Enabled; - btnCancel.Enabled = frmMenuDetails.Enabled; - } - } - - void SelectCurrentMenuBarItem () - { - MenuBarItem menuBarItem = null; - - if (_menuBar?.Menus is { Length: > 0 }) - { - menuBarItem = _menuBar.Menus [_currentSelectedMenuBar]; - lblMenuBar.Text = menuBarItem.Title; - } - - SetFrameDetails (menuBarItem); - _currentMenuBarItem = menuBarItem; - DataContext.Menus = new (); - SetListViewSource (_currentMenuBarItem, true); - lblParent.Text = string.Empty; - - if (_currentMenuBarItem is null) - { - lblMenuBar.Text = string.Empty; - } - } - - void SetListViewSource (MenuItem currentMenuBarItem, bool fill = false) - { - DataContext.Menus = []; - var menuBarItem = currentMenuBarItem as MenuBarItem; - - if (menuBarItem != null && menuBarItem?.Children == null) - { - return; - } - - if (!fill) - { - return; - } - - if (menuBarItem != null) - { - foreach (MenuItem child in menuBarItem?.Children) - { - var m = new DynamicMenuItemList { Title = child.Title, MenuItem = child }; - DataContext.Menus.Add (m); - } - } - } - - MenuItem CreateNewMenu (DynamicMenuItem item, MenuItem parent = null) - { - MenuItem newMenu; - - if (item.HasSubMenu) - { - newMenu = new MenuBarItem (item.Title, new MenuItem [] { }, parent); - } - else if (parent != null) - { - newMenu = new (item.Title, item.Help, null, null, parent); - newMenu.CheckType = item.CheckStyle; - newMenu.Action = frmMenuDetails.CreateAction (newMenu, item); - - if (Key.TryParse (item.ShortcutKey, out Key key)) - { - newMenu.ShortcutKey = key; - } - newMenu.AllowNullChecked = item.AllowNullChecked; - } - else if (item.IsTopLevel) - { - newMenu = new MenuBarItem (item.Title, item.Help, null); - newMenu.Action = frmMenuDetails.CreateAction (newMenu, item); - - if (Key.TryParse (item.ShortcutKey, out Key key)) - { - newMenu.ShortcutKey = key; - } - } - else - { - newMenu = new MenuBarItem (item.Title, item.Help, null); - - ((MenuBarItem)newMenu).Children [0].Action = - frmMenuDetails.CreateAction (newMenu, item); - - if (Key.TryParse (item.ShortcutKey, out Key key)) - { - ((MenuBarItem)newMenu).Children [0].ShortcutKey = key; - } - } - - return newMenu; - } - - void UpdateMenuItem (MenuItem currentEditMenuBarItem, DynamicMenuItem menuItem, int index) - { - currentEditMenuBarItem.Title = menuItem.Title; - currentEditMenuBarItem.Help = menuItem.Help; - currentEditMenuBarItem.CheckType = menuItem.CheckStyle; - - if (currentEditMenuBarItem.Parent is MenuBarItem parent - && parent.Children.Length == 1 - && currentEditMenuBarItem.CheckType == MenuItemCheckStyle.Radio) - { - currentEditMenuBarItem.Checked = true; - } - - if (menuItem.IsTopLevel && currentEditMenuBarItem is MenuBarItem) - { - ((MenuBarItem)currentEditMenuBarItem).Children = null; - - currentEditMenuBarItem.Action = - frmMenuDetails.CreateAction (currentEditMenuBarItem, menuItem); - - if (Key.TryParse (menuItem.ShortcutKey, out Key key)) - { - currentEditMenuBarItem.ShortcutKey = key; - } - - SetListViewSource (currentEditMenuBarItem, true); - } - else if (menuItem.HasSubMenu) - { - currentEditMenuBarItem.Action = null; - - if (currentEditMenuBarItem is MenuBarItem && ((MenuBarItem)currentEditMenuBarItem).Children == null) - { - ((MenuBarItem)currentEditMenuBarItem).Children = new MenuItem [] { }; - } - else if (currentEditMenuBarItem.Parent != null) - { - frmMenuDetails.UpdateParent (ref currentEditMenuBarItem); - } - else - { - currentEditMenuBarItem = - new MenuBarItem ( - currentEditMenuBarItem.Title, - new MenuItem [] { }, - currentEditMenuBarItem.Parent - ); - } - - SetListViewSource (currentEditMenuBarItem, true); - } - else if (currentEditMenuBarItem is MenuBarItem && currentEditMenuBarItem.Parent != null) - { - frmMenuDetails.UpdateParent (ref currentEditMenuBarItem); - - currentEditMenuBarItem = new ( - menuItem.Title, - menuItem.Help, - frmMenuDetails.CreateAction (currentEditMenuBarItem, menuItem), - null, - currentEditMenuBarItem.Parent - ); - } - else - { - if (currentEditMenuBarItem is MenuBarItem) - { - ((MenuBarItem)currentEditMenuBarItem).Children = null; - DataContext.Menus = new (); - } - - currentEditMenuBarItem.Action = - frmMenuDetails.CreateAction (currentEditMenuBarItem, menuItem); - - if (Key.TryParse (menuItem.ShortcutKey, out Key key)) - { - currentEditMenuBarItem.ShortcutKey = key; - } - } - - if (currentEditMenuBarItem.Parent == null) - { - DataContext.MenuBar = currentEditMenuBarItem.Title; - } - else - { - if (DataContext.Menus.Count == 0) - { - DataContext.Menus.Add ( - new () - { - Title = currentEditMenuBarItem.Title, MenuItem = currentEditMenuBarItem - } - ); - } - - DataContext.Menus [index] = - new () - { - Title = currentEditMenuBarItem.Title, MenuItem = currentEditMenuBarItem - }; - } - - currentEditMenuBarItem.CheckType = menuItem.CheckStyle; - SetFrameDetails (currentEditMenuBarItem); - } - - //_frmMenuDetails.Initialized += (s, e) => _frmMenuDetails.Enabled = false; - } - - public DynamicMenuItemModel DataContext { get; set; } - } - - public class DynamicMenuItem - { - public string Action { get; set; } = string.Empty; - public bool AllowNullChecked { get; set; } - public MenuItemCheckStyle CheckStyle { get; set; } - public bool HasSubMenu { get; set; } - public string Help { get; set; } = string.Empty; - public bool IsTopLevel { get; set; } - public string HotKey { get; set; } - public string ShortcutKey { get; set; } - public string Title { get; set; } = "_New"; - } - - public class DynamicMenuItemList - { - public MenuItem MenuItem { get; set; } - public string Title { get; set; } - public override string ToString () { return $"{Title}, {MenuItem.HotKey}, {MenuItem.ShortcutKey} "; } - } - - public class DynamicMenuItemModel : INotifyPropertyChanged - { - private string _menuBar; - private ObservableCollection _menus; - private string _parent; - public DynamicMenuItemModel () { Menus = []; } - - public string MenuBar - { - get => _menuBar; - set - { - if (value == _menuBar) - { - return; - } - - _menuBar = value; - - PropertyChanged?.Invoke ( - this, - new (GetPropertyName ()) - ); - } - } - - public ObservableCollection Menus - { - get => _menus; - set - { - if (value == _menus) - { - return; - } - - _menus = value; - - PropertyChanged?.Invoke ( - this, - new (GetPropertyName ()) - ); - } - } - - public string Parent - { - get => _parent; - set - { - if (value == _parent) - { - return; - } - - _parent = value; - - PropertyChanged?.Invoke ( - this, - new (GetPropertyName ()) - ); - } - } - - public event PropertyChangedEventHandler PropertyChanged; - public string GetPropertyName ([CallerMemberName] string propertyName = null) { return propertyName; } - } - - public interface IValueConverter - { - object Convert (object value, object parameter = null); - } - - public class ListWrapperConverter : IValueConverter - { - public object Convert (object value, object parameter = null) { return new ListWrapper ((ObservableCollection)value); } - } - - public class UStringValueConverter : IValueConverter - { - public object Convert (object value, object parameter = null) - { - byte [] data = Encoding.ASCII.GetBytes (value.ToString () ?? string.Empty); - - return StringExtensions.ToString (data); - } - } -} diff --git a/Examples/UICatalog/Scenarios/Editor.cs b/Examples/UICatalog/Scenarios/Editor.cs index b005fec55..3b2e13813 100644 --- a/Examples/UICatalog/Scenarios/Editor.cs +++ b/Examples/UICatalog/Scenarios/Editor.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; +#nullable enable + using System.Diagnostics; using System.Globalization; -using System.IO; -using System.Linq; using System.Text; using System.Text.RegularExpressions; -using System.Threading; -using static UICatalog.Scenarios.DynamicMenuBar; namespace UICatalog.Scenarios; @@ -21,313 +17,177 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Menus")] public class Editor : Scenario { - private Window _appWindow; - private List _cultureInfos; + private Window? _appWindow; + private List? _cultureInfos; private string _fileName = "demo.txt"; private bool _forceMinimumPosToZero = true; private bool _matchCase; private bool _matchWholeWord; - private MenuItem _miForceMinimumPosToZero; - private byte [] _originalText; + private CheckBox? _miForceMinimumPosToZeroCheckBox; + private byte []? _originalText; private bool _saved = true; - private TabView _tabView; - private string _textToFind; - private string _textToReplace; - private TextView _textView; - private FindReplaceWindow _findReplaceWindow; + private TabView? _tabView; + private string _textToFind = string.Empty; + private string _textToReplace = string.Empty; + private TextView? _textView; + private FindReplaceWindow? _findReplaceWindow; public override void Main () { - // Init Application.Init (); - // Setup - Create a top-level application window and configure it. _appWindow = new () { - //Title = GetQuitKeyAndName (), Title = _fileName ?? "Untitled", BorderStyle = LineStyle.None }; - _cultureInfos = Application.SupportedCultures; + _cultureInfos = Application.SupportedCultures?.ToList (); _textView = new () { X = 0, Y = 1, Width = Dim.Fill (), - Height = Dim.Fill (1), + Height = Dim.Fill (1) }; - CreateDemoFile (_fileName); + CreateDemoFile (_fileName!); LoadFile (); _appWindow.Add (_textView); - var menu = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ("_New", "", () => New ()), - new ("_Open", "", () => Open ()), - new ("_Save", "", () => Save ()), - new ("_Save As", "", () => SaveAs ()), - new ("_Close", "", () => CloseFile ()), - null, - new ("_Quit", "", () => Quit ()) - } - ), - new ( - "_Edit", - new MenuItem [] - { - new ( - "_Copy", - "", - () => Copy (), - null, - null, - KeyCode.CtrlMask | KeyCode.C - ), - new ( - "C_ut", - "", - () => Cut (), - null, - null, - KeyCode.CtrlMask | KeyCode.W - ), - new ( - "_Paste", - "", - () => Paste (), - null, - null, - KeyCode.CtrlMask | KeyCode.Y - ), - null, - new ( - "_Find", - "", - () => Find (), - null, - null, - KeyCode.CtrlMask | KeyCode.S - ), - new ( - "Find _Next", - "", - () => FindNext (), - null, - null, - KeyCode.CtrlMask - | KeyCode.ShiftMask - | KeyCode.S - ), - new ( - "Find P_revious", - "", - () => FindPrevious (), - null, - null, - KeyCode.CtrlMask - | KeyCode.ShiftMask - | KeyCode.AltMask - | KeyCode.S - ), - new ( - "_Replace", - "", - () => Replace (), - null, - null, - KeyCode.CtrlMask | KeyCode.R - ), - new ( - "Replace Ne_xt", - "", - () => ReplaceNext (), - null, - null, - KeyCode.CtrlMask - | KeyCode.ShiftMask - | KeyCode.R - ), - new ( - "Replace Pre_vious", - "", - () => ReplacePrevious (), - null, - null, - KeyCode.CtrlMask - | KeyCode.ShiftMask - | KeyCode.AltMask - | KeyCode.R - ), - new ( - "Replace _All", - "", - () => ReplaceAll (), - null, - null, - KeyCode.CtrlMask - | KeyCode.ShiftMask - | KeyCode.AltMask - | KeyCode.A - ), - null, - new ( - "_Select All", - "", - () => SelectAll (), - null, - null, - KeyCode.CtrlMask | KeyCode.T - ) - } - ), - new ("_ScrollBarView", CreateKeepChecked ()), - new ("_Cursor", CreateCursorRadio ()), - new ( - "Forma_t", - new [] - { - CreateWrapChecked (), - CreateAutocomplete (), - CreateAllowsTabChecked (), - CreateReadOnlyChecked (), - CreateUseSameRuneTypeForWords (), - CreateSelectWordOnlyOnDoubleClick (), - new MenuItem ( - "Colors", - "", - () => _textView.PromptForColors (), - null, - null, - KeyCode.CtrlMask | KeyCode.L - ) - } - ), - new ( - "_Responder", - new [] { CreateCanFocusChecked (), CreateEnabledChecked (), CreateVisibleChecked () } - ), - new ( - "Conte_xtMenu", - new [] - { - _miForceMinimumPosToZero = new ( - "ForceMinimumPosTo_Zero", - "", - () => - { - //_miForceMinimumPosToZero.Checked = - // _forceMinimumPosToZero = - // !_forceMinimumPosToZero; + // MenuBar + MenuBar menu = new (); - //_textView.ContextMenu.ForceMinimumPosToZero = - // _forceMinimumPosToZero; - } - ) - { - CheckType = MenuItemCheckStyle.Checked, - Checked = _forceMinimumPosToZero - }, - new MenuBarItem ("_Languages", GetSupportedCultures ()) - } - ) - ] + menu.Add ( + new MenuBarItem ( + "_File", + [ + new MenuItem { Title = "_New", Action = () => New () }, + new MenuItem { Title = "_Open", Action = Open }, + new MenuItem { Title = "_Save", Action = () => Save () }, + new MenuItem { Title = "_Save As", Action = () => SaveAs () }, + new MenuItem { Title = "_Close", Action = CloseFile }, + new MenuItem { Title = "_Quit", Action = Quit } + ] + ) + ); + + menu.Add ( + new MenuBarItem ( + "_Edit", + [ + new MenuItem { Title = "_Copy", Key = Key.C.WithCtrl, Action = Copy }, + new MenuItem { Title = "C_ut", Key = Key.W.WithCtrl, Action = Cut }, + new MenuItem { Title = "_Paste", Key = Key.Y.WithCtrl, Action = Paste }, + new MenuItem { Title = "_Find", Key = Key.S.WithCtrl, Action = Find }, + new MenuItem { Title = "Find _Next", Key = Key.S.WithCtrl.WithShift, Action = FindNext }, + new MenuItem { Title = "Find P_revious", Key = Key.S.WithCtrl.WithShift.WithAlt, Action = FindPrevious }, + new MenuItem { Title = "_Replace", Key = Key.R.WithCtrl, Action = Replace }, + new MenuItem { Title = "Replace Ne_xt", Key = Key.R.WithCtrl.WithShift, Action = ReplaceNext }, + new MenuItem { Title = "Replace Pre_vious", Key = Key.R.WithCtrl.WithShift.WithAlt, Action = ReplacePrevious }, + new MenuItem { Title = "Replace _All", Key = Key.A.WithCtrl.WithShift.WithAlt, Action = ReplaceAll }, + new MenuItem { Title = "_Select All", Key = Key.T.WithCtrl, Action = SelectAll } + ] + ) + ); + + menu.Add (new MenuBarItem ("_ScrollBars", CreateScrollBarsMenu ())); + menu.Add (new MenuBarItem ("_Cursor", CreateCursorRadio ())); + + menu.Add ( + new MenuBarItem ( + "Forma_t", + [ + CreateWrapChecked (), + CreateAutocomplete (), + CreateAllowsTabChecked (), + CreateReadOnlyChecked (), + CreateUseSameRuneTypeForWords (), + CreateSelectWordOnlyOnDoubleClick (), + new MenuItem { Title = "Colors", Key = Key.L.WithCtrl, Action = () => _textView?.PromptForColors () } + ] + ) + ); + + menu.Add ( + new MenuBarItem ( + "_View", + [CreateCanFocusChecked (), CreateEnabledChecked (), CreateVisibleChecked ()] + ) + ); + + _miForceMinimumPosToZeroCheckBox = new () + { + Title = "ForceMinimumPosTo_Zero", + CheckedState = _forceMinimumPosToZero ? CheckState.Checked : CheckState.UnChecked }; + _miForceMinimumPosToZeroCheckBox.CheckedStateChanging += (s, e) => + { + _forceMinimumPosToZero = e.Result == CheckState.Checked; + + // Note: PopoverMenu.ForceMinimumPosToZero property doesn't exist in v2 + // if (_textView?.ContextMenu is { }) + // { + // _textView.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; + // } + }; + + menu.Add ( + new MenuBarItem ( + "Conte_xtMenu", + [ + new MenuItem { CommandView = _miForceMinimumPosToZeroCheckBox }, + new MenuBarItem ("_Languages", GetSupportedCultures ()) + ] + ) + ); + _appWindow.Add (menu); - var siCursorPosition = new Shortcut (KeyCode.Null, "", null); + Shortcut siCursorPosition = new (Key.Empty, "", null); - var statusBar = new StatusBar ( - new [] - { - new (Application.QuitKey, $"Quit", Quit), - new (Key.F2, "Open", Open), - new (Key.F3, "Save", () => Save ()), - new (Key.F4, "Save As", () => SaveAs ()), - new (Key.Empty, $"OS Clipboard IsSupported : {Clipboard.IsSupported}", null), - siCursorPosition, - } - ) + StatusBar statusBar = new ( + [ + new (Application.QuitKey, "Quit", Quit), + new (Key.F2, "Open", Open), + new (Key.F3, "Save", () => Save ()), + new (Key.F4, "Save As", () => SaveAs ()), + new (Key.Empty, $"OS Clipboard IsSupported : {Clipboard.IsSupported}", null), + siCursorPosition + ] + ) { AlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast }; _textView.VerticalScrollBar.AutoShow = false; - _textView.UnwrappedCursorPosition += (s, e) => - { - siCursorPosition.Title = $"Ln {e.Y + 1}, Col {e.X + 1}"; - }; + + _textView.UnwrappedCursorPosition += (s, e) => { siCursorPosition.Title = $"Ln {e.Y + 1}, Col {e.X + 1}"; }; _appWindow.Add (statusBar); - //_scrollBar = new (_textView, true); - - //_scrollBar.ChangedPosition += (s, e) => - // { - // _textView.TopRow = _scrollBar.Position; - - // if (_textView.TopRow != _scrollBar.Position) - // { - // _scrollBar.Position = _textView.TopRow; - // } - - // _textView.SetNeedsDraw (); - // }; - - //_scrollBar.OtherScrollBarView.ChangedPosition += (s, e) => - // { - // _textView.LeftColumn = _scrollBar.OtherScrollBarView.Position; - - // if (_textView.LeftColumn != _scrollBar.OtherScrollBarView.Position) - // { - // _scrollBar.OtherScrollBarView.Position = _textView.LeftColumn; - // } - - // _textView.SetNeedsDraw (); - // }; - - //_textView.DrawingContent += (s, e) => - // { - // _scrollBar.Size = _textView.Lines; - // _scrollBar.Position = _textView.TopRow; - - // if (_scrollBar.OtherScrollBarView != null) - // { - // _scrollBar.OtherScrollBarView.Size = _textView.Maxlength; - // _scrollBar.OtherScrollBarView.Position = _textView.LeftColumn; - // } - // }; - - _appWindow.Closed += (s, e) => Thread.CurrentThread.CurrentUICulture = new ("en-US"); CreateFindReplace (); - // Run - Start the application. Application.Run (_appWindow); _appWindow.Dispose (); - - // Shutdown - Calling Application.Shutdown is required. Application.Shutdown (); - } private bool CanCloseFile () { + if (_textView is null || _originalText is null || _appWindow is null) + { + return true; + } + if (_textView.Text == Encoding.Unicode.GetString (_originalText)) { - //System.Diagnostics.Debug.Assert (!_textView.IsDirty); return true; } @@ -356,7 +216,7 @@ public class Editor : Scenario private void CloseFile () { - if (!CanCloseFile ()) + if (!CanCloseFile () || _textView is null) { return; } @@ -374,6 +234,11 @@ public class Editor : Scenario private void ContinueFind (bool next = true, bool replace = false) { + if (_textView is null) + { + return; + } + if (!replace && string.IsNullOrEmpty (_textToFind)) { Find (); @@ -383,7 +248,7 @@ public class Editor : Scenario if (replace && (string.IsNullOrEmpty (_textToFind) - || (_findReplaceWindow == null && string.IsNullOrEmpty (_textToReplace)))) + || (_findReplaceWindow is null && string.IsNullOrEmpty (_textToReplace)))) { Replace (); @@ -454,204 +319,466 @@ public class Editor : Scenario } } - private void Copy () + private void Copy () { _textView?.Copy (); } + + private MenuItem [] CreateScrollBarsMenu () { - if (_textView != null) + if (_textView is null) { - _textView.Copy (); + return []; } + + List menuItems = []; + + // Vertical ScrollBar AutoShow + CheckBox verticalAutoShowCheckBox = new () + { + Title = "_Vertical ScrollBar AutoShow", + CheckedState = _textView.VerticalScrollBar.AutoShow ? CheckState.Checked : CheckState.UnChecked + }; + + verticalAutoShowCheckBox.CheckedStateChanged += (s, e) => + { + _textView.VerticalScrollBar.AutoShow = verticalAutoShowCheckBox.CheckedState == CheckState.Checked; + }; + + MenuItem verticalItem = new () { CommandView = verticalAutoShowCheckBox }; + + verticalItem.Accepting += (s, e) => + { + verticalAutoShowCheckBox.AdvanceCheckState (); + e.Handled = true; + }; + + menuItems.Add (verticalItem); + + // Horizontal ScrollBar AutoShow + CheckBox horizontalAutoShowCheckBox = new () + { + Title = "_Horizontal ScrollBar AutoShow", + CheckedState = _textView.HorizontalScrollBar.AutoShow ? CheckState.Checked : CheckState.UnChecked + }; + + horizontalAutoShowCheckBox.CheckedStateChanged += (s, e) => + { + _textView.HorizontalScrollBar.AutoShow = horizontalAutoShowCheckBox.CheckedState == CheckState.Checked; + }; + + MenuItem horizontalItem = new () { CommandView = horizontalAutoShowCheckBox }; + + horizontalItem.Accepting += (s, e) => + { + horizontalAutoShowCheckBox.AdvanceCheckState (); + e.Handled = true; + }; + + menuItems.Add (horizontalItem); + + return [.. menuItems]; } - private MenuItem CreateAllowsTabChecked () + private MenuItem [] CreateCursorRadio () { - var item = new MenuItem { Title = "Allows Tab" }; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = _textView.AllowsTab; - item.Action += () => { _textView.AllowsTab = (bool)(item.Checked = !item.Checked); }; + if (_textView is null) + { + return []; + } + + List menuItems = []; + List radioGroup = []; + + void AddRadioItem (string title, CursorVisibility visibility) + { + CheckBox checkBox = new () + { + Title = title, + CheckedState = _textView.CursorVisibility == visibility ? CheckState.Checked : CheckState.UnChecked + }; + + radioGroup.Add (checkBox); + + checkBox.CheckedStateChanging += (s, e) => + { + if (e.Result == CheckState.Checked) + { + _textView.CursorVisibility = visibility; + + foreach (CheckBox cb in radioGroup) + { + if (cb != checkBox) + { + cb.CheckedState = CheckState.UnChecked; + } + } + } + }; + + MenuItem item = new () { CommandView = checkBox }; + + item.Accepting += (s, e) => + { + checkBox.AdvanceCheckState (); + e.Handled = true; + }; + + menuItems.Add (item); + } + + AddRadioItem ("_Invisible", CursorVisibility.Invisible); + AddRadioItem ("_Box", CursorVisibility.Box); + AddRadioItem ("_Underline", CursorVisibility.Underline); + + menuItems.Add (new () { Title = "" }); + menuItems.Add (new () { Title = "xTerm :" }); + menuItems.Add (new () { Title = "" }); + + AddRadioItem (" _Default", CursorVisibility.Default); + AddRadioItem (" _Vertical", CursorVisibility.Vertical); + AddRadioItem (" V_ertical Fix", CursorVisibility.VerticalFix); + AddRadioItem (" B_ox Fix", CursorVisibility.BoxFix); + AddRadioItem (" U_nderline Fix", CursorVisibility.UnderlineFix); + + return [.. menuItems]; + } + + private MenuItem [] GetSupportedCultures () + { + if (_cultureInfos is null) + { + return []; + } + + List supportedCultures = []; + List allCheckBoxes = []; + int index = -1; + + void CreateCultureMenuItem (string title, string cultureName, bool isChecked) + { + CheckBox checkBox = new () + { + Title = title, + CheckedState = isChecked ? CheckState.Checked : CheckState.UnChecked + }; + + allCheckBoxes.Add (checkBox); + + checkBox.CheckedStateChanging += (s, e) => + { + if (e.Result == CheckState.Checked) + { + Thread.CurrentThread.CurrentUICulture = new (cultureName); + + foreach (CheckBox cb in allCheckBoxes) + { + cb.CheckedState = cb == checkBox ? CheckState.Checked : CheckState.UnChecked; + } + } + }; + + MenuItem item = new () { CommandView = checkBox }; + + item.Accepting += (s, e) => + { + checkBox.AdvanceCheckState (); + e.Handled = true; + }; + + supportedCultures.Add (item); + } + + foreach (CultureInfo c in _cultureInfos) + { + if (index == -1) + { + CreateCultureMenuItem ("_English", "en-US", Thread.CurrentThread.CurrentUICulture.Name == "en-US"); + index++; + } + + CreateCultureMenuItem ($"_{c.Parent.EnglishName}", c.Name, Thread.CurrentThread.CurrentUICulture.Name == c.Name); + } + + return [.. supportedCultures]; + } + + private MenuItem CreateWrapChecked () + { + if (_textView is null) + { + return new () { Title = "Word Wrap" }; + } + + CheckBox checkBox = new () + { + Title = "Word Wrap", + CheckedState = _textView.WordWrap ? CheckState.Checked : CheckState.UnChecked + }; + + checkBox.CheckedStateChanged += (s, e) => { _textView.WordWrap = checkBox.CheckedState == CheckState.Checked; }; + + MenuItem item = new () { CommandView = checkBox }; + + item.Accepting += (s, e) => + { + checkBox.AdvanceCheckState (); + e.Handled = true; + }; return item; } private MenuItem CreateAutocomplete () { - var singleWordGenerator = new SingleWordSuggestionGenerator (); + if (_textView is null) + { + return new () { Title = "Autocomplete" }; + } + + SingleWordSuggestionGenerator singleWordGenerator = new (); _textView.Autocomplete.SuggestionGenerator = singleWordGenerator; - var auto = new MenuItem (); - auto.Title = "Autocomplete"; - auto.CheckType |= MenuItemCheckStyle.Checked; - auto.Checked = false; + CheckBox checkBox = new () + { + Title = "Autocomplete", + CheckedState = CheckState.UnChecked + }; - auto.Action += () => - { - if ((bool)(auto.Checked = !auto.Checked)) - { - // setup autocomplete with all words currently in the editor - singleWordGenerator.AllSuggestions = - Regex.Matches (_textView.Text, "\\w+") - .Select (s => s.Value) - .Distinct () - .ToList (); - } - else - { - singleWordGenerator.AllSuggestions.Clear (); - } - }; + checkBox.CheckedStateChanged += (s, e) => + { + if (checkBox.CheckedState == CheckState.Checked) + { + singleWordGenerator.AllSuggestions = + Regex.Matches (_textView.Text, "\\w+") + .Select (s => s.Value) + .Distinct () + .ToList (); + } + else + { + singleWordGenerator.AllSuggestions.Clear (); + } + }; - return auto; - } + MenuItem item = new () { CommandView = checkBox }; - private MenuItem CreateCanFocusChecked () - { - var item = new MenuItem { Title = "CanFocus" }; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = _textView.CanFocus; - - item.Action += () => - { - _textView.CanFocus = (bool)(item.Checked = !item.Checked); - - if (_textView.CanFocus) - { - _textView.SetFocus (); - } - }; + item.Accepting += (s, e) => + { + checkBox.AdvanceCheckState (); + e.Handled = true; + }; return item; } - private MenuItem [] CreateCursorRadio () + private MenuItem CreateAllowsTabChecked () { - List menuItems = new (); - - menuItems.Add ( - new ("_Invisible", "", () => SetCursor (CursorVisibility.Invisible)) - { - CheckType = MenuItemCheckStyle.Radio, - Checked = _textView.CursorVisibility - == CursorVisibility.Invisible - } - ); - - menuItems.Add ( - new ("_Box", "", () => SetCursor (CursorVisibility.Box)) - { - CheckType = MenuItemCheckStyle.Radio, - Checked = _textView.CursorVisibility == CursorVisibility.Box - } - ); - - menuItems.Add ( - new ("_Underline", "", () => SetCursor (CursorVisibility.Underline)) - { - CheckType = MenuItemCheckStyle.Radio, - Checked = _textView.CursorVisibility - == CursorVisibility.Underline - } - ); - menuItems.Add (new ("", "", () => { }, () => false)); - menuItems.Add (new ("xTerm :", "", () => { }, () => false)); - menuItems.Add (new ("", "", () => { }, () => false)); - - menuItems.Add ( - new (" _Default", "", () => SetCursor (CursorVisibility.Default)) - { - CheckType = MenuItemCheckStyle.Radio, - Checked = _textView.CursorVisibility - == CursorVisibility.Default - } - ); - - menuItems.Add ( - new (" _Vertical", "", () => SetCursor (CursorVisibility.Vertical)) - { - CheckType = MenuItemCheckStyle.Radio, - Checked = _textView.CursorVisibility - == CursorVisibility.Vertical - } - ); - - menuItems.Add ( - new (" V_ertical Fix", "", () => SetCursor (CursorVisibility.VerticalFix)) - { - CheckType = MenuItemCheckStyle.Radio, - Checked = _textView.CursorVisibility == CursorVisibility.VerticalFix - } - ); - - menuItems.Add ( - new (" B_ox Fix", "", () => SetCursor (CursorVisibility.BoxFix)) - { - CheckType = MenuItemCheckStyle.Radio, - Checked = _textView.CursorVisibility - == CursorVisibility.BoxFix - } - ); - - menuItems.Add ( - new (" U_nderline Fix", "", () => SetCursor (CursorVisibility.UnderlineFix)) - { - CheckType = MenuItemCheckStyle.Radio, - Checked = _textView.CursorVisibility == CursorVisibility.UnderlineFix - } - ); - - void SetCursor (CursorVisibility visibility) + if (_textView is null) { - _textView.CursorVisibility = visibility; - var title = ""; - - switch (visibility) - { - case CursorVisibility.Default: - title = " _Default"; - - break; - case CursorVisibility.Invisible: - title = "_Invisible"; - - break; - case CursorVisibility.Underline: - title = "_Underline"; - - break; - case CursorVisibility.UnderlineFix: - title = " U_nderline Fix"; - - break; - case CursorVisibility.Vertical: - title = " _Vertical"; - - break; - case CursorVisibility.VerticalFix: - title = " V_ertical Fix"; - - break; - case CursorVisibility.Box: - title = "_Box"; - - break; - case CursorVisibility.BoxFix: - title = " B_ox Fix"; - - break; - } - - foreach (MenuItem menuItem in menuItems) - { - menuItem.Checked = menuItem.Title.Equals (title) && visibility == _textView.CursorVisibility; - } + return new () { Title = "Allows Tab" }; } - return menuItems.ToArray (); + CheckBox checkBox = new () + { + Title = "Allows Tab", + CheckedState = _textView.AllowsTab ? CheckState.Checked : CheckState.UnChecked + }; + + checkBox.CheckedStateChanged += (s, e) => { _textView.AllowsTab = checkBox.CheckedState == CheckState.Checked; }; + + MenuItem item = new () { CommandView = checkBox }; + + item.Accepting += (s, e) => + { + checkBox.AdvanceCheckState (); + e.Handled = true; + }; + + return item; + } + + private MenuItem CreateReadOnlyChecked () + { + if (_textView is null) + { + return new () { Title = "Read Only" }; + } + + CheckBox checkBox = new () + { + Title = "Read Only", + CheckedState = _textView.ReadOnly ? CheckState.Checked : CheckState.UnChecked + }; + + checkBox.CheckedStateChanged += (s, e) => { _textView.ReadOnly = checkBox.CheckedState == CheckState.Checked; }; + + MenuItem item = new () { CommandView = checkBox }; + + item.Accepting += (s, e) => + { + checkBox.AdvanceCheckState (); + e.Handled = true; + }; + + return item; + } + + private MenuItem CreateUseSameRuneTypeForWords () + { + if (_textView is null) + { + return new () { Title = "UseSameRuneTypeForWords" }; + } + + CheckBox checkBox = new () + { + Title = "UseSameRuneTypeForWords", + CheckedState = _textView.UseSameRuneTypeForWords ? CheckState.Checked : CheckState.UnChecked + }; + + checkBox.CheckedStateChanged += (s, e) => { _textView.UseSameRuneTypeForWords = checkBox.CheckedState == CheckState.Checked; }; + + MenuItem item = new () { CommandView = checkBox }; + + item.Accepting += (s, e) => + { + checkBox.AdvanceCheckState (); + e.Handled = true; + }; + + return item; + } + + private MenuItem CreateSelectWordOnlyOnDoubleClick () + { + if (_textView is null) + { + return new () { Title = "SelectWordOnlyOnDoubleClick" }; + } + + CheckBox checkBox = new () + { + Title = "SelectWordOnlyOnDoubleClick", + CheckedState = _textView.SelectWordOnlyOnDoubleClick ? CheckState.Checked : CheckState.UnChecked + }; + + checkBox.CheckedStateChanged += (s, e) => { _textView.SelectWordOnlyOnDoubleClick = checkBox.CheckedState == CheckState.Checked; }; + + MenuItem item = new () { CommandView = checkBox }; + + item.Accepting += (s, e) => + { + checkBox.AdvanceCheckState (); + e.Handled = true; + }; + + return item; + } + + private MenuItem CreateCanFocusChecked () + { + if (_textView is null) + { + return new () { Title = "CanFocus" }; + } + + CheckBox checkBox = new () + { + Title = "CanFocus", + CheckedState = _textView.CanFocus ? CheckState.Checked : CheckState.UnChecked + }; + + checkBox.CheckedStateChanged += (s, e) => + { + _textView.CanFocus = checkBox.CheckedState == CheckState.Checked; + + if (_textView.CanFocus) + { + _textView.SetFocus (); + } + }; + + MenuItem item = new () { CommandView = checkBox }; + + item.Accepting += (s, e) => + { + checkBox.AdvanceCheckState (); + e.Handled = true; + }; + + return item; + } + + private MenuItem CreateEnabledChecked () + { + if (_textView is null) + { + return new () { Title = "Enabled" }; + } + + CheckBox checkBox = new () + { + Title = "Enabled", + CheckedState = _textView.Enabled ? CheckState.Checked : CheckState.UnChecked + }; + + checkBox.CheckedStateChanged += (s, e) => + { + _textView.Enabled = checkBox.CheckedState == CheckState.Checked; + + if (_textView.Enabled) + { + _textView.SetFocus (); + } + }; + + MenuItem item = new () { CommandView = checkBox }; + + item.Accepting += (s, e) => + { + checkBox.AdvanceCheckState (); + e.Handled = true; + }; + + return item; + } + + private MenuItem CreateVisibleChecked () + { + if (_textView is null) + { + return new () { Title = "Visible" }; + } + + CheckBox checkBox = new () + { + Title = "Visible", + CheckedState = _textView.Visible ? CheckState.Checked : CheckState.UnChecked + }; + + checkBox.CheckedStateChanged += (s, e) => + { + _textView.Visible = checkBox.CheckedState == CheckState.Checked; + + if (_textView.Visible) + { + _textView.SetFocus (); + } + }; + + MenuItem item = new () { CommandView = checkBox }; + + item.Accepting += (s, e) => + { + checkBox.AdvanceCheckState (); + e.Handled = true; + }; + + return item; } private void CreateDemoFile (string fileName) { - var sb = new StringBuilder (); + StringBuilder sb = new (); - // FIXED: BUGBUG: #279 TextView does not know how to deal with \r\n, only \r sb.Append ("Hello world.\n"); sb.Append ("This is a test of the Emergency Broadcast System.\n"); @@ -667,341 +794,33 @@ public class Editor : Scenario sw.Close (); } - private MenuItem CreateEnabledChecked () - { - var item = new MenuItem { Title = "Enabled" }; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = _textView.Enabled; - - item.Action += () => - { - _textView.Enabled = (bool)(item.Checked = !item.Checked); - - if (_textView.Enabled) - { - _textView.SetFocus (); - } - }; - - return item; - } - - private class FindReplaceWindow : Window - { - private TextView _textView; - public FindReplaceWindow (TextView textView) - { - Title = "Find and Replace"; - - _textView = textView; - X = Pos.AnchorEnd () - 1; - Y = 2; - Width = 57; - Height = 11; - Arrangement = ViewArrangement.Movable; - - KeyBindings.Add (Key.Esc, Command.Cancel); - AddCommand (Command.Cancel, () => - { - Visible = false; - - return true; - }); - VisibleChanged += FindReplaceWindow_VisibleChanged; - Initialized += FindReplaceWindow_Initialized; - - //var btnCancel = new Button - //{ - // X = Pos.AnchorEnd (), - // Y = Pos.AnchorEnd (), - // Text = "Cancel" - //}; - //btnCancel.Accept += (s, e) => { Visible = false; }; - //Add (btnCancel); - } - - private void FindReplaceWindow_VisibleChanged (object sender, EventArgs e) - { - if (Visible == false) - { - _textView.SetFocus (); - } - else - { - FocusDeepest (NavigationDirection.Forward, null); - } - } - - private void FindReplaceWindow_Initialized (object sender, EventArgs e) - { - Border.LineStyle = LineStyle.Dashed; - Border.Thickness = new (0, 1, 0, 0); - } - } - - private void ShowFindReplace (bool isFind = true) - { - _findReplaceWindow.Visible = true; - _findReplaceWindow.SuperView.MoveSubViewToStart (_findReplaceWindow); - _tabView.SetFocus (); - _tabView.SelectedTab = isFind ? _tabView.Tabs.ToArray () [0] : _tabView.Tabs.ToArray () [1]; - _tabView.SelectedTab.View.FocusDeepest (NavigationDirection.Forward, null); - } - - private void CreateFindReplace () - { - _findReplaceWindow = new (_textView); - _tabView = new () - { - X = 0, Y = 0, - Width = Dim.Fill (), Height = Dim.Fill (0) - }; - - _tabView.AddTab (new () { DisplayText = "Find", View = CreateFindTab () }, true); - _tabView.AddTab (new () { DisplayText = "Replace", View = CreateReplaceTab () }, false); - _tabView.SelectedTabChanged += (s, e) => _tabView.SelectedTab.View.FocusDeepest (NavigationDirection.Forward, null); - _findReplaceWindow.Add (_tabView); - -// _tabView.SelectedTab.View.FocusLast (null); // Hack to get the first tab to be focused - _findReplaceWindow.Visible = false; - _appWindow.Add (_findReplaceWindow); - } - - private MenuItem [] CreateKeepChecked () - { - var item = new MenuItem (); - item.Title = "Keep Content Always In Viewport"; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = true; - //item.Action += () => _scrollBar.KeepContentAlwaysInViewport = (bool)(item.Checked = !item.Checked); - - return new [] { item }; - } - - private MenuItem CreateSelectWordOnlyOnDoubleClick () - { - var item = new MenuItem { Title = "SelectWordOnlyOnDoubleClick" }; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = _textView.SelectWordOnlyOnDoubleClick; - item.Action += () => _textView.SelectWordOnlyOnDoubleClick = (bool)(item.Checked = !item.Checked); - - return item; - } - - private MenuItem CreateUseSameRuneTypeForWords () - { - var item = new MenuItem { Title = "UseSameRuneTypeForWords" }; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = _textView.UseSameRuneTypeForWords; - item.Action += () => _textView.UseSameRuneTypeForWords = (bool)(item.Checked = !item.Checked); - - return item; - } - - private MenuItem CreateReadOnlyChecked () - { - var item = new MenuItem { Title = "Read Only" }; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = _textView.ReadOnly; - item.Action += () => _textView.ReadOnly = (bool)(item.Checked = !item.Checked); - - return item; - } - - private MenuItem CreateVisibleChecked () - { - var item = new MenuItem { Title = "Visible" }; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = _textView.Visible; - - item.Action += () => - { - _textView.Visible = (bool)(item.Checked = !item.Checked); - - if (_textView.Visible) - { - _textView.SetFocus (); - } - }; - - return item; - } - - private MenuItem CreateWrapChecked () - { - var item = new MenuItem { Title = "Word Wrap" }; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = _textView.WordWrap; - - item.Action += () => - { - _textView.WordWrap = (bool)(item.Checked = !item.Checked); - - if (_textView.WordWrap) - { - //_scrollBar.OtherScrollBarView.ShowScrollIndicator = false; - } - }; - - return item; - } - - private void Cut () - { - if (_textView != null) - { - _textView.Cut (); - } - } - - private void Find () { ShowFindReplace (true); } - private void FindNext () { ContinueFind (); } - private void FindPrevious () { ContinueFind (false); } - - private View CreateFindTab () - { - var d = new View () - { - Width = Dim.Fill (), - Height = Dim.Fill () - }; - - int lblWidth = "Replace:".Length; - - var label = new Label - { - Width = lblWidth, - TextAlignment = Alignment.End, - - Text = "Find:" - }; - d.Add (label); - - SetFindText (); - - var txtToFind = new TextField - { - X = Pos.Right (label) + 1, - Y = Pos.Top (label), - Width = Dim.Fill (1), - Text = _textToFind - }; - txtToFind.HasFocusChanging += (s, e) => txtToFind.Text = _textToFind; - d.Add (txtToFind); - - var btnFindNext = new Button - { - X = Pos.Align (Alignment.Center), - Y = Pos.AnchorEnd (), - Enabled = !string.IsNullOrEmpty (txtToFind.Text), - IsDefault = true, - - Text = "Find _Next" - }; - btnFindNext.Accepting += (s, e) => FindNext (); - d.Add (btnFindNext); - - var btnFindPrevious = new Button - { - X = Pos.Align (Alignment.Center), - Y = Pos.AnchorEnd (), - Enabled = !string.IsNullOrEmpty (txtToFind.Text), - Text = "Find _Previous" - }; - btnFindPrevious.Accepting += (s, e) => FindPrevious (); - d.Add (btnFindPrevious); - - txtToFind.TextChanged += (s, e) => - { - _textToFind = txtToFind.Text; - _textView.FindTextChanged (); - btnFindNext.Enabled = !string.IsNullOrEmpty (txtToFind.Text); - btnFindPrevious.Enabled = !string.IsNullOrEmpty (txtToFind.Text); - }; - - var ckbMatchCase = new CheckBox - { - X = 0, Y = Pos.Top (txtToFind) + 2, CheckedState = _matchCase ? CheckState.Checked : CheckState.UnChecked, Text = "Match c_ase" - }; - ckbMatchCase.CheckedStateChanging += (s, e) => _matchCase = e.Result == CheckState.Checked; - d.Add (ckbMatchCase); - - var ckbMatchWholeWord = new CheckBox - { - X = 0, Y = Pos.Top (ckbMatchCase) + 1, CheckedState = _matchWholeWord ? CheckState.Checked : CheckState.UnChecked, Text = "Match _whole word" - }; - ckbMatchWholeWord.CheckedStateChanging += (s, e) => _matchWholeWord = e.Result == CheckState.Checked; - d.Add (ckbMatchWholeWord); - return d; - } - - private MenuItem [] GetSupportedCultures () - { - List supportedCultures = new (); - int index = -1; - - foreach (CultureInfo c in _cultureInfos) - { - var culture = new MenuItem { CheckType = MenuItemCheckStyle.Checked }; - - if (index == -1) - { - culture.Title = "_English"; - culture.Help = "en-US"; - culture.Checked = Thread.CurrentThread.CurrentUICulture.Name == "en-US"; - CreateAction (supportedCultures, culture); - supportedCultures.Add (culture); - index++; - culture = new () { CheckType = MenuItemCheckStyle.Checked }; - } - - culture.Title = $"_{c.Parent.EnglishName}"; - culture.Help = c.Name; - culture.Checked = Thread.CurrentThread.CurrentUICulture.Name == c.Name; - CreateAction (supportedCultures, culture); - supportedCultures.Add (culture); - } - - return supportedCultures.ToArray (); - - void CreateAction (List supportedCultures, MenuItem culture) - { - culture.Action += () => - { - Thread.CurrentThread.CurrentUICulture = new (culture.Help); - culture.Checked = true; - - foreach (MenuItem item in supportedCultures) - { - item.Checked = item.Help == Thread.CurrentThread.CurrentUICulture.Name; - } - }; - } - } - private void LoadFile () { - if (_fileName != null) + if (_fileName is null || _textView is null || _appWindow is null) { - // FIXED: BUGBUG: #452 TextView.LoadFile keeps file open and provides no way of closing it - _textView.Load (_fileName); - - //_textView.Text = System.IO.File.ReadAllText (_fileName); - _originalText = Encoding.Unicode.GetBytes (_textView.Text); - _appWindow.Title = _fileName; - _saved = true; + return; } + + _textView.Load (_fileName); + _originalText = Encoding.Unicode.GetBytes (_textView.Text); + _appWindow.Title = _fileName; + _saved = true; } private void New (bool checkChanges = true) { + if (_appWindow is null || _textView is null) + { + return; + } + if (checkChanges && !CanCloseFile ()) { return; } _appWindow.Title = "Untitled.txt"; - _fileName = null; + _fileName = null!; _originalText = new MemoryStream ().ToArray (); _textView.Text = Encoding.Unicode.GetString (_originalText); } @@ -1013,8 +832,8 @@ public class Editor : Scenario return; } - List aTypes = new () - { + List aTypes = + [ new AllowedType ( "Text", ".txt;.bin;.xml;.json", @@ -1024,8 +843,9 @@ public class Editor : Scenario ".json" ), new AllowedTypeAny () - }; - var d = new OpenDialog { Title = "Open", AllowedTypes = aTypes, AllowsMultipleSelection = false }; + ]; + + OpenDialog d = new () { Title = "Open", AllowedTypes = aTypes, AllowsMultipleSelection = false }; Application.Run (d); if (!d.Canceled && d.FilePaths.Count > 0) @@ -1037,13 +857,7 @@ public class Editor : Scenario d.Dispose (); } - private void Paste () - { - if (_textView != null) - { - _textView.Paste (); - } - } + private void Paste () { _textView?.Paste (); } private void Quit () { @@ -1059,7 +873,12 @@ public class Editor : Scenario private void ReplaceAll () { - if (string.IsNullOrEmpty (_textToFind) || (string.IsNullOrEmpty (_textToReplace) && _findReplaceWindow == null)) + if (_textView is null) + { + return; + } + + if (string.IsNullOrEmpty (_textToFind) || (string.IsNullOrEmpty (_textToReplace) && _findReplaceWindow is null)) { Replace (); @@ -1087,9 +906,14 @@ public class Editor : Scenario private void ReplaceNext () { ContinueFind (true, true); } private void ReplacePrevious () { ContinueFind (false, true); } - private View CreateReplaceTab () + private View CreateFindTab () { - var d = new View () + if (_textView is null) + { + return new (); + } + + View d = new () { Width = Dim.Fill (), Height = Dim.Fill () @@ -1097,7 +921,7 @@ public class Editor : Scenario int lblWidth = "Replace:".Length; - var label = new Label + Label label = new () { Width = lblWidth, TextAlignment = Alignment.End, @@ -1107,17 +931,104 @@ public class Editor : Scenario SetFindText (); - var txtToFind = new TextField + TextField txtToFind = new () { X = Pos.Right (label) + 1, Y = Pos.Top (label), Width = Dim.Fill (1), Text = _textToFind }; - txtToFind.HasFocusChanging += (s, e) => txtToFind.Text = _textToFind; + txtToFind.HasFocusChanging += (s, e) => { txtToFind.Text = _textToFind; }; d.Add (txtToFind); - var btnFindNext = new Button + Button btnFindNext = new () + { + X = Pos.Align (Alignment.Center), + Y = Pos.AnchorEnd (), + Enabled = !string.IsNullOrEmpty (txtToFind.Text), + IsDefault = true, + Text = "Find _Next" + }; + btnFindNext.Accepting += (s, e) => { FindNext (); }; + d.Add (btnFindNext); + + Button btnFindPrevious = new () + { + X = Pos.Align (Alignment.Center), + Y = Pos.AnchorEnd (), + Enabled = !string.IsNullOrEmpty (txtToFind.Text), + Text = "Find _Previous" + }; + btnFindPrevious.Accepting += (s, e) => { FindPrevious (); }; + d.Add (btnFindPrevious); + + txtToFind.TextChanged += (s, e) => + { + _textToFind = txtToFind.Text; + _textView.FindTextChanged (); + btnFindNext.Enabled = !string.IsNullOrEmpty (txtToFind.Text); + btnFindPrevious.Enabled = !string.IsNullOrEmpty (txtToFind.Text); + }; + + CheckBox ckbMatchCase = new () + { + X = 0, + Y = Pos.Top (txtToFind) + 2, + CheckedState = _matchCase ? CheckState.Checked : CheckState.UnChecked, + Text = "Match c_ase" + }; + ckbMatchCase.CheckedStateChanging += (s, e) => { _matchCase = e.Result == CheckState.Checked; }; + d.Add (ckbMatchCase); + + CheckBox ckbMatchWholeWord = new () + { + X = 0, + Y = Pos.Top (ckbMatchCase) + 1, + CheckedState = _matchWholeWord ? CheckState.Checked : CheckState.UnChecked, + Text = "Match _whole word" + }; + ckbMatchWholeWord.CheckedStateChanging += (s, e) => { _matchWholeWord = e.Result == CheckState.Checked; }; + d.Add (ckbMatchWholeWord); + + return d; + } + + private View CreateReplaceTab () + { + if (_textView is null) + { + return new (); + } + + View d = new () + { + Width = Dim.Fill (), + Height = Dim.Fill () + }; + + int lblWidth = "Replace:".Length; + + Label label = new () + { + Width = lblWidth, + TextAlignment = Alignment.End, + Text = "Find:" + }; + d.Add (label); + + SetFindText (); + + TextField txtToFind = new () + { + X = Pos.Right (label) + 1, + Y = Pos.Top (label), + Width = Dim.Fill (1), + Text = _textToFind + }; + txtToFind.HasFocusChanging += (s, e) => { txtToFind.Text = _textToFind; }; + d.Add (txtToFind); + + Button btnFindNext = new () { X = Pos.Align (Alignment.Center), Y = Pos.AnchorEnd (), @@ -1125,7 +1036,7 @@ public class Editor : Scenario IsDefault = true, Text = "Replace _Next" }; - btnFindNext.Accepting += (s, e) => ReplaceNext (); + btnFindNext.Accepting += (s, e) => { ReplaceNext (); }; d.Add (btnFindNext); label = new () @@ -1138,34 +1049,34 @@ public class Editor : Scenario SetFindText (); - var txtToReplace = new TextField + TextField txtToReplace = new () { X = Pos.Right (label) + 1, Y = Pos.Top (label), Width = Dim.Fill (1), Text = _textToReplace }; - txtToReplace.TextChanged += (s, e) => _textToReplace = txtToReplace.Text; + txtToReplace.TextChanged += (s, e) => { _textToReplace = txtToReplace.Text; }; d.Add (txtToReplace); - var btnFindPrevious = new Button + Button btnFindPrevious = new () { X = Pos.Align (Alignment.Center), Y = Pos.AnchorEnd (), Enabled = !string.IsNullOrEmpty (txtToFind.Text), Text = "Replace _Previous" }; - btnFindPrevious.Accepting += (s, e) => ReplacePrevious (); + btnFindPrevious.Accepting += (s, e) => { ReplacePrevious (); }; d.Add (btnFindPrevious); - var btnReplaceAll = new Button + Button btnReplaceAll = new () { X = Pos.Align (Alignment.Center), Y = Pos.AnchorEnd (), Enabled = !string.IsNullOrEmpty (txtToFind.Text), Text = "Replace _All" }; - btnReplaceAll.Accepting += (s, e) => ReplaceAll (); + btnReplaceAll.Accepting += (s, e) => { ReplaceAll (); }; d.Add (btnReplaceAll); txtToFind.TextChanged += (s, e) => @@ -1177,18 +1088,24 @@ public class Editor : Scenario btnReplaceAll.Enabled = !string.IsNullOrEmpty (txtToFind.Text); }; - var ckbMatchCase = new CheckBox + CheckBox ckbMatchCase = new () { - X = 0, Y = Pos.Top (txtToFind) + 2, CheckedState = _matchCase ? CheckState.Checked : CheckState.UnChecked, Text = "Match c_ase" + X = 0, + Y = Pos.Top (txtToFind) + 2, + CheckedState = _matchCase ? CheckState.Checked : CheckState.UnChecked, + Text = "Match c_ase" }; - ckbMatchCase.CheckedStateChanging += (s, e) => _matchCase = e.Result == CheckState.Checked; + ckbMatchCase.CheckedStateChanging += (s, e) => { _matchCase = e.Result == CheckState.Checked; }; d.Add (ckbMatchCase); - var ckbMatchWholeWord = new CheckBox + CheckBox ckbMatchWholeWord = new () { - X = 0, Y = Pos.Top (ckbMatchCase) + 1, CheckedState = _matchWholeWord ? CheckState.Checked : CheckState.UnChecked, Text = "Match _whole word" + X = 0, + Y = Pos.Top (ckbMatchCase) + 1, + CheckedState = _matchWholeWord ? CheckState.Checked : CheckState.UnChecked, + Text = "Match _whole word" }; - ckbMatchWholeWord.CheckedStateChanging += (s, e) => _matchWholeWord = e.Result == CheckState.Checked; + ckbMatchWholeWord.CheckedStateChanging += (s, e) => { _matchWholeWord = e.Result == CheckState.Checked; }; d.Add (ckbMatchWholeWord); return d; @@ -1196,10 +1113,8 @@ public class Editor : Scenario private bool Save () { - if (_fileName != null) + if (_fileName is { } && _appWindow is { }) { - // FIXED: BUGBUG: #279 TextView does not know how to deal with \r\n, only \r - // As a result files saved on Windows and then read back will show invalid chars. return SaveFile (_appWindow.Title, _fileName); } @@ -1208,11 +1123,18 @@ public class Editor : Scenario private bool SaveAs () { - List aTypes = new () + if (_appWindow is null) { - new AllowedType ("Text Files", ".txt", ".bin", ".xml"), new AllowedTypeAny () - }; - var sd = new SaveDialog { Title = "Save file", AllowedTypes = aTypes }; + return false; + } + + List aTypes = + [ + new AllowedType ("Text Files", ".txt", ".bin", ".xml"), + new AllowedTypeAny () + ]; + + SaveDialog sd = new () { Title = "Save file", AllowedTypes = aTypes }; sd.Path = _appWindow.Title; Application.Run (sd); @@ -1251,6 +1173,11 @@ public class Editor : Scenario private bool SaveFile (string title, string file) { + if (_appWindow is null || _textView is null) + { + return false; + } + try { _appWindow.Title = title; @@ -1271,13 +1198,121 @@ public class Editor : Scenario return true; } - private void SelectAll () { _textView.SelectAll (); } + private void SelectAll () { _textView?.SelectAll (); } private void SetFindText () { + if (_textView is null) + { + return; + } + _textToFind = !string.IsNullOrEmpty (_textView.SelectedText) ? _textView.SelectedText : string.IsNullOrEmpty (_textToFind) ? "" : _textToFind; _textToReplace = string.IsNullOrEmpty (_textToReplace) ? "" : _textToReplace; } + + private void Cut () { _textView?.Cut (); } + + private void Find () { ShowFindReplace (); } + private void FindNext () { ContinueFind (); } + private void FindPrevious () { ContinueFind (false); } + + private void ShowFindReplace (bool isFind = true) + { + if (_findReplaceWindow is null || _tabView is null) + { + return; + } + + _findReplaceWindow.Visible = true; + _findReplaceWindow.SuperView?.MoveSubViewToStart (_findReplaceWindow); + _tabView.SetFocus (); + _tabView.SelectedTab = isFind ? _tabView.Tabs.ToArray () [0] : _tabView.Tabs.ToArray () [1]; + _tabView.SelectedTab?.View?.FocusDeepest (NavigationDirection.Forward, null); + } + + private void CreateFindReplace () + { + if (_textView is null || _appWindow is null) + { + return; + } + + _findReplaceWindow = new (_textView); + + _tabView = new () + { + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill (0) + }; + + _tabView.AddTab (new () { DisplayText = "Find", View = CreateFindTab () }, true); + _tabView.AddTab (new () { DisplayText = "Replace", View = CreateReplaceTab () }, false); + + _tabView.SelectedTabChanged += (s, e) => { _tabView.SelectedTab?.View?.FocusDeepest (NavigationDirection.Forward, null); }; + + _findReplaceWindow.Add (_tabView); + _findReplaceWindow.Visible = false; + _appWindow.Add (_findReplaceWindow); + } + + private class FindReplaceWindow : Window + { + private readonly TextView _textView; + + public FindReplaceWindow (TextView textView) + { + Title = "Find and Replace"; + + _textView = textView; + X = Pos.AnchorEnd () - 1; + Y = 2; + Width = 57; + Height = 11; + Arrangement = ViewArrangement.Movable; + + KeyBindings.Add (Key.Esc, Command.Cancel); + + AddCommand ( + Command.Cancel, + () => + { + Visible = false; + + return true; + }); + + VisibleChanged += FindReplaceWindow_VisibleChanged; + Initialized += FindReplaceWindow_Initialized; + } + + private void FindReplaceWindow_Initialized (object? sender, EventArgs e) + { + if (Border is { }) + { + Border.LineStyle = LineStyle.Dashed; + Border.Thickness = new (0, 1, 0, 0); + } + } + + private void FindReplaceWindow_VisibleChanged (object? sender, EventArgs e) + { + if (!Visible) + { + _textView.SetFocus (); + } + else + { + FocusDeepest (NavigationDirection.Forward, null); + } + } + } } + + + + diff --git a/Examples/UICatalog/Scenarios/GraphViewExample.cs b/Examples/UICatalog/Scenarios/GraphViewExample.cs index 5dcd5df40..203896e68 100644 --- a/Examples/UICatalog/Scenarios/GraphViewExample.cs +++ b/Examples/UICatalog/Scenarios/GraphViewExample.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; +#nullable enable + using System.Text; -using Application = Terminal.Gui.App.Application; namespace UICatalog.Scenarios; @@ -13,144 +10,53 @@ namespace UICatalog.Scenarios; public class GraphViewExample : Scenario { private readonly Thickness _thickness = new (1, 1, 1, 1); - private TextView _about; + private TextView? _about; private int _currentGraph; - private Action [] _graphs; - private GraphView _graphView; - private MenuItem _miDiags; - private MenuItem _miShowBorder; + private Action []? _graphs; + private GraphView? _graphView; + private CheckBox? _diagCheckBox; + private CheckBox? _showBorderCheckBox; private ViewDiagnosticFlags _viewDiagnostics; public override void Main () { Application.Init (); - Toplevel app = new (); - _graphs = new [] + Window app = new () { - () => SetupPeriodicTableScatterPlot (), //0 - () => SetupLifeExpectancyBarGraph (true), //1 - () => SetupLifeExpectancyBarGraph (false), //2 - () => SetupPopulationPyramid (), //3 - () => SetupLineGraph (), //4 - () => SetupSineWave (), //5 - () => SetupDisco (), //6 - () => MultiBarGraph () //7 + BorderStyle = LineStyle.None }; - var menu = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ( - "Scatter _Plot", - "", - () => _graphs [_currentGraph = - 0] () - ), - new ( - "_V Bar Graph", - "", - () => _graphs [_currentGraph = - 1] () - ), - new ( - "_H Bar Graph", - "", - () => _graphs [_currentGraph = - 2] () - ), - new ( - "P_opulation Pyramid", - "", - () => _graphs [_currentGraph = - 3] () - ), - new ( - "_Line Graph", - "", - () => _graphs [_currentGraph = - 4] () - ), - new ( - "Sine _Wave", - "", - () => _graphs [_currentGraph = - 5] () - ), - new ( - "Silent _Disco", - "", - () => _graphs [_currentGraph = - 6] () - ), - new ( - "_Multi Bar Graph", - "", - () => _graphs [_currentGraph = - 7] () - ), - new ("_Quit", "", () => Quit ()) - } - ), - new ( - "_View", - new [] - { - new ("Zoom _In", "", () => Zoom (0.5f)), - new ("Zoom _Out", "", () => Zoom (2f)), - new ("MarginLeft++", "", () => Margin (true, true)), - new ("MarginLeft--", "", () => Margin (true, false)), - new ("MarginBottom++", "", () => Margin (false, true)), - new ("MarginBottom--", "", () => Margin (false, false)), - _miShowBorder = new ( - "_Enable Margin, Border, and Padding", - "", - () => ShowBorder () - ) - { - Checked = true, - CheckType = MenuItemCheckStyle - .Checked - }, - _miDiags = new ( - "_Diagnostics", - "", - () => ToggleDiagnostics () - ) - { - Checked = View.Diagnostics - == (ViewDiagnosticFlags - .Thickness - | ViewDiagnosticFlags - .Ruler), - CheckType = MenuItemCheckStyle.Checked - } - } - ) - ] - }; - app.Add (menu); + _graphs = + [ + SetupPeriodicTableScatterPlot, + () => SetupLifeExpectancyBarGraph (true), + () => SetupLifeExpectancyBarGraph (false), + SetupPopulationPyramid, + SetupLineGraph, + SetupSineWave, + SetupDisco, + MultiBarGraph + ]; + // MenuBar + MenuBar menu = new (); + + // GraphView _graphView = new () { X = 0, - Y = 1, + Y = Pos.Bottom (menu), Width = Dim.Percent (70), Height = Dim.Fill (1), BorderStyle = LineStyle.Single }; _graphView.Border!.Thickness = _thickness; _graphView.Margin!.Thickness = _thickness; - _graphView.Padding.Thickness = _thickness; + _graphView.Padding!.Thickness = _thickness; - app.Add (_graphView); - - var frameRight = new FrameView + // About TextView + FrameView frameRight = new () { X = Pos.Right (_graphView), Y = Pos.Top (_graphView), @@ -159,23 +65,24 @@ public class GraphViewExample : Scenario Title = "About" }; - frameRight.Add ( - _about = new () { Width = Dim.Fill (), Height = Dim.Fill (), ReadOnly = true } - ); + _about = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (), + ReadOnly = true + }; + frameRight.Add (_about); - app.Add (frameRight); + // StatusBar + StatusBar statusBar = new ( + [ + new (Key.G.WithCtrl, "Next Graph", () => _graphs! [_currentGraph++ % _graphs.Length] ()), + new (Key.PageUp, "Zoom In", () => Zoom (0.5f)), + new (Key.PageDown, "Zoom Out", () => Zoom (2f)) + ] + ); - var statusBar = new StatusBar ( - new Shortcut [] - { - new (Key.G.WithCtrl, "Next Graph", () => _graphs [_currentGraph++ % _graphs.Length] ()), - new (Key.PageUp, "Zoom In", () => Zoom (0.5f)), - new (Key.PageDown, "Zoom Out", () => Zoom (2f)) - } - ); - app.Add (statusBar); - - var diagShortcut = new Shortcut + Shortcut? diagShortcut = new () { Key = Key.F10, CommandView = new CheckBox @@ -184,7 +91,128 @@ public class GraphViewExample : Scenario CanFocus = false } }; - statusBar.Add (diagShortcut).Accepting += DiagShortcut_Accept; + + statusBar.Add (diagShortcut); + diagShortcut.Accepting += DiagShortcut_Accept; + + // Menu setup + _showBorderCheckBox = new () + { + Title = "_Enable Margin, Border, and Padding", + CheckedState = CheckState.Checked + }; + _showBorderCheckBox.CheckedStateChanged += (s, e) => ShowBorder (); + + _diagCheckBox = new () + { + Title = "_Diagnostics", + CheckedState = View.Diagnostics == (ViewDiagnosticFlags.Thickness | ViewDiagnosticFlags.Ruler) + ? CheckState.Checked + : CheckState.UnChecked + }; + _diagCheckBox.CheckedStateChanged += (s, e) => ToggleDiagnostics (); + + menu.Add ( + new MenuBarItem ( + "_File", + [ + new MenuItem + { + Title = "Scatter _Plot", + Action = () => _graphs [_currentGraph = 0] () + }, + new MenuItem + { + Title = "_V Bar Graph", + Action = () => _graphs [_currentGraph = 1] () + }, + new MenuItem + { + Title = "_H Bar Graph", + Action = () => _graphs [_currentGraph = 2] () + }, + new MenuItem + { + Title = "P_opulation Pyramid", + Action = () => _graphs [_currentGraph = 3] () + }, + new MenuItem + { + Title = "_Line Graph", + Action = () => _graphs [_currentGraph = 4] () + }, + new MenuItem + { + Title = "Sine _Wave", + Action = () => _graphs [_currentGraph = 5] () + }, + new MenuItem + { + Title = "Silent _Disco", + Action = () => _graphs [_currentGraph = 6] () + }, + new MenuItem + { + Title = "_Multi Bar Graph", + Action = () => _graphs [_currentGraph = 7] () + }, + new MenuItem + { + Title = "_Quit", + Action = Quit + } + ] + ) + ); + + menu.Add ( + new MenuBarItem ( + "_View", + [ + new MenuItem + { + Title = "Zoom _In", + Action = () => Zoom (0.5f) + }, + new MenuItem + { + Title = "Zoom _Out", + Action = () => Zoom (2f) + }, + new MenuItem + { + Title = "MarginLeft++", + Action = () => Margin (true, true) + }, + new MenuItem + { + Title = "MarginLeft--", + Action = () => Margin (true, false) + }, + new MenuItem + { + Title = "MarginBottom++", + Action = () => Margin (false, true) + }, + new MenuItem + { + Title = "MarginBottom--", + Action = () => Margin (false, false) + }, + new MenuItem + { + CommandView = _showBorderCheckBox + }, + new MenuItem + { + CommandView = _diagCheckBox + } + ] + ) + ); + + // Add views in order of visual appearance + app.Add (menu, _graphView, frameRight, statusBar); _graphs [_currentGraph++ % _graphs.Length] (); @@ -195,29 +223,31 @@ public class GraphViewExample : Scenario Application.Shutdown (); } - private void DiagShortcut_Accept (object sender, CommandEventArgs e) + private void DiagShortcut_Accept (object? sender, CommandEventArgs e) { ToggleDiagnostics (); if (sender is Shortcut shortcut && shortcut.CommandView is CheckBox checkBox) { - checkBox.CheckedState = _miDiags.Checked ?? false ? CheckState.Checked : CheckState.UnChecked; + checkBox.CheckedState = _diagCheckBox?.CheckedState ?? CheckState.UnChecked; } } private void ToggleDiagnostics () { - _miDiags.Checked = !_miDiags.Checked; - - View.Diagnostics = _miDiags.Checked == true - ? ViewDiagnosticFlags.Thickness - | ViewDiagnosticFlags.Ruler + View.Diagnostics = _diagCheckBox?.CheckedState == CheckState.Checked + ? ViewDiagnosticFlags.Thickness | ViewDiagnosticFlags.Ruler : ViewDiagnosticFlags.Off; Application.LayoutAndDraw (); } private void Margin (bool left, bool increase) { + if (_graphView is null) + { + return; + } + if (left) { _graphView.MarginLeft = (uint)Math.Max (0, _graphView.MarginLeft + (increase ? 1 : -1)); @@ -232,6 +262,11 @@ public class GraphViewExample : Scenario private void MultiBarGraph () { + if (_graphView is null || _about is null) + { + return; + } + _graphView.Reset (); _graphView.Title = "Multi Bar"; @@ -241,14 +276,14 @@ public class GraphViewExample : Scenario Color fore = _graphView.GetAttributeForRole (VisualRole.Normal).Foreground == Color.Black ? Color.White : _graphView.GetAttributeForRole (VisualRole.Normal).Foreground; - var black = new Attribute (fore, Color.Black); - var cyan = new Attribute (Color.BrightCyan, Color.Black); - var magenta = new Attribute (Color.BrightMagenta, Color.Black); - var red = new Attribute (Color.BrightRed, Color.Black); + Attribute black = new (fore, Color.Black); + Attribute cyan = new (Color.BrightCyan, Color.Black); + Attribute magenta = new (Color.BrightMagenta, Color.Black); + Attribute red = new (Color.BrightRed, Color.Black); _graphView.GraphColor = black; - var series = new MultiBarSeries (3, 1, 0.25f, new [] { magenta, cyan, red }); + MultiBarSeries series = new (3, 1, 0.25f, [magenta, cyan, red]); Rune stiple = Glyphs.Stipple; @@ -277,20 +312,20 @@ public class GraphViewExample : Scenario _graphView.AxisY.Minimum = 0; - var legend = new LegendAnnotation (new (_graphView.Viewport.Width - 20, 0, 20, 5)); + LegendAnnotation legend = new (new (_graphView.Viewport.Width - 20, 0, 20, 5)); legend.AddEntry ( - new (stiple, series.SubSeries.ElementAt (0).OverrideBarColor), + new (stiple, series.SubSeries.ElementAt (0).OverrideBarColor ?? black), "Lower Third" ); legend.AddEntry ( - new (stiple, series.SubSeries.ElementAt (1).OverrideBarColor), + new (stiple, series.SubSeries.ElementAt (1).OverrideBarColor ?? cyan), "Middle Third" ); legend.AddEntry ( - new (stiple, series.SubSeries.ElementAt (2).OverrideBarColor), + new (stiple, series.SubSeries.ElementAt (2).OverrideBarColor ?? red), "Upper Third" ); _graphView.Annotations.Add (legend); @@ -300,6 +335,11 @@ public class GraphViewExample : Scenario private void SetupDisco () { + if (_graphView is null || _about is null) + { + return; + } + _graphView.Reset (); _graphView.Title = "Graphic Equalizer"; @@ -308,11 +348,11 @@ public class GraphViewExample : Scenario _graphView.GraphColor = new Attribute (Color.White, Color.Black); - var stiple = new GraphCellToRender ((Rune)'\u2593'); + GraphCellToRender stiple = new ((Rune)'\u2593'); - var r = new Random (); - var series = new DiscoBarSeries (); - List bars = new (); + Random r = new (); + DiscoBarSeries series = new (); + List bars = []; Func genSample = () => { @@ -323,16 +363,13 @@ public class GraphViewExample : Scenario { bars.Add ( new (null, stiple, r.Next (0, 100)) - { - //ColorGetter = colorDelegate - } ); } - _graphView.SetNeedsDraw (); + _graphView?.SetNeedsDraw (); // while the equaliser is showing - return _graphView.Series.Contains (series); + return _graphView is { } && _graphView.Series.Contains (series); }; Application.AddTimeout (TimeSpan.FromMilliseconds (250), genSample); @@ -351,120 +388,45 @@ public class GraphViewExample : Scenario _graphView.SetNeedsDraw (); } - /* - Country,Both,Male,Female - -"Switzerland",83.4,81.8,85.1 -"South Korea",83.3,80.3,86.1 -"Singapore",83.2,81,85.5 -"Spain",83.2,80.7,85.7 -"Cyprus",83.1,81.1,85.1 -"Australia",83,81.3,84.8 -"Italy",83,80.9,84.9 -"Norway",83,81.2,84.7 -"Israel",82.6,80.8,84.4 -"France",82.5,79.8,85.1 -"Luxembourg",82.4,80.6,84.2 -"Sweden",82.4,80.8,84 -"Iceland",82.3,80.8,83.9 -"Canada",82.2,80.4,84.1 -"New Zealand",82,80.4,83.5 -"Malta,81.9",79.9,83.8 -"Ireland",81.8,80.2,83.5 -"Netherlands",81.8,80.4,83.1 -"Germany",81.7,78.7,84.8 -"Austria",81.6,79.4,83.8 -"Finland",81.6,79.2,84 -"Portugal",81.6,78.6,84.4 -"Belgium",81.4,79.3,83.5 -"United Kingdom",81.4,79.8,83 -"Denmark",81.3,79.6,83 -"Slovenia",81.3,78.6,84.1 -"Greece",81.1,78.6,83.6 -"Kuwait",81,79.3,83.9 -"Costa Rica",80.8,78.3,83.4*/ private void SetupLifeExpectancyBarGraph (bool verticalBars) { + if (_graphView is null || _about is null) + { + return; + } + _graphView.Reset (); _graphView.Title = $"Life Expectancy - {(verticalBars ? "Vertical" : "Horizontal")}"; _about.Text = "This graph shows the life expectancy at birth of a range of countries"; - var softStiple = new GraphCellToRender ((Rune)'\u2591'); - var mediumStiple = new GraphCellToRender ((Rune)'\u2592'); + GraphCellToRender softStiple = new ((Rune)'\u2591'); + GraphCellToRender mediumStiple = new ((Rune)'\u2592'); - var barSeries = new BarSeries + BarSeries barSeries = new () { - Bars = new () - { + Bars = + [ new ("Switzerland", softStiple, 83.4f), - new ( - "South Korea", - !verticalBars - ? mediumStiple - : softStiple, - 83.3f - ), + new ("South Korea", !verticalBars ? mediumStiple : softStiple, 83.3f), new ("Singapore", softStiple, 83.2f), - new ( - "Spain", - !verticalBars - ? mediumStiple - : softStiple, - 83.2f - ), + new ("Spain", !verticalBars ? mediumStiple : softStiple, 83.2f), new ("Cyprus", softStiple, 83.1f), - new ( - "Australia", - !verticalBars - ? mediumStiple - : softStiple, - 83 - ), + new ("Australia", !verticalBars ? mediumStiple : softStiple, 83), new ("Italy", softStiple, 83), - new ( - "Norway", - !verticalBars - ? mediumStiple - : softStiple, - 83 - ), + new ("Norway", !verticalBars ? mediumStiple : softStiple, 83), new ("Israel", softStiple, 82.6f), - new ( - "France", - !verticalBars - ? mediumStiple - : softStiple, - 82.5f - ), + new ("France", !verticalBars ? mediumStiple : softStiple, 82.5f), new ("Luxembourg", softStiple, 82.4f), - new ( - "Sweden", - !verticalBars - ? mediumStiple - : softStiple, - 82.4f - ), + new ("Sweden", !verticalBars ? mediumStiple : softStiple, 82.4f), new ("Iceland", softStiple, 82.3f), - new ( - "Canada", - !verticalBars - ? mediumStiple - : softStiple, - 82.2f - ), + new ("Canada", !verticalBars ? mediumStiple : softStiple, 82.2f), new ("New Zealand", softStiple, 82), - new ( - "Malta", - !verticalBars - ? mediumStiple - : softStiple, - 81.9f - ), + new ("Malta", !verticalBars ? mediumStiple : softStiple, 81.9f), new ("Ireland", softStiple, 81.8f) - } + ] }; _graphView.Series.Add (barSeries); @@ -526,50 +488,62 @@ public class GraphViewExample : Scenario private void SetupLineGraph () { + if (_graphView is null || _about is null) + { + return; + } + _graphView.Reset (); _graphView.Title = "Line"; _about.Text = "This graph shows random points"; - var black = new Attribute (_graphView.GetAttributeForRole (VisualRole.Normal).Foreground, Color.Black, _graphView.GetAttributeForRole (VisualRole.Normal).Style); - var cyan = new Attribute (Color.BrightCyan, Color.Black); - var magenta = new Attribute (Color.BrightMagenta, Color.Black); - var red = new Attribute (Color.BrightRed, Color.Black); + Attribute black = new ( + _graphView.GetAttributeForRole (VisualRole.Normal).Foreground, + Color.Black, + _graphView.GetAttributeForRole (VisualRole.Normal).Style); + Attribute cyan = new (Color.BrightCyan, Color.Black); + Attribute magenta = new (Color.BrightMagenta, Color.Black); + Attribute red = new (Color.BrightRed, Color.Black); _graphView.GraphColor = black; - List randomPoints = new (); + List randomPoints = []; - var r = new Random (); + Random r = new (); for (var i = 0; i < 10; i++) { randomPoints.Add (new (r.Next (100), r.Next (100))); } - var points = new ScatterSeries { Points = randomPoints }; + ScatterSeries points = new () { Points = randomPoints }; - var line = new PathAnnotation + PathAnnotation line = new () { - LineColor = cyan, Points = randomPoints.OrderBy (p => p.X).ToList (), BeforeSeries = true + LineColor = cyan, + Points = randomPoints.OrderBy (p => p.X).ToList (), + BeforeSeries = true }; _graphView.Series.Add (points); _graphView.Annotations.Add (line); - randomPoints = new (); + randomPoints = []; for (var i = 0; i < 10; i++) { randomPoints.Add (new (r.Next (100), r.Next (100))); } - var points2 = new ScatterSeries { Points = randomPoints, Fill = new ((Rune)'x', red) }; + ScatterSeries points2 = new () { Points = randomPoints, Fill = new ((Rune)'x', red) }; - var line2 = new PathAnnotation + PathAnnotation line2 = new () { - LineColor = magenta, Points = randomPoints.OrderBy (p => p.X).ToList (), BeforeSeries = true + LineColor = magenta, + Points = randomPoints.OrderBy (p => p.X).ToList (), + BeforeSeries = true }; _graphView.Series.Add (points2); @@ -609,6 +583,11 @@ public class GraphViewExample : Scenario private void SetupPeriodicTableScatterPlot () { + if (_graphView is null || _about is null) + { + return; + } + _graphView.Reset (); _graphView.Title = "Scatter Plot"; @@ -620,8 +599,8 @@ public class GraphViewExample : Scenario _graphView.Series.Add ( new ScatterSeries { - Points = new () - { + Points = + [ new (1, 1.007f), new (2, 4.002f), new (3, 6.941f), @@ -737,7 +716,7 @@ public class GraphViewExample : Scenario new (116, 292), new (117, 295), new (118, 294) - } + ] } ); @@ -764,29 +743,10 @@ public class GraphViewExample : Scenario private void SetupPopulationPyramid () { - /* - Age,M,F -0-4,2009363,1915127 -5-9,2108550,2011016 -10-14,2022370,1933970 -15-19,1880611,1805522 -20-24,2072674,2001966 -25-29,2275138,2208929 -30-34,2361054,2345774 -35-39,2279836,2308360 -40-44,2148253,2159877 -45-49,2128343,2167778 -50-54,2281421,2353119 -55-59,2232388,2306537 -60-64,1919839,1985177 -65-69,1647391,1734370 -70-74,1624635,1763853 -75-79,1137438,1304709 -80-84,766956,969611 -85-89,438663,638892 -90-94,169952,320625 -95-99,34524,95559 -100+,3016,12818*/ + if (_graphView is null || _about is null) + { + return; + } _about.Text = "This graph shows population of each age divided by gender"; @@ -816,16 +776,16 @@ public class GraphViewExample : Scenario _graphView.AxisY.ShowLabelsEvery = 0; _graphView.AxisY.Minimum = 0; - var stiple = new GraphCellToRender (Glyphs.Stipple); + GraphCellToRender stiple = new (Glyphs.Stipple); // Bars in 2 directions // Males (negative to make the bars go left) - var malesSeries = new BarSeries + BarSeries malesSeries = new () { Orientation = Orientation.Horizontal, - Bars = new () - { + Bars = + [ new ("0-4", stiple, -2009363), new ("5-9", stiple, -2108550), new ("10-14", stiple, -2022370), @@ -847,16 +807,16 @@ public class GraphViewExample : Scenario new ("90-94", stiple, -169952), new ("95-99", stiple, -34524), new ("100+", stiple, -3016) - } + ] }; _graphView.Series.Add (malesSeries); // Females - var femalesSeries = new BarSeries + BarSeries femalesSeries = new () { Orientation = Orientation.Horizontal, - Bars = new () - { + Bars = + [ new ("0-4", stiple, 1915127), new ("5-9", stiple, 2011016), new ("10-14", stiple, 1933970), @@ -878,11 +838,11 @@ public class GraphViewExample : Scenario new ("90-94", stiple, 320625), new ("95-99", stiple, 95559), new ("100+", stiple, 12818) - } + ] }; - var softStiple = new GraphCellToRender ((Rune)'\u2591'); - var mediumStiple = new GraphCellToRender ((Rune)'\u2592'); + GraphCellToRender softStiple = new ((Rune)'\u2591'); + GraphCellToRender mediumStiple = new ((Rune)'\u2592'); for (var i = 0; i < malesSeries.Bars.Count; i++) { @@ -903,14 +863,19 @@ public class GraphViewExample : Scenario private void SetupSineWave () { + if (_graphView is null || _about is null) + { + return; + } + _graphView.Reset (); _graphView.Title = "Sine Wave"; _about.Text = "This graph shows a sine wave"; - var points = new ScatterSeries (); - var line = new PathAnnotation (); + ScatterSeries points = new (); + PathAnnotation line = new (); // Draw line first so it does not draw over top of points or axis labels line.BeforeSeries = true; @@ -950,25 +915,33 @@ public class GraphViewExample : Scenario private void ShowBorder () { - _miShowBorder.Checked = !_miShowBorder.Checked; + if (_graphView is null) + { + return; + } - if (_miShowBorder.Checked == true) + if (_showBorderCheckBox?.CheckedState == CheckState.Checked) { _graphView.BorderStyle = LineStyle.Single; _graphView.Border!.Thickness = _thickness; _graphView.Margin!.Thickness = _thickness; - _graphView.Padding.Thickness = _thickness; + _graphView.Padding!.Thickness = _thickness; } else { _graphView.BorderStyle = LineStyle.None; _graphView.Margin!.Thickness = Thickness.Empty; - _graphView.Padding.Thickness = Thickness.Empty; + _graphView.Padding!.Thickness = Thickness.Empty; } } private void Zoom (float factor) { + if (_graphView is null) + { + return; + } + _graphView.CellSize = new ( _graphView.CellSize.X * factor, _graphView.CellSize.Y * factor @@ -980,7 +953,7 @@ public class GraphViewExample : Scenario _graphView.SetNeedsDraw (); } - private class DiscoBarSeries : BarSeries + private sealed class DiscoBarSeries : BarSeries { private readonly Attribute _brightgreen; private readonly Attribute _brightred; @@ -999,35 +972,22 @@ public class GraphViewExample : Scenario protected override void DrawBarLine (GraphView graph, Point start, Point end, BarSeriesBar beingDrawn) { - IDriver driver = Application.Driver; - int x = start.X; for (int y = end.Y; y <= start.Y; y++) { float height = graph.ScreenToGraphSpace (x, y).Y; - if (height >= 85) - { - graph.SetAttribute (_red); - } - else if (height >= 66) - { - graph.SetAttribute (_brightred); - } - else if (height >= 45) - { - graph.SetAttribute (_brightyellow); - } - else if (height >= 25) - { - graph.SetAttribute (_brightgreen); - } - else - { - graph.SetAttribute (_green); - } + Attribute attr = height switch + { + >= 85 => _red, + >= 66 => _brightred, + >= 45 => _brightyellow, + >= 25 => _brightgreen, + _ => _green + }; + graph.SetAttribute (attr); graph.AddRune (x, y, beingDrawn.Fill.Rune); } } diff --git a/Examples/UICatalog/Scenarios/HexEditor.cs b/Examples/UICatalog/Scenarios/HexEditor.cs index 9b13e1656..45abe08ac 100644 --- a/Examples/UICatalog/Scenarios/HexEditor.cs +++ b/Examples/UICatalog/Scenarios/HexEditor.cs @@ -14,7 +14,7 @@ public class HexEditor : Scenario { private string? _fileName; private HexView? _hexView; - private MenuItemv2? _miReadOnly; + private MenuItem? _miReadOnly; private bool _saved = true; private Shortcut? _scAddress; private Shortcut? _scInfo; @@ -49,13 +49,13 @@ public class HexEditor : Scenario app.Add (_hexView); - var menu = new MenuBarv2 + var menu = new MenuBar { Menus = [ new ( "_File", - new MenuItemv2 [] + new MenuItem [] { new ("_New", "", New), new ("_Open", "", Open), @@ -66,7 +66,7 @@ public class HexEditor : Scenario ), new ( "_Edit", - new MenuItemv2 [] + new MenuItem [] { new ("_Copy", "", Copy), new ("C_ut", "", Cut), @@ -75,7 +75,7 @@ public class HexEditor : Scenario ), new ( "_Options", - new MenuItemv2 [] + new MenuItem [] { _miReadOnly = new ( "_Read Only", diff --git a/Examples/UICatalog/Scenarios/InteractiveTree.cs b/Examples/UICatalog/Scenarios/InteractiveTree.cs index c3b414901..a91448c10 100644 --- a/Examples/UICatalog/Scenarios/InteractiveTree.cs +++ b/Examples/UICatalog/Scenarios/InteractiveTree.cs @@ -1,4 +1,4 @@ -using System.Linq; +#nullable enable namespace UICatalog.Scenarios; @@ -7,46 +7,54 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("TreeView")] public class InteractiveTree : Scenario { - private TreeView _treeView; + private TreeView? _treeView; public override void Main () { Application.Init (); - var appWindow = new Toplevel () + + Window appWindow = new () { Title = GetName (), + BorderStyle = LineStyle.None }; - var menu = new MenuBar - { - Menus = - [ - new ("_File", new MenuItem [] { new ("_Quit", "", Quit) }) - ] - }; - appWindow.Add (menu); + // MenuBar + MenuBar menu = new (); + + menu.Add ( + new MenuBarItem ( + "_File", + [ + new MenuItem + { + Title = "_Quit", + Action = Quit + } + ] + ) + ); _treeView = new () { X = 0, - Y = 1, + Y = Pos.Bottom (menu), Width = Dim.Fill (), Height = Dim.Fill (1) }; _treeView.KeyDown += TreeView_KeyPress; - appWindow.Add (_treeView); + // StatusBar + StatusBar statusBar = new ( + [ + new (Application.QuitKey, "Quit", Quit), + new (Key.C.WithCtrl, "Add Child", AddChildNode), + new (Key.T.WithCtrl, "Add Root", AddRootNode), + new (Key.R.WithCtrl, "Rename Node", RenameNode) + ] + ); - var statusBar = new StatusBar ( - new Shortcut [] - { - new (Application.QuitKey, "Quit", Quit), - new (Key.C.WithCtrl, "Add Child", AddChildNode), - new (Key.T.WithCtrl, "Add Root", AddRootNode), - new (Key.R.WithCtrl, "Rename Node", RenameNode) - } - ); - appWindow.Add (statusBar); + appWindow.Add (menu, _treeView, statusBar); Application.Run (appWindow); appWindow.Dispose (); @@ -55,9 +63,14 @@ public class InteractiveTree : Scenario private void AddChildNode () { - ITreeNode node = _treeView.SelectedObject; + if (_treeView is null) + { + return; + } - if (node != null) + ITreeNode? node = _treeView.SelectedObject; + + if (node is { }) { if (GetText ("Text", "Enter text for node:", "", out string entered)) { @@ -69,6 +82,11 @@ public class InteractiveTree : Scenario private void AddRootNode () { + if (_treeView is null) + { + return; + } + if (GetText ("Text", "Enter text for node:", "", out string entered)) { _treeView.AddObject (new TreeNode (entered)); @@ -79,20 +97,20 @@ public class InteractiveTree : Scenario { var okPressed = false; - var ok = new Button { Text = "Ok", IsDefault = true }; + Button ok = new () { Text = "Ok", IsDefault = true }; ok.Accepting += (s, e) => - { - okPressed = true; - Application.RequestStop (); - }; - var cancel = new Button { Text = "Cancel" }; + { + okPressed = true; + Application.RequestStop (); + }; + Button cancel = new () { Text = "Cancel" }; cancel.Accepting += (s, e) => Application.RequestStop (); - var d = new Dialog { Title = title, Buttons = [ok, cancel] }; + Dialog d = new () { Title = title, Buttons = [ok, cancel] }; - var lbl = new Label { X = 0, Y = 1, Text = label }; + Label lbl = new () { X = 0, Y = 1, Text = label }; - var tf = new TextField { Text = initialText, X = 0, Y = 2, Width = Dim.Fill () }; + TextField tf = new () { Text = initialText, X = 0, Y = 2, Width = Dim.Fill () }; d.Add (lbl, tf); tf.SetFocus (); @@ -100,7 +118,7 @@ public class InteractiveTree : Scenario Application.Run (d); d.Dispose (); - enteredText = okPressed ? tf.Text : null; + enteredText = okPressed ? tf.Text : string.Empty; return okPressed; } @@ -109,9 +127,14 @@ public class InteractiveTree : Scenario private void RenameNode () { - ITreeNode node = _treeView.SelectedObject; + if (_treeView is null) + { + return; + } - if (node != null) + ITreeNode? node = _treeView.SelectedObject; + + if (node is { }) { if (GetText ("Text", "Enter text for node:", node.Text, out string entered)) { @@ -121,13 +144,18 @@ public class InteractiveTree : Scenario } } - private void TreeView_KeyPress (object sender, Key obj) + private void TreeView_KeyPress (object? sender, Key obj) { + if (_treeView is null) + { + return; + } + if (obj.KeyCode == Key.Delete) { - ITreeNode toDelete = _treeView.SelectedObject; + ITreeNode? toDelete = _treeView.SelectedObject; - if (toDelete == null) + if (toDelete is null) { return; } @@ -141,9 +169,9 @@ public class InteractiveTree : Scenario } else { - ITreeNode parent = _treeView.GetParent (toDelete); + ITreeNode? parent = _treeView.GetParent (toDelete); - if (parent == null) + if (parent is null) { MessageBox.ErrorQuery ( "Could not delete", diff --git a/Examples/UICatalog/Scenarios/ListColumns.cs b/Examples/UICatalog/Scenarios/ListColumns.cs index 5861acaad..8ed35942e 100644 --- a/Examples/UICatalog/Scenarios/ListColumns.cs +++ b/Examples/UICatalog/Scenarios/ListColumns.cs @@ -1,6 +1,6 @@ -using System; +#nullable enable + using System.Collections; -using System.Collections.Generic; using System.Data; namespace UICatalog.Scenarios; @@ -13,19 +13,19 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Scrolling")] public class ListColumns : Scenario { - private Scheme _alternatingScheme; - private DataTable _currentTable; - private TableView _listColView; - private MenuItem _miAlternatingColors; - private MenuItem _miAlwaysUseNormalColorForVerticalCellLines; - private MenuItem _miBottomline; - private MenuItem _miCellLines; - private MenuItem _miCursor; - private MenuItem _miExpandLastColumn; - private MenuItem _miOrientVertical; - private MenuItem _miScrollParallel; - private MenuItem _miSmoothScrolling; - private MenuItem _miTopline; + private Scheme? _alternatingScheme; + private DataTable? _currentTable; + private TableView? _listColView; + private CheckBox? _alternatingColorsCheckBox; + private CheckBox? _alwaysUseNormalColorForVerticalCellLinesCheckBox; + private CheckBox? _bottomlineCheckBox; + private CheckBox? _cellLinesCheckBox; + private CheckBox? _cursorCheckBox; + private CheckBox? _expandLastColumnCheckBox; + private CheckBox? _orientVerticalCheckBox; + private CheckBox? _scrollParallelCheckBox; + private CheckBox? _smoothScrollingCheckBox; + private CheckBox? _toplineCheckBox; /// /// Builds a simple list in which values are the index. This helps testing that scrolling etc is working @@ -35,7 +35,7 @@ public class ListColumns : Scenario /// public static IList BuildSimpleList (int items) { - List list = new (); + List list = []; for (var i = 0; i < items; i++) { @@ -47,20 +47,22 @@ public class ListColumns : Scenario public override void Main () { - // Init Application.Init (); - // Setup - Create a top-level application window and configure it. - Toplevel top = new (); Window appWindow = new () { - Title = GetQuitKeyAndName () + Title = GetQuitKeyAndName (), + BorderStyle = LineStyle.None }; + // MenuBar + MenuBar menuBar = new (); + _listColView = new () { + Y = Pos.Bottom(menuBar), Width = Dim.Fill (), - Height = Dim.Fill (), + Height = Dim.Fill (1), Style = new () { ShowHeaders = false, @@ -70,178 +72,43 @@ public class ListColumns : Scenario ExpandLastColumn = false } }; - var listColStyle = new ListColumnStyle (); + ListColumnStyle listColStyle = new (); - var menu = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ( - "Open_BigListExample", - "", - () => OpenSimpleList (true) - ), - new ( - "Open_SmListExample", - "", - () => OpenSimpleList (false) - ), - new ( - "_CloseExample", - "", - () => CloseExample () - ), - new ("_Quit", "", () => Quit ()) - } - ), - new ( - "_View", - new [] - { - _miTopline = - new ("_TopLine", "", () => ToggleTopline ()) - { - Checked = _listColView.Style - .ShowHorizontalHeaderOverline, - CheckType = MenuItemCheckStyle.Checked - }, - _miBottomline = new ( - "_BottomLine", - "", - () => ToggleBottomline () - ) - { - Checked = _listColView.Style - .ShowHorizontalBottomline, - CheckType = MenuItemCheckStyle.Checked - }, - _miCellLines = new ( - "_CellLines", - "", - () => ToggleCellLines () - ) - { - Checked = _listColView.Style - .ShowVerticalCellLines, - CheckType = MenuItemCheckStyle.Checked - }, - _miExpandLastColumn = new ( - "_ExpandLastColumn", - "", - () => ToggleExpandLastColumn () - ) - { - Checked = _listColView.Style.ExpandLastColumn, - CheckType = MenuItemCheckStyle.Checked - }, - _miAlwaysUseNormalColorForVerticalCellLines = - new ( - "_AlwaysUseNormalColorForVerticalCellLines", - "", - () => - ToggleAlwaysUseNormalColorForVerticalCellLines () - ) - { - Checked = _listColView.Style - .AlwaysUseNormalColorForVerticalCellLines, - CheckType = MenuItemCheckStyle.Checked - }, - _miSmoothScrolling = new ( - "_SmoothHorizontalScrolling", - "", - () => ToggleSmoothScrolling () - ) - { - Checked = _listColView.Style - .SmoothHorizontalScrolling, - CheckType = MenuItemCheckStyle.Checked - }, - _miAlternatingColors = new ( - "Alternating Colors", - "", - () => ToggleAlternatingColors () - ) { CheckType = MenuItemCheckStyle.Checked }, - _miCursor = new ( - "Invert Selected Cell First Character", - "", - () => - ToggleInvertSelectedCellFirstCharacter () - ) - { - Checked = _listColView.Style - .InvertSelectedCellFirstCharacter, - CheckType = MenuItemCheckStyle.Checked - } - } - ), - new ( - "_List", - new [] - { - //new MenuItem ("_Hide Headers", "", HideHeaders), - _miOrientVertical = new ( - "_OrientVertical", - "", - () => ToggleVerticalOrientation () - ) - { - Checked = listColStyle.Orientation - == Orientation.Vertical, - CheckType = MenuItemCheckStyle.Checked - }, - _miScrollParallel = new ( - "_ScrollParallel", - "", - () => ToggleScrollParallel () - ) - { - Checked = listColStyle.ScrollParallel, - CheckType = MenuItemCheckStyle.Checked - }, - new ("Set _Max Cell Width", "", SetListMaxWidth), - new ("Set Mi_n Cell Width", "", SetListMinWidth) - } - ) - ] - }; - var statusBar = new StatusBar ( - new Shortcut [] - { - new (Key.F2, "OpenBigListEx", () => OpenSimpleList (true)), - new (Key.F3, "CloseExample", CloseExample), - new (Key.F4, "OpenSmListEx", () => OpenSimpleList (false)), - new (Application.QuitKey, "Quit", Quit) - } - ); - appWindow.Add (_listColView); + // Status Bar + StatusBar statusBar = new ( + [ + new (Key.F2, "OpenBigListEx", () => OpenSimpleList (true)), + new (Key.F3, "CloseExample", CloseExample), + new (Key.F4, "OpenSmListEx", () => OpenSimpleList (false)), + new (Application.QuitKey, "Quit", Quit) + ] + ); - var selectedCellLabel = new Label + // Selected cell label + Label selectedCellLabel = new () { X = 0, Y = Pos.Bottom (_listColView), Text = "0,0", - Width = Dim.Fill (), TextAlignment = Alignment.End }; - appWindow.Add (selectedCellLabel); - - _listColView.SelectedCellChanged += (s, e) => { selectedCellLabel.Text = $"{_listColView.SelectedRow},{_listColView.SelectedColumn}"; }; + _listColView.SelectedCellChanged += (s, e) => + { + if (_listColView is { }) + { + selectedCellLabel.Text = $"{_listColView.SelectedRow},{_listColView.SelectedColumn}"; + } + }; _listColView.KeyDown += TableViewKeyPress; - //SetupScrollBar (); - _alternatingScheme = new () { - Disabled = appWindow.GetAttributeForRole(VisualRole.Disabled), + Disabled = appWindow.GetAttributeForRole (VisualRole.Disabled), HotFocus = appWindow.GetAttributeForRole (VisualRole.HotFocus), - Focus = appWindow.GetAttributeForRole(VisualRole.Focus), + Focus = appWindow.GetAttributeForRole (VisualRole.Focus), Normal = new (Color.White, Color.BrightBlue) }; @@ -250,37 +117,210 @@ public class ListColumns : Scenario _listColView.KeyBindings.ReplaceCommands (Key.Space, Command.Accept); - top.Add (menu, appWindow, statusBar); - appWindow.Y = 1; - appWindow.Height = Dim.Fill(Dim.Func (_ => statusBar.Frame.Height)); + // Setup menu checkboxes + _toplineCheckBox = new () + { + Title = "_TopLine", + CheckedState = _listColView.Style.ShowHorizontalHeaderOverline ? CheckState.Checked : CheckState.UnChecked + }; + _toplineCheckBox.CheckedStateChanged += (s, e) => ToggleTopline (); - // Run - Start the application. - Application.Run (top); - top.Dispose (); + _bottomlineCheckBox = new () + { + Title = "_BottomLine", + CheckedState = _listColView.Style.ShowHorizontalBottomline ? CheckState.Checked : CheckState.UnChecked + }; + _bottomlineCheckBox.CheckedStateChanged += (s, e) => ToggleBottomline (); - // Shutdown - Calling Application.Shutdown is required. + _cellLinesCheckBox = new () + { + Title = "_CellLines", + CheckedState = _listColView.Style.ShowVerticalCellLines ? CheckState.Checked : CheckState.UnChecked + }; + _cellLinesCheckBox.CheckedStateChanged += (s, e) => ToggleCellLines (); + + _expandLastColumnCheckBox = new () + { + Title = "_ExpandLastColumn", + CheckedState = _listColView.Style.ExpandLastColumn ? CheckState.Checked : CheckState.UnChecked + }; + _expandLastColumnCheckBox.CheckedStateChanged += (s, e) => ToggleExpandLastColumn (); + + _alwaysUseNormalColorForVerticalCellLinesCheckBox = new () + { + Title = "_AlwaysUseNormalColorForVerticalCellLines", + CheckedState = _listColView.Style.AlwaysUseNormalColorForVerticalCellLines ? CheckState.Checked : CheckState.UnChecked + }; + _alwaysUseNormalColorForVerticalCellLinesCheckBox.CheckedStateChanged += (s, e) => ToggleAlwaysUseNormalColorForVerticalCellLines (); + + _smoothScrollingCheckBox = new () + { + Title = "_SmoothHorizontalScrolling", + CheckedState = _listColView.Style.SmoothHorizontalScrolling ? CheckState.Checked : CheckState.UnChecked + }; + _smoothScrollingCheckBox.CheckedStateChanged += (s, e) => ToggleSmoothScrolling (); + + _alternatingColorsCheckBox = new () + { + Title = "Alternating Colors" + }; + _alternatingColorsCheckBox.CheckedStateChanged += (s, e) => ToggleAlternatingColors (); + + _cursorCheckBox = new () + { + Title = "Invert Selected Cell First Character", + CheckedState = _listColView.Style.InvertSelectedCellFirstCharacter ? CheckState.Checked : CheckState.UnChecked + }; + _cursorCheckBox.CheckedStateChanged += (s, e) => ToggleInvertSelectedCellFirstCharacter (); + + _orientVerticalCheckBox = new () + { + Title = "_OrientVertical", + CheckedState = listColStyle.Orientation == Orientation.Vertical ? CheckState.Checked : CheckState.UnChecked + }; + _orientVerticalCheckBox.CheckedStateChanged += (s, e) => ToggleVerticalOrientation (); + + _scrollParallelCheckBox = new () + { + Title = "_ScrollParallel", + CheckedState = listColStyle.ScrollParallel ? CheckState.Checked : CheckState.UnChecked + }; + _scrollParallelCheckBox.CheckedStateChanged += (s, e) => ToggleScrollParallel (); + + menuBar.Add ( + new MenuBarItem ( + "_File", + [ + new MenuItem + { + Title = "Open_BigListExample", + Action = () => OpenSimpleList (true) + }, + new MenuItem + { + Title = "Open_SmListExample", + Action = () => OpenSimpleList (false) + }, + new MenuItem + { + Title = "_CloseExample", + Action = CloseExample + }, + new MenuItem + { + Title = "_Quit", + Action = Quit + } + ] + ) + ); + + menuBar.Add ( + new MenuBarItem ( + "_View", + [ + new MenuItem + { + CommandView = _toplineCheckBox + }, + new MenuItem + { + CommandView = _bottomlineCheckBox + }, + new MenuItem + { + CommandView = _cellLinesCheckBox + }, + new MenuItem + { + CommandView = _expandLastColumnCheckBox + }, + new MenuItem + { + CommandView = _alwaysUseNormalColorForVerticalCellLinesCheckBox + }, + new MenuItem + { + CommandView = _smoothScrollingCheckBox + }, + new MenuItem + { + CommandView = _alternatingColorsCheckBox + }, + new MenuItem + { + CommandView = _cursorCheckBox + } + ] + ) + ); + + menuBar.Add ( + new MenuBarItem ( + "_List", + [ + new MenuItem + { + CommandView = _orientVerticalCheckBox + }, + new MenuItem + { + CommandView = _scrollParallelCheckBox + }, + new MenuItem + { + Title = "Set _Max Cell Width", + Action = SetListMaxWidth + }, + new MenuItem + { + Title = "Set Mi_n Cell Width", + Action = SetListMinWidth + } + ] + ) + ); + + // Add views in order of visual appearance + appWindow.Add (menuBar, _listColView, selectedCellLabel, statusBar); + + Application.Run (appWindow); + appWindow.Dispose (); Application.Shutdown (); } - private void CloseExample () { _listColView.Table = null; } + private void CloseExample () + { + if (_listColView is { }) + { + _listColView.Table = null; + } + } + private void OpenSimpleList (bool big) { SetTable (BuildSimpleList (big ? 1023 : 31)); } + private void Quit () { Application.RequestStop (); } private void RunListWidthDialog (string prompt, Action setter, Func getter) { + if (_listColView is null) + { + return; + } + var accepted = false; - var ok = new Button { Text = "Ok", IsDefault = true }; + Button ok = new () { Text = "Ok", IsDefault = true }; ok.Accepting += (s, e) => - { - accepted = true; - Application.RequestStop (); - }; - var cancel = new Button { Text = "Cancel" }; + { + accepted = true; + Application.RequestStop (); + }; + Button cancel = new () { Text = "Cancel" }; cancel.Accepting += (s, e) => { Application.RequestStop (); }; - var d = new Dialog { Title = prompt, Buttons = [ok, cancel] }; + Dialog d = new () { Title = prompt, Buttons = [ok, cancel] }; - var tf = new TextField { Text = getter (_listColView).ToString (), X = 0, Y = 0, Width = Dim.Fill () }; + TextField tf = new () { Text = getter (_listColView).ToString (), X = 0, Y = 0, Width = Dim.Fill () }; d.Add (tf); tf.SetFocus (); @@ -304,63 +344,37 @@ public class ListColumns : Scenario private void SetListMaxWidth () { RunListWidthDialog ("MaxCellWidth", (s, v) => s.MaxCellWidth = v, s => s.MaxCellWidth); - _listColView.SetNeedsDraw (); + _listColView?.SetNeedsDraw (); } private void SetListMinWidth () { RunListWidthDialog ("MinCellWidth", (s, v) => s.MinCellWidth = v, s => s.MinCellWidth); - _listColView.SetNeedsDraw (); + _listColView?.SetNeedsDraw (); } private void SetTable (IList list) { + if (_listColView is null) + { + return; + } + _listColView.Table = new ListTableSource (list, _listColView); - if ((ListTableSource)_listColView.Table != null) + if (_listColView.Table is ListTableSource listTableSource) { - _currentTable = ((ListTableSource)_listColView.Table).DataTable; + _currentTable = listTableSource.DataTable; } } - //private void SetupScrollBar () - //{ - // var scrollBar = new ScrollBarView (_listColView, true); // (listColView, true, true); - - // scrollBar.ChangedPosition += (s, e) => - // { - // _listColView.RowOffset = scrollBar.Position; - - // if (_listColView.RowOffset != scrollBar.Position) - // { - // scrollBar.Position = _listColView.RowOffset; - // } - - // _listColView.SetNeedsDraw (); - // }; - // /* - // scrollBar.OtherScrollBarView.ChangedPosition += (s,e) => { - // listColView.ColumnOffset = scrollBar.OtherScrollBarView.Position; - // if (listColView.ColumnOffset != scrollBar.OtherScrollBarView.Position) { - // scrollBar.OtherScrollBarView.Position = listColView.ColumnOffset; - // } - // listColView.SetNeedsDraw (); - // }; - // */ - - // _listColView.DrawingContent += (s, e) => - // { - // scrollBar.Size = _listColView.Table?.Rows ?? 0; - // scrollBar.Position = _listColView.RowOffset; - - // //scrollBar.OtherScrollBarView.Size = listColView.Table?.Columns - 1 ?? 0; - // //scrollBar.OtherScrollBarView.Position = listColView.ColumnOffset; - // scrollBar.Refresh (); - // }; - //} - - private void TableViewKeyPress (object sender, Key e) + private void TableViewKeyPress (object? sender, Key e) { + if (_currentTable is null || _listColView is null) + { + return; + } + if (e.KeyCode == Key.Delete) { // set all selected cells to null @@ -376,12 +390,14 @@ public class ListColumns : Scenario private void ToggleAlternatingColors () { - //toggle menu item - _miAlternatingColors.Checked = !_miAlternatingColors.Checked; - - if (_miAlternatingColors.Checked == true) + if (_listColView is null || _alternatingColorsCheckBox is null) { - _listColView.Style.RowColorGetter = a => { return a.RowIndex % 2 == 0 ? _alternatingScheme : null; }; + return; + } + + if (_alternatingColorsCheckBox.CheckedState == CheckState.Checked) + { + _listColView.Style.RowColorGetter = a => a.RowIndex % 2 == 0 ? _alternatingScheme : null; } else { @@ -393,81 +409,106 @@ public class ListColumns : Scenario private void ToggleAlwaysUseNormalColorForVerticalCellLines () { - _miAlwaysUseNormalColorForVerticalCellLines.Checked = - !_miAlwaysUseNormalColorForVerticalCellLines.Checked; + if (_listColView is null || _alwaysUseNormalColorForVerticalCellLinesCheckBox is null) + { + return; + } _listColView.Style.AlwaysUseNormalColorForVerticalCellLines = - (bool)_miAlwaysUseNormalColorForVerticalCellLines.Checked; + _alwaysUseNormalColorForVerticalCellLinesCheckBox.CheckedState == CheckState.Checked; _listColView.Update (); } private void ToggleBottomline () { - _miBottomline.Checked = !_miBottomline.Checked; - _listColView.Style.ShowHorizontalBottomline = (bool)_miBottomline.Checked; + if (_listColView is null || _bottomlineCheckBox is null) + { + return; + } + + _listColView.Style.ShowHorizontalBottomline = _bottomlineCheckBox.CheckedState == CheckState.Checked; _listColView.Update (); } private void ToggleCellLines () { - _miCellLines.Checked = !_miCellLines.Checked; - _listColView.Style.ShowVerticalCellLines = (bool)_miCellLines.Checked; + if (_listColView is null || _cellLinesCheckBox is null) + { + return; + } + + _listColView.Style.ShowVerticalCellLines = _cellLinesCheckBox.CheckedState == CheckState.Checked; _listColView.Update (); } private void ToggleExpandLastColumn () { - _miExpandLastColumn.Checked = !_miExpandLastColumn.Checked; - _listColView.Style.ExpandLastColumn = (bool)_miExpandLastColumn.Checked; + if (_listColView is null || _expandLastColumnCheckBox is null) + { + return; + } + + _listColView.Style.ExpandLastColumn = _expandLastColumnCheckBox.CheckedState == CheckState.Checked; _listColView.Update (); } private void ToggleInvertSelectedCellFirstCharacter () { - //toggle menu item - _miCursor.Checked = !_miCursor.Checked; - _listColView.Style.InvertSelectedCellFirstCharacter = (bool)_miCursor.Checked; + if (_listColView is null || _cursorCheckBox is null) + { + return; + } + + _listColView.Style.InvertSelectedCellFirstCharacter = _cursorCheckBox.CheckedState == CheckState.Checked; _listColView.SetNeedsDraw (); } private void ToggleScrollParallel () { - _miScrollParallel.Checked = !_miScrollParallel.Checked; - - if ((ListTableSource)_listColView.Table != null) + if (_listColView?.Table is not ListTableSource listTableSource || _scrollParallelCheckBox is null) { - ((ListTableSource)_listColView.Table).Style.ScrollParallel = (bool)_miScrollParallel.Checked; - _listColView.SetNeedsDraw (); + return; } + + listTableSource.Style.ScrollParallel = _scrollParallelCheckBox.CheckedState == CheckState.Checked; + _listColView.SetNeedsDraw (); } private void ToggleSmoothScrolling () { - _miSmoothScrolling.Checked = !_miSmoothScrolling.Checked; - _listColView.Style.SmoothHorizontalScrolling = (bool)_miSmoothScrolling.Checked; + if (_listColView is null || _smoothScrollingCheckBox is null) + { + return; + } + + _listColView.Style.SmoothHorizontalScrolling = _smoothScrollingCheckBox.CheckedState == CheckState.Checked; _listColView.Update (); } private void ToggleTopline () { - _miTopline.Checked = !_miTopline.Checked; - _listColView.Style.ShowHorizontalHeaderOverline = (bool)_miTopline.Checked; + if (_listColView is null || _toplineCheckBox is null) + { + return; + } + + _listColView.Style.ShowHorizontalHeaderOverline = _toplineCheckBox.CheckedState == CheckState.Checked; _listColView.Update (); } private void ToggleVerticalOrientation () { - _miOrientVertical.Checked = !_miOrientVertical.Checked; - - if ((ListTableSource)_listColView.Table != null) + if (_listColView?.Table is not ListTableSource listTableSource || _orientVerticalCheckBox is null) { - ((ListTableSource)_listColView.Table).Style.Orientation = (bool)_miOrientVertical.Checked - ? Orientation.Vertical - : Orientation.Horizontal; - _listColView.SetNeedsDraw (); + return; } + + listTableSource.Style.Orientation = _orientVerticalCheckBox.CheckedState == CheckState.Checked + ? Orientation.Vertical + : Orientation.Horizontal; + _listColView.SetNeedsDraw (); } } diff --git a/Examples/UICatalog/Scenarios/Localization.cs b/Examples/UICatalog/Scenarios/Localization.cs index 229800f5e..0da975790 100644 --- a/Examples/UICatalog/Scenarios/Localization.cs +++ b/Examples/UICatalog/Scenarios/Localization.cs @@ -1,7 +1,6 @@ -using System; +#nullable enable + using System.Globalization; -using System.Linq; -using System.Threading; namespace UICatalog.Scenarios; @@ -10,11 +9,11 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Tests")] public class Localization : Scenario { - private CheckBox _allowAnyCheckBox; - private string [] _cultureInfoNameSource; - private CultureInfo [] _cultureInfoSource; + private CheckBox? _allowAnyCheckBox; + private string []? _cultureInfoNameSource; + private CultureInfo []? _cultureInfoSource; private OpenMode _currentOpenMode = OpenMode.File; - private ComboBox _languageComboBox; + private ComboBox? _languageComboBox; public CultureInfo CurrentCulture { get; private set; } = Thread.CurrentThread.CurrentUICulture; public void Quit () @@ -25,6 +24,11 @@ public class Localization : Scenario public void SetCulture (CultureInfo culture) { + if (_languageComboBox is null || _cultureInfoSource is null) + { + return; + } + if (_cultureInfoSource [_languageComboBox.SelectedItem] != culture) { _languageComboBox.SelectedItem = Array.IndexOf (_cultureInfoSource, culture); @@ -43,70 +47,67 @@ public class Localization : Scenario public override void Main () { Application.Init (); - var top = new Toplevel (); - var win = new Window { Title = GetQuitKeyAndName () }; - _cultureInfoSource = Application.SupportedCultures.Append (CultureInfo.InvariantCulture).ToArray (); - _cultureInfoNameSource = Application.SupportedCultures.Select (c => $"{c.NativeName} ({c.Name})") + Window win = new () + { + Title = GetQuitKeyAndName (), + BorderStyle = LineStyle.None + }; + + _cultureInfoSource = Application.SupportedCultures!.Append (CultureInfo.InvariantCulture).ToArray (); + + _cultureInfoNameSource = Application.SupportedCultures!.Select (c => $"{c.NativeName} ({c.Name})") .Append ("Invariant") .ToArray (); - MenuItem [] languageMenus = Application.SupportedCultures - .Select ( - c => new MenuItem ( - $"{c.NativeName} ({c.Name})", - "", - () => SetCulture (c) - ) + MenuItem [] languageMenus = Application.SupportedCultures! + .Select (c => new MenuItem + { + Title = $"{c.NativeName} ({c.Name})", + Action = () => SetCulture (c) + } ) .Concat ( - new MenuItem [] - { - null, - new ( - "Invariant", - "", - () => - SetCulture ( - CultureInfo - .InvariantCulture - ) - ) - } + [ + new () + { + Title = "Invariant", + Action = () => SetCulture (CultureInfo.InvariantCulture) + } + ] ) .ToArray (); - var menu = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new MenuBarItem ( - "_Language", - languageMenus - ), - null, - new ("_Quit", "", Quit) - } - ) - ] - }; - top.Add (menu); + // MenuBar + MenuBar menu = new (); - var selectLanguageLabel = new Label + menu.Add ( + new MenuBarItem ( + "_File", + [ + new MenuBarItem ( + "_Language", + languageMenus + ), + new MenuItem + { + Title = "_Quit", + Action = Quit + } + ] + ) + ); + + Label selectLanguageLabel = new () { X = 2, - Y = 1, - + Y = Pos.Bottom (menu) + 1, Width = Dim.Fill (2), Text = "Please select a language." }; win.Add (selectLanguageLabel); - _languageComboBox = new() + _languageComboBox = new () { X = 2, Y = Pos.Bottom (selectLanguageLabel) + 1, @@ -120,11 +121,10 @@ public class Localization : Scenario _languageComboBox.SelectedItemChanged += LanguageComboBox_SelectChanged; win.Add (_languageComboBox); - var textAndFileDialogLabel = new Label + Label textAndFileDialogLabel = new () { X = 2, Y = Pos.Top (_languageComboBox) + 3, - Width = Dim.Fill (2), Height = 1, Text = @@ -132,13 +132,16 @@ public class Localization : Scenario }; win.Add (textAndFileDialogLabel); - var textField = new TextView + TextView textField = new () { - X = 2, Y = Pos.Bottom (textAndFileDialogLabel) + 1, Width = Dim.Fill (32), Height = 1 + X = 2, + Y = Pos.Bottom (textAndFileDialogLabel) + 1, + Width = Dim.Fill (32), + Height = 1 }; win.Add (textField); - _allowAnyCheckBox = new() + _allowAnyCheckBox = new () { X = Pos.Right (textField) + 1, Y = Pos.Bottom (textAndFileDialogLabel) + 1, @@ -147,46 +150,53 @@ public class Localization : Scenario }; win.Add (_allowAnyCheckBox); - var openDialogButton = new Button + Button openDialogButton = new () { - X = Pos.Right (_allowAnyCheckBox) + 1, Y = Pos.Bottom (textAndFileDialogLabel) + 1, Text = "Open" + X = Pos.Right (_allowAnyCheckBox) + 1, + Y = Pos.Bottom (textAndFileDialogLabel) + 1, + Text = "Open" }; openDialogButton.Accepting += (sender, e) => ShowFileDialog (false); win.Add (openDialogButton); - var saveDialogButton = new Button + Button saveDialogButton = new () { - X = Pos.Right (openDialogButton) + 1, Y = Pos.Bottom (textAndFileDialogLabel) + 1, Text = "Save" + X = Pos.Right (openDialogButton) + 1, + Y = Pos.Bottom (textAndFileDialogLabel) + 1, + Text = "Save" }; saveDialogButton.Accepting += (sender, e) => ShowFileDialog (true); win.Add (saveDialogButton); - var wizardLabel = new Label + Label wizardLabel = new () { X = 2, Y = Pos.Bottom (textField) + 1, - Width = Dim.Fill (2), Text = "Click the button to open a wizard." }; win.Add (wizardLabel); - var wizardButton = new Button { X = 2, Y = Pos.Bottom (wizardLabel) + 1, Text = "Open _wizard" }; + Button wizardButton = new () { X = 2, Y = Pos.Bottom (wizardLabel) + 1, Text = "Open _wizard" }; wizardButton.Accepting += (sender, e) => ShowWizard (); win.Add (wizardButton); win.Unloaded += (sender, e) => Quit (); - win.Y = Pos.Bottom (menu); - top.Add (win); + win.Add (menu); - Application.Run (top); - top.Dispose (); + Application.Run (win); + win.Dispose (); Application.Shutdown (); } public void ShowFileDialog (bool isSaveFile) { + if (_allowAnyCheckBox is null) + { + return; + } + FileDialog dialog = isSaveFile ? new SaveDialog () : new OpenDialog { OpenMode = _currentOpenMode }; dialog.AllowedTypes = @@ -213,16 +223,21 @@ public class Localization : Scenario public void ShowWizard () { - var wizard = new Wizard { Height = 8, Width = 36, Title = "The wizard" }; - wizard.AddStep (new() { HelpText = "Wizard first step" }); - wizard.AddStep (new() { HelpText = "Wizard step 2", NextButtonText = ">>> (_N)" }); - wizard.AddStep (new() { HelpText = "Wizard last step" }); + Wizard wizard = new () { Height = 8, Width = 36, Title = "The wizard" }; + wizard.AddStep (new () { HelpText = "Wizard first step" }); + wizard.AddStep (new () { HelpText = "Wizard step 2", NextButtonText = ">>> (_N)" }); + wizard.AddStep (new () { HelpText = "Wizard last step" }); Application.Run (wizard); wizard.Dispose (); } - private void LanguageComboBox_SelectChanged (object sender, ListViewItemEventArgs e) + private void LanguageComboBox_SelectChanged (object? sender, ListViewItemEventArgs e) { + if (_cultureInfoNameSource is null || _cultureInfoSource is null) + { + return; + } + if (e.Value is string cultureName) { int index = Array.IndexOf (_cultureInfoNameSource, cultureName); diff --git a/Examples/UICatalog/Scenarios/MenuBarScenario.cs b/Examples/UICatalog/Scenarios/MenuBarScenario.cs deleted file mode 100644 index e61438767..000000000 --- a/Examples/UICatalog/Scenarios/MenuBarScenario.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using static System.Runtime.InteropServices.JavaScript.JSType; - -namespace UICatalog.Scenarios; - -[ScenarioMetadata ("MenuBar", "Demonstrates the MenuBar using the demo menu.")] -[ScenarioCategory ("Controls")] -[ScenarioCategory ("Menus")] -public class MenuBarScenario : Scenario -{ - private Label _currentMenuBarItem; - private Label _currentMenuItem; - private Label _focusedView; - private Label _lastAction; - private Label _lastKey; - - public override void Main () - { - // Init - Application.Init (); - - // Setup - Create a top-level application window and configure it. - Window appWindow = new () - { - Title = GetQuitKeyAndName (), - BorderStyle = LineStyle.None - }; - - MenuItem mbiCurrent = null; - MenuItem miCurrent = null; - - var label = new Label { X = 0, Y = 10, Text = "Last Key: " }; - appWindow.Add (label); - - _lastKey = new Label { X = Pos.Right (label), Y = Pos.Top (label), Text = "" }; - - appWindow.Add (_lastKey); - label = new Label { X = 0, Y = Pos.Bottom (label), Text = "Current MenuBarItem: " }; - appWindow.Add (label); - - _currentMenuBarItem = new Label { X = Pos.Right (label), Y = Pos.Top (label), Text = "" }; - appWindow.Add (_currentMenuBarItem); - - label = new Label { X = 0, Y = Pos.Bottom (label), Text = "Current MenuItem: " }; - appWindow.Add (label); - - _currentMenuItem = new Label { X = Pos.Right (label), Y = Pos.Top (label), Text = "" }; - appWindow.Add (_currentMenuItem); - - label = new Label { X = 0, Y = Pos.Bottom (label), Text = "Last Action: " }; - appWindow.Add (label); - - _lastAction = new Label { X = Pos.Right (label), Y = Pos.Top (label), Text = "" }; - appWindow.Add (_lastAction); - - label = new Label { X = 0, Y = Pos.Bottom (label), Text = "Focused View: " }; - appWindow.Add (label); - - _focusedView = new Label { X = Pos.Right (label), Y = Pos.Top (label), Text = "" }; - appWindow.Add (_focusedView); - - MenuBar menuBar = new MenuBar (); - menuBar.UseKeysUpDownAsKeysLeftRight = true; - menuBar.Key = KeyCode.F9; - menuBar.Title = "TestMenuBar"; - - bool FnAction (string s) - { - _lastAction.Text = s; - - return true; - } - - // Declare a variable for the function - Func fnActionVariable = FnAction; - - menuBar.EnableForDesign (ref fnActionVariable); - - menuBar.MenuOpening += (s, e) => - { - mbiCurrent = e.CurrentMenu; - SetCurrentMenuBarItem (mbiCurrent); - SetCurrentMenuItem (miCurrent); - _lastAction.Text = string.Empty; - }; - - menuBar.MenuOpened += (s, e) => - { - miCurrent = e.MenuItem; - SetCurrentMenuBarItem (mbiCurrent); - SetCurrentMenuItem (miCurrent); - }; - - menuBar.MenuClosing += (s, e) => - { - mbiCurrent = null; - miCurrent = null; - SetCurrentMenuBarItem (mbiCurrent); - SetCurrentMenuItem (miCurrent); - }; - - Application.KeyDown += (s, e) => - { - _lastAction.Text = string.Empty; - _lastKey.Text = e.ToString (); - }; - - // There's no focus change event, so this is a bit of a hack. - menuBar.SubViewsLaidOut += (s, e) => { _focusedView.Text = appWindow.MostFocused?.ToString () ?? "None"; }; - - var openBtn = new Button { X = Pos.Center (), Y = 4, Text = "_Open Menu", IsDefault = true }; - openBtn.Accepting += (s, e) => { menuBar.OpenMenu (); }; - appWindow.Add (openBtn); - - var hideBtn = new Button { X = Pos.Center (), Y = Pos.Bottom (openBtn), Text = "Toggle Menu._Visible" }; - hideBtn.Accepting += (s, e) => { menuBar.Visible = !menuBar.Visible; }; - appWindow.Add (hideBtn); - - var enableBtn = new Button { X = Pos.Center (), Y = Pos.Bottom (hideBtn), Text = "_Toggle Menu.Enable" }; - enableBtn.Accepting += (s, e) => { menuBar.Enabled = !menuBar.Enabled; }; - appWindow.Add (enableBtn); - - appWindow.Add (menuBar); - - // Run - Start the application. - Application.Run (appWindow); - appWindow.Dispose (); - - // Shutdown - Calling Application.Shutdown is required. - Application.Shutdown (); - } - - private void SetCurrentMenuBarItem (MenuItem mbi) { _currentMenuBarItem.Text = mbi != null ? mbi.Title : "Closed"; } - private void SetCurrentMenuItem (MenuItem mi) { _currentMenuItem.Text = mi != null ? mi.Title : "None"; } -} diff --git a/Examples/UICatalog/Scenarios/Menus.cs b/Examples/UICatalog/Scenarios/Menus.cs index 755844715..4a5313145 100644 --- a/Examples/UICatalog/Scenarios/Menus.cs +++ b/Examples/UICatalog/Scenarios/Menus.cs @@ -189,7 +189,7 @@ public class Menus : Scenario Application.KeyBindings.Remove (Key.F5); Application.KeyBindings.Add (Key.F5, this, Command.Edit); - var menuBar = new MenuBarv2 + var menuBar = new MenuBar { Title = "MenuHost MenuBar" }; @@ -257,7 +257,7 @@ public class Menus : Scenario menuBar.Accepted += (o, args) => { - if (args.Context?.Source is MenuItemv2 mi && mi.CommandView == enableOverwriteMenuItemCb) + if (args.Context?.Source is MenuItem mi && mi.CommandView == enableOverwriteMenuItemCb) { Logging.Debug ($"menuBar.Accepted: {args.Context.Source?.Title}"); @@ -302,7 +302,7 @@ public class Menus : Scenario menuBar.Accepted += (o, args) => { - if (args.Context?.Source is MenuItemv2 mi && mi.CommandView == editModeMenuItemCb) + if (args.Context?.Source is MenuItem mi && mi.CommandView == editModeMenuItemCb) { Logging.Debug ($"menuBar.Accepted: {args.Context.Source?.Title}"); diff --git a/Examples/UICatalog/Scenarios/MultiColouredTable.cs b/Examples/UICatalog/Scenarios/MultiColouredTable.cs index cf013f86c..9b717d1ed 100644 --- a/Examples/UICatalog/Scenarios/MultiColouredTable.cs +++ b/Examples/UICatalog/Scenarios/MultiColouredTable.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable + using System.Data; using System.Text; @@ -10,40 +11,49 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("TableView")] public class MultiColouredTable : Scenario { - private DataTable _table; - private TableViewColors _tableView; + private DataTable? _table; + private TableViewColors? _tableView; public override void Main () { - // Init Application.Init (); - // Setup - Create a top-level application window and configure it. - Toplevel appWindow = new () + Window appWindow = new () { - Title = GetQuitKeyAndName () + Title = GetQuitKeyAndName (), + BorderStyle = LineStyle.None, }; - _tableView = new () { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1) }; + // MenuBar + var menu = new MenuBar (); - var menu = new MenuBar - { - Menus = - [ - new ("_File", new MenuItem [] { new ("_Quit", "", Quit) }) - ] - }; - appWindow.Add (menu); + menu.Add ( + new MenuBarItem ( + "_File", + [ + new MenuItem + { + Title = "_Quit", + Action = Quit + } + ] + ) + ); - var statusBar = new StatusBar (new Shortcut [] { new (Application.QuitKey, "Quit", Quit) }); + _tableView = new () { X = 0, Y = Pos.Bottom (menu), Width = Dim.Fill (), Height = Dim.Fill (1) }; - appWindow.Add (statusBar); + // StatusBar + var statusBar = new StatusBar ( + [ + new (Application.QuitKey, "Quit", Quit) + ] + ); - appWindow.Add (_tableView); + appWindow.Add (menu, _tableView, statusBar); _tableView.CellActivated += EditCurrentCell; - var dt = new DataTable (); + DataTable dt = new (); dt.Columns.Add ("Col1"); dt.Columns.Add ("Col2"); @@ -54,34 +64,33 @@ public class MultiColouredTable : Scenario dt.Rows.Add (DBNull.Value, DBNull.Value); dt.Rows.Add (DBNull.Value, DBNull.Value); - _tableView.SetScheme (new () - { - Disabled = appWindow.GetAttributeForRole (VisualRole.Disabled), - HotFocus = appWindow.GetAttributeForRole (VisualRole.HotFocus), - Focus = appWindow.GetAttributeForRole (VisualRole.Focus), - Normal = new (Color.DarkGray, Color.Black) - }); + _tableView.SetScheme ( + new () + { + Disabled = appWindow.GetAttributeForRole (VisualRole.Disabled), + HotFocus = appWindow.GetAttributeForRole (VisualRole.HotFocus), + Focus = appWindow.GetAttributeForRole (VisualRole.Focus), + Normal = new (Color.DarkGray, Color.Black) + } + ); _tableView.Table = new DataTableSource (_table = dt); - // Run - Start the application. Application.Run (appWindow); appWindow.Dispose (); - - // Shutdown - Calling Application.Shutdown is required. Application.Shutdown (); } - private void EditCurrentCell (object sender, CellActivatedEventArgs e) + private void EditCurrentCell (object? sender, CellActivatedEventArgs e) { - if (e.Table == null) + if (e.Table is null || _table is null || _tableView is null) { return; } var oldValue = e.Table [e.Row, e.Col].ToString (); - if (GetText ("Enter new value", e.Table.ColumnNames [e.Col], oldValue, out string newText)) + if (GetText ("Enter new value", e.Table.ColumnNames [e.Col], oldValue ?? "", out string newText)) { try { @@ -101,20 +110,20 @@ public class MultiColouredTable : Scenario { var okPressed = false; - var ok = new Button { Text = "Ok", IsDefault = true }; + Button ok = new () { Text = "Ok", IsDefault = true }; ok.Accepting += (s, e) => - { - okPressed = true; - Application.RequestStop (); - }; - var cancel = new Button { Text = "Cancel" }; + { + okPressed = true; + Application.RequestStop (); + }; + Button cancel = new () { Text = "Cancel" }; cancel.Accepting += (s, e) => { Application.RequestStop (); }; - var d = new Dialog { Title = title, Buttons = [ok, cancel] }; + Dialog d = new () { Title = title, Buttons = [ok, cancel] }; - var lbl = new Label { X = 0, Y = 1, Text = label }; + Label lbl = new () { X = 0, Y = 1, Text = label }; - var tf = new TextField { Text = initialText, X = 0, Y = 2, Width = Dim.Fill () }; + TextField tf = new () { Text = initialText, X = 0, Y = 2, Width = Dim.Fill () }; d.Add (lbl, tf); tf.SetFocus (); @@ -122,7 +131,7 @@ public class MultiColouredTable : Scenario Application.Run (d); d.Dispose (); - enteredText = okPressed ? tf.Text : null; + enteredText = okPressed ? tf.Text : string.Empty; return okPressed; } @@ -155,20 +164,20 @@ public class MultiColouredTable : Scenario break; case 1: SetAttribute ( - new ( - Color.BrightRed, - cellColor.Background - ) - ); + new ( + Color.BrightRed, + cellColor.Background + ) + ); break; case 2: SetAttribute ( - new ( - Color.BrightYellow, - cellColor.Background - ) - ); + new ( + Color.BrightYellow, + cellColor.Background + ) + ); break; case 3: @@ -177,29 +186,29 @@ public class MultiColouredTable : Scenario break; case 4: SetAttribute ( - new ( - Color.BrightGreen, - cellColor.Background - ) - ); + new ( + Color.BrightGreen, + cellColor.Background + ) + ); break; case 5: SetAttribute ( - new ( - Color.BrightBlue, - cellColor.Background - ) - ); + new ( + Color.BrightBlue, + cellColor.Background + ) + ); break; case 6: SetAttribute ( - new ( - Color.BrightCyan, - cellColor.Background - ) - ); + new ( + Color.BrightCyan, + cellColor.Background + ) + ); break; case 7: diff --git a/Examples/UICatalog/Scenarios/Notepad.cs b/Examples/UICatalog/Scenarios/Notepad.cs index 595604a7b..4a2f7d2e6 100644 --- a/Examples/UICatalog/Scenarios/Notepad.cs +++ b/Examples/UICatalog/Scenarios/Notepad.cs @@ -1,4 +1,5 @@ #nullable enable + namespace UICatalog.Scenarios; [ScenarioMetadata ("Notepad", "Multi-tab text editor using the TabView control.")] @@ -16,41 +17,65 @@ public class Notepad : Scenario { Application.Init (); - Toplevel top = new (); - - var menu = new MenuBar + Window top = new () { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ( - "_New", - "", - () => New (), - null, - null, - KeyCode.N - | KeyCode.CtrlMask - | KeyCode.AltMask - ), - new ("_Open", "", Open), - new ("_Save", "", Save), - new ("Save _As", "", () => SaveAs ()), - new ("_Close", "", Close), - new ("_Quit", "", Quit) - } - ), - new ( - "_About", - "", - () => MessageBox.Query ("Notepad", "About Notepad...", "Ok") - ) - ] + BorderStyle = LineStyle.None, }; - top.Add (menu); + + // MenuBar + MenuBar menu = new (); + + menu.Add ( + new MenuBarItem ( + "_File", + [ + new MenuItem + { + Title = "_New", + Key = Key.N.WithCtrl.WithAlt, + Action = New + }, + new MenuItem + { + Title = "_Open", + Action = Open + }, + new MenuItem + { + Title = "_Save", + Action = Save + }, + new MenuItem + { + Title = "Save _As", + Action = () => SaveAs () + }, + new MenuItem + { + Title = "_Close", + Action = Close + }, + new MenuItem + { + Title = "_Quit", + Action = Quit + } + ] + ) + ); + + menu.Add ( + new MenuBarItem ( + "_About", + [ + new MenuItem + { + Title = "_About", + Action = () => MessageBox.Query ("Notepad", "About Notepad...", "Ok") + } + ] + ) + ); _tabView = CreateNewTabView (); @@ -58,27 +83,28 @@ public class Notepad : Scenario _tabView.ApplyStyleChanges (); _tabView.X = 0; - _tabView.Y = 1; + _tabView.Y = Pos.Bottom (menu); _tabView.Width = Dim.Fill (); _tabView.Height = Dim.Fill (1); - top.Add (_tabView); LenShortcut = new (Key.Empty, "Len: ", null); - 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 - ] - ) + // StatusBar + StatusBar statusBar = new ( + [ + 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 }; - top.Add (statusBar); + + top.Add (menu, _tabView, statusBar); _focusedTabView = _tabView; _tabView.SelectedTabChanged += TabView_SelectedTabChanged; @@ -87,55 +113,52 @@ public class Notepad : Scenario top.Ready += (s, e) => { New (); - LenShortcut.Title = $"Len:{_focusedTabView.Text?.Length ?? 0}"; + LenShortcut.Title = $"Len:{_focusedTabView?.Text?.Length ?? 0}"; }; Application.Run (top); top.Dispose (); - Application.Shutdown (); } - public void Save () { Save (_focusedTabView!, _focusedTabView!.SelectedTab!); } + public void Save () + { + if (_focusedTabView?.SelectedTab is { }) + { + Save (_focusedTabView, _focusedTabView.SelectedTab); + } + } public void Save (TabView tabViewToSave, Tab tabToSave) { - var tab = tabToSave as OpenedFile; - - if (tab == null) + if (tabToSave is not OpenedFile tab) { return; } - if (tab.File == null) + if (tab.File is null) { SaveAs (); } + else + { + tab.Save (); + } - tab.Save (); tabViewToSave.SetNeedsDraw (); } public bool SaveAs () { - var tab = _focusedTabView!.SelectedTab as OpenedFile; - - if (tab == null) + if (_focusedTabView?.SelectedTab is not OpenedFile tab) { return false; } - var fd = new SaveDialog (); + SaveDialog fd = new (); Application.Run (fd); - if (string.IsNullOrWhiteSpace (fd.Path)) - { - fd.Dispose (); - - return false; - } - - if (fd.Canceled) + if (string.IsNullOrWhiteSpace (fd.Path) || fd.Canceled) { fd.Dispose (); @@ -151,13 +174,17 @@ public class Notepad : Scenario return true; } - private void Close () { Close (_focusedTabView!, _focusedTabView!.SelectedTab!); } + private void Close () + { + if (_focusedTabView?.SelectedTab is { }) + { + Close (_focusedTabView, _focusedTabView.SelectedTab); + } + } private void Close (TabView tv, Tab tabToClose) { - var tab = tabToClose as OpenedFile; - - if (tab == null) + if (tabToClose is not OpenedFile tab) { return; } @@ -182,7 +209,7 @@ public class Notepad : Scenario if (result == 0) { - if (tab.File == null) + if (tab.File is null) { SaveAs (); } @@ -207,7 +234,7 @@ public class Notepad : Scenario private TabView CreateNewTabView () { - var tv = new TabView { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill () }; + TabView tv = new () { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill () }; tv.TabClicked += TabView_TabClicked; tv.SelectedTabChanged += TabView_SelectedTabChanged; @@ -220,7 +247,7 @@ public class Notepad : Scenario private void Open () { - var open = new OpenDialog { Title = "Open", AllowsMultipleSelection = true }; + OpenDialog open = new () { Title = "Open", AllowsMultipleSelection = true }; Application.Run (open); @@ -246,21 +273,29 @@ 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) + private void Open (FileInfo? fileInfo, string tabName) { - var tab = new OpenedFile (this) { DisplayText = tabName, File = fileInfo }; + if (_focusedTabView is null) + { + return; + } + + OpenedFile tab = new (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 TabView_SelectedTabChanged (object? sender, TabChangedEventArgs e) { - LenShortcut!.Title = $"Len:{e.NewTab?.View?.Text?.Length ?? 0}"; + if (LenShortcut is { }) + { + LenShortcut.Title = $"Len:{e.NewTab?.View?.Text?.Length ?? 0}"; + } e.NewTab?.View?.SetFocus (); } @@ -275,30 +310,33 @@ public class Notepad : Scenario View [] items; - if (e.Tab == null) + if (e.Tab is null) { - items = [new MenuItemv2 ("Open", "", Open)]; + items = [new MenuItem { Title = "Open", Action = Open }]; } else { 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 MenuItem { Title = "Save", Action = () => Save (_focusedTabView!, e.Tab) }, + new MenuItem { Title = "Close", Action = () => Close (tv, e.Tab) } ]; - - PopoverMenu? contextMenu = new (items); - - // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused - // and the context menu is disposed when it is closed. - tv.App!.Popover?.Register (contextMenu); - contextMenu?.MakeVisible (e.MouseEvent.ScreenPosition); - - e.MouseEvent.Handled = true; } + + PopoverMenu contextMenu = new (items); + + // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused + // and the context menu is disposed when it is closed. + if (sender is TabView tabView && tabView.App?.Popover is { }) + { + tabView.App.Popover.Register (contextMenu); + } + + contextMenu.MakeVisible (e.MouseEvent.ScreenPosition); + + e.MouseEvent.Handled = true; } private class OpenedFile (Notepad notepad) : Tab @@ -307,8 +345,8 @@ public class Notepad : Scenario public OpenedFile CloneTo (TabView other) { - var newTab = new OpenedFile (_notepad) { DisplayText = base.Text, File = File }; - newTab.View = newTab.CreateTextView (newTab.File!); + OpenedFile newTab = new (_notepad) { DisplayText = Text, File = File }; + newTab.View = newTab.CreateTextView (newTab.File); newTab.SavedText = newTab.View.Text; newTab.RegisterTextViewEvents (other); other.AddTab (newTab, true); @@ -340,7 +378,10 @@ public class Notepad : Scenario public void RegisterTextViewEvents (TabView parent) { - var textView = (TextView)View!; + if (View is not TextView textView) + { + return; + } // when user makes changes rename tab to indicate unsaved textView.ContentsChanged += (s, k) => @@ -363,25 +404,27 @@ public class Notepad : Scenario } } - _notepad.LenShortcut!.Title = $"Len:{textView.Text.Length}"; + if (_notepad.LenShortcut is { }) + { + _notepad.LenShortcut.Title = $"Len:{textView.Text.Length}"; + } }; } /// The text of the tab the last time it was saved - /// public string? SavedText { get; set; } - public bool UnsavedChanges => !string.Equals (SavedText, View!.Text); + public bool UnsavedChanges => View is { } && !string.Equals (SavedText, View.Text); internal void Save () { - string newText = View!.Text; - - if (File is null || string.IsNullOrWhiteSpace (File.FullName)) + if (View is null || File is null || string.IsNullOrWhiteSpace (File.FullName)) { return; } + string newText = View.Text; + System.IO.File.WriteAllText (File.FullName, newText); SavedText = newText; diff --git a/Examples/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs b/Examples/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs index a10fd6ca8..512a106f0 100644 --- a/Examples/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs +++ b/Examples/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable + +using System; namespace UICatalog.Scenarios; @@ -8,99 +10,173 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Tests")] public class RuneWidthGreaterThanOne : Scenario { - private Button _button; - private Label _label; - private Label _labelR; - private Label _labelV; - private string _lastRunesUsed; - private TextField _text; - private Window _win; + private Button? _button; + private Label? _label; + private Label? _labelR; + private Label? _labelV; + private string? _lastRunesUsed; + private TextField? _text; + private Window? _win; public override void Main () { Application.Init (); - Toplevel topLevel = new (); - - var menu = new MenuBar + // Window (top-level) + Window win = new () { - Menus = - [ - new MenuBarItem ( - "Padding", - new MenuItem [] - { - new ( - "With Padding", - "", - () => _win.Padding.Thickness = - new Thickness (1) - ), - new ( - "Without Padding", - "", - () => _win.Padding.Thickness = - new Thickness (0) - ) - } - ), - new MenuBarItem ( - "BorderStyle", - new MenuItem [] - { - new ( - "Single", - "", - () => _win.BorderStyle = LineStyle.Single - ), - new ( - "None", - "", - () => _win.BorderStyle = LineStyle.None - ) - } - ), - new MenuBarItem ( - "Runes length", - new MenuItem [] - { - new ("Wide", "", WideRunes), - new ("Narrow", "", NarrowRunes), - new ("Mixed", "", MixedRunes) - } - ) - ] + X = 5, + Y = 5, + Width = Dim.Fill (22), + Height = Dim.Fill (5), + Arrangement = ViewArrangement.Overlapped | ViewArrangement.Movable + }; + _win = win; + + // MenuBar + MenuBar menu = new (); + + // Controls + _label = new () + { + X = Pos.Center (), + Y = 1 }; - _label = new Label + _text = new () { - X = Pos.Center (), Y = 1, + X = Pos.Center (), + Y = 3, + Width = 20 }; - _text = new TextField { X = Pos.Center (), Y = 3, Width = 20 }; - _button = new Button { X = Pos.Center (), Y = 5 }; - _labelR = new Label { X = Pos.AnchorEnd (30), Y = 18 }; - _labelV = new Label + _button = new () { - TextDirection = TextDirection.TopBottom_LeftRight, X = Pos.AnchorEnd (30), Y = Pos.Bottom (_labelR) + X = Pos.Center (), + Y = 5 }; - _win = new Window { X = 5, Y = 5, Width = Dim.Fill (22), Height = Dim.Fill (5) }; - _win.Add (_label, _text, _button, _labelR, _labelV); - topLevel.Add (menu, _win); + + _labelR = new () + { + X = Pos.AnchorEnd (30), + Y = 18 + }; + + _labelV = new () + { + TextDirection = TextDirection.TopBottom_LeftRight, + X = Pos.AnchorEnd (30), + Y = Pos.Bottom (_labelR) + }; + + menu.Add ( + new MenuBarItem ( + "Padding", + [ + new MenuItem + { + Title = "With Padding", + Action = () => + { + if (_win is { }) + { + _win.Padding!.Thickness = new (1); + } + } + }, + new MenuItem + { + Title = "Without Padding", + Action = () => + { + if (_win is { }) + { + _win.Padding!.Thickness = new (0); + } + } + } + ] + ) + ); + + menu.Add ( + new MenuBarItem ( + "BorderStyle", + [ + new MenuItem + { + Title = "Single", + Action = () => + { + if (_win is { }) + { + _win.BorderStyle = LineStyle.Single; + } + } + }, + new MenuItem + { + Title = "None", + Action = () => + { + if (_win is { }) + { + _win.BorderStyle = LineStyle.None; + } + } + } + ] + ) + ); + + menu.Add ( + new MenuBarItem ( + "Runes length", + [ + new MenuItem + { + Title = "Wide", + Action = WideRunes + }, + new MenuItem + { + Title = "Narrow", + Action = NarrowRunes + }, + new MenuItem + { + Title = "Mixed", + Action = MixedRunes + } + ] + ) + ); + + // Add views in order of visual appearance + win.Add (menu, _label, _text, _button, _labelR, _labelV); WideRunes (); - //NarrowRunes (); - //MixedRunes (); - Application.Run (topLevel); - topLevel.Dispose (); + Application.Run (win); + win.Dispose (); Application.Shutdown (); } - private void MixedMessage (object sender, EventArgs e) { MessageBox.Query ("Say Hello 你", $"Hello {_text.Text}", "Ok"); } + private void MixedMessage (object? sender, EventArgs e) + { + if (_text is { }) + { + MessageBox.Query ("Say Hello 你", $"Hello {_text.Text}", "Ok"); + } + } private void MixedRunes () { + if (_label is null || _text is null || _button is null || _labelR is null || _labelV is null || _win is null) + { + return; + } + UnsetClickedEvent (); _label.Text = "Enter your name 你:"; _text.Text = "gui.cs 你:"; @@ -117,10 +193,21 @@ public class RuneWidthGreaterThanOne : Scenario Application.LayoutAndDraw (); } - private void NarrowMessage (object sender, EventArgs e) { MessageBox.Query ("Say Hello", $"Hello {_text.Text}", "Ok"); } + private void NarrowMessage (object? sender, EventArgs e) + { + if (_text is { }) + { + MessageBox.Query ("Say Hello", $"Hello {_text.Text}", "Ok"); + } + } private void NarrowRunes () { + if (_label is null || _text is null || _button is null || _labelR is null || _labelV is null || _win is null) + { + return; + } + UnsetClickedEvent (); _label.Text = "Enter your name:"; _text.Text = "gui.cs"; @@ -139,6 +226,11 @@ public class RuneWidthGreaterThanOne : Scenario private void UnsetClickedEvent () { + if (_button is null) + { + return; + } + switch (_lastRunesUsed) { case "Narrow": @@ -156,10 +248,21 @@ public class RuneWidthGreaterThanOne : Scenario } } - private void WideMessage (object sender, EventArgs e) { MessageBox.Query ("こんにちはと言う", $"こんにちは {_text.Text}", "Ok"); } + private void WideMessage (object? sender, EventArgs e) + { + if (_text is { }) + { + MessageBox.Query ("こんにちはと言う", $"こんにちは {_text.Text}", "Ok"); + } + } private void WideRunes () { + if (_label is null || _text is null || _button is null || _labelR is null || _labelV is null || _win is null) + { + return; + } + UnsetClickedEvent (); _label.Text = "あなたの名前を入力してください:"; _text.Text = "ティラミス"; @@ -175,4 +278,4 @@ public class RuneWidthGreaterThanOne : Scenario _lastRunesUsed = "Wide"; Application.LayoutAndDraw (); } -} +} \ No newline at end of file diff --git a/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs b/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs index 92e126b84..5e795a9c0 100644 --- a/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs +++ b/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs @@ -1,8 +1,7 @@ -using System; -using System.Collections.Generic; +#nullable enable + using System.Collections.ObjectModel; using System.ComponentModel; -using System.Threading; namespace UICatalog.Scenarios; @@ -18,54 +17,52 @@ public class SingleBackgroundWorker : Scenario Application.Shutdown (); } - public class MainApp : Toplevel + public class MainApp : Window { private readonly ListView _listLog; private readonly ObservableCollection _log = []; private DateTime? _startStaging; - private BackgroundWorker _worker; + private BackgroundWorker? _worker; public MainApp () { - var menu = new MenuBar - { - Menus = - [ - new ( - "_Options", - new MenuItem [] - { - new ( - "_Run Worker", - "", - () => RunWorker (), - null, - null, - KeyCode.CtrlMask | KeyCode.R - ), - null, - new ( - "_Quit", - "", - () => Application.RequestStop (), - null, - null, - Application.QuitKey - ) - } - ) - ] - }; + BorderStyle = LineStyle.None; + // MenuBar + MenuBar menu = new (); - var statusBar = new StatusBar ( - [ - new (Application.QuitKey, "Quit", () => Application.RequestStop ()), - new (Key.R.WithCtrl, "Run Worker", RunWorker) - ]); + menu.Add ( + new MenuBarItem ( + "_Options", + [ + new MenuItem + { + Title = "_Run Worker", + Key = Key.R.WithCtrl, + Action = RunWorker + }, + new MenuItem + { + Title = "_Quit", + Key = Application.QuitKey, + Action = () => Application.RequestStop () + } + ] + ) + ); - var workerLogTop = new Toplevel + // StatusBar + StatusBar statusBar = new ( + [ + new (Application.QuitKey, "Quit", () => Application.RequestStop ()), + new (Key.R.WithCtrl, "Run Worker", RunWorker) + ] + ); + + Window workerLogTop = new () { - Title = "Worker Log Top" + Title = "Worker Log Top", + Y = Pos.Bottom (menu), + Height = Dim.Fill (1) }; workerLogTop.Add ( @@ -82,9 +79,6 @@ public class SingleBackgroundWorker : Scenario }; workerLogTop.Add (_listLog); - workerLogTop.Y = 1; - workerLogTop.Height = Dim.Fill (Dim.Func (_ => statusBar.Frame.Height)); - Add (menu, workerLogTop, statusBar); Title = "MainApp"; } @@ -93,11 +87,11 @@ public class SingleBackgroundWorker : Scenario { _worker = new () { WorkerSupportsCancellation = true }; - var cancel = new Button { Text = "Cancel Worker" }; + Button cancel = new () { Text = "Cancel Worker" }; cancel.Accepting += (s, e) => { - if (_worker == null) + if (_worker is null) { _log.Add ($"Worker is not running at {DateTime.Now}!"); _listLog.SetNeedsDraw (); @@ -116,9 +110,10 @@ public class SingleBackgroundWorker : Scenario _log.Add ($"Worker is started at {_startStaging}.{_startStaging:fff}"); _listLog.SetNeedsDraw (); - var md = new Dialog + Dialog md = new () { - Title = $"Running Worker started at {_startStaging}.{_startStaging:fff}", Buttons = [cancel] + Title = $"Running Worker started at {_startStaging}.{_startStaging:fff}", + Buttons = [cancel] }; md.Add ( @@ -127,7 +122,7 @@ public class SingleBackgroundWorker : Scenario _worker.DoWork += (s, e) => { - List stageResult = new (); + List stageResult = []; for (var i = 0; i < 200; i++) { @@ -135,7 +130,7 @@ public class SingleBackgroundWorker : Scenario e.Result = stageResult; Thread.Sleep (1); - if (_worker.CancellationPending) + if (_worker?.CancellationPending == true) { e.Cancel = true; @@ -152,7 +147,7 @@ public class SingleBackgroundWorker : Scenario Application.RequestStop (); } - if (e.Error != null) + if (e.Error is { }) { // Failed _log.Add ( @@ -177,14 +172,22 @@ public class SingleBackgroundWorker : Scenario _listLog.SetNeedsDraw (); Application.LayoutAndDraw (); - var builderUI = - new StagingUIController (_startStaging, e.Result as ObservableCollection); - Toplevel top = Application.TopRunnable; - top.Visible = false; - Application.TopRunnable.Visible = false; + StagingUIController builderUI = + new (_startStaging, e.Result as ObservableCollection); + Toplevel? top = Application.TopRunnable; + + if (top is { }) + { + top.Visible = false; + } + builderUI.Load (); builderUI.Dispose (); - top.Visible = true; + + if (top is { }) + { + top.Visible = true; + } } _worker = null; @@ -197,13 +200,16 @@ public class SingleBackgroundWorker : Scenario public class StagingUIController : Window { - private Toplevel _top; + private Toplevel? _top; - public StagingUIController (DateTime? start, ObservableCollection list) + public StagingUIController (DateTime? start, ObservableCollection? list) { _top = new () { - Title = "_top", Width = Dim.Fill (), Height = Dim.Fill (), Modal = true + Title = "_top", + Width = Dim.Fill (), + Height = Dim.Fill (), + Modal = true }; _top.KeyDown += (s, e) => @@ -230,74 +236,78 @@ public class SingleBackgroundWorker : Scenario return n == 0; } - var menu = new MenuBar - { - Menus = - [ - new ( - "_Stage", - new MenuItem [] - { - new ( - "_Close", - "", - () => - { - if (Close ()) - { - Application.RequestStop (); - } - }, - null, - null, - KeyCode.CtrlMask | KeyCode.C - ) - } - ) - ] - }; + // MenuBar + MenuBar menu = new (); + + menu.Add ( + new MenuBarItem ( + "_Stage", + [ + new MenuItem + { + Title = "_Close", + Key = Key.C.WithCtrl, + Action = () => + { + if (Close ()) + { + Application.RequestStop (); + } + } + } + ] + ) + ); _top.Add (menu); - var statusBar = new StatusBar ( - [ - new ( - Key.C.WithCtrl, - "Close", - () => + // StatusBar + StatusBar statusBar = new ( + [ + new ( + Key.C.WithCtrl, + "Close", + () => + { + if (Close ()) { - if (Close ()) - { - Application.RequestStop (); - } + Application.RequestStop (); } - ) - ]); + } + ) + ] + ); _top.Add (statusBar); - Y = 1; + Y = Pos.Bottom (menu); Height = Dim.Fill (1); Title = $"Worker started at {start}.{start:fff}"; SchemeName = "Base"; - Add ( - new ListView - { - X = 0, - Y = 0, - Width = Dim.Fill (), - Height = Dim.Fill (), - Source = new ListWrapper (list) - } - ); + if (list is { }) + { + Add ( + new ListView + { + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill (), + Source = new ListWrapper (list) + } + ); + } _top.Add (this); } public void Load () { - Application.Run (_top); - _top.Dispose (); - _top = null; + if (_top is { }) + { + Application.Run (_top); + _top.Dispose (); + _top = null; + } } } } diff --git a/Examples/UICatalog/Scenarios/SyntaxHighlighting.cs b/Examples/UICatalog/Scenarios/SyntaxHighlighting.cs index ad9f93460..3328264ee 100644 --- a/Examples/UICatalog/Scenarios/SyntaxHighlighting.cs +++ b/Examples/UICatalog/Scenarios/SyntaxHighlighting.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Linq; +using System.ComponentModel; using System.Reflection; using System.Text; using System.Text.Json; @@ -88,7 +84,6 @@ public class SyntaxHighlighting : Scenario private Attribute _blue; private Attribute _green; private Attribute _magenta; - private MenuItem _miWrap; private TextView _textView; private Attribute _white; @@ -99,7 +94,7 @@ public class SyntaxHighlighting : Scenario /// The type of object to read from the file. /// The file path to read the object instance from. /// Returns a new instance of the object read from the Json file. - public static T ReadFromJsonFile (string filePath) where T : new() + public static T ReadFromJsonFile (string filePath) where T : new () { TextReader reader = null; @@ -127,46 +122,26 @@ public class SyntaxHighlighting : Scenario // Setup - Create a top-level application window and configure it. Toplevel appWindow = new (); - var menu = new MenuBar - { - Menus = - [ - new ( - "_TextView", - new [] - { - _miWrap = new ( - "_Word Wrap", - "", - () => WordWrap () - ) - { - CheckType = MenuItemCheckStyle - .Checked - }, - null, - new ( - "_Syntax Highlighting", - "", - () => ApplySyntaxHighlighting () - ), - null, - new ( - "_Load Text Cells", - "", - () => ApplyLoadCells () - ), - new ( - "_Save Text Cells", - "", - () => SaveCells () - ), - null, - new ("_Quit", "", () => Quit ()) - } - ) - ] - }; + var menu = new MenuBar (); + + MenuItem wrapMenuItem = CreateWordWrapMenuItem (); + + menu.Add ( + new MenuBarItem ( + "_TextView", + [ + wrapMenuItem, + new Line (), + new MenuItem { Title = "_Syntax Highlighting", Action = ApplySyntaxHighlighting }, + new Line (), + new MenuItem { Title = "_Load Text Cells", Action = ApplyLoadCells }, + new MenuItem { Title = "_Save Text Cells", Action = SaveCells }, + new Line (), + new MenuItem { Title = "_Quit", Action = Quit } + ] + ) + ); + appWindow.Add (menu); _textView = new () @@ -192,6 +167,33 @@ public class SyntaxHighlighting : Scenario Application.Shutdown (); } + private MenuItem CreateWordWrapMenuItem () + { + CheckBox checkBox = new () + { + Title = "_Word Wrap", + CheckedState = _textView?.WordWrap == true ? CheckState.Checked : CheckState.UnChecked + }; + + checkBox.CheckedStateChanged += (s, e) => + { + if (_textView is { }) + { + _textView.WordWrap = checkBox.CheckedState == CheckState.Checked; + } + }; + + MenuItem item = new () { CommandView = checkBox }; + + item.Accepting += (s, e) => + { + checkBox.AdvanceCheckState (); + e.Handled = true; + }; + + return item; + } + /// /// Writes the given object instance to a Json file. /// Object type must have a parameterless constructor. @@ -211,7 +213,7 @@ public class SyntaxHighlighting : Scenario /// If false the file will be overwritten if it already exists. If true the contents will be appended /// to the file. /// - public static void WriteToJsonFile (string filePath, T objectToWrite, bool append = false) where T : new() + public static void WriteToJsonFile (string filePath, T objectToWrite, bool append = false) where T : new () { TextWriter writer = null; @@ -263,10 +265,10 @@ public class SyntaxHighlighting : Scenario { ClearAllEvents (); - _green = new Attribute (Color.Green, Color.Black); - _blue = new Attribute (Color.Blue, Color.Black); - _magenta = new Attribute (Color.Magenta, Color.Black); - _white = new Attribute (Color.White, Color.Black); + _green = new (Color.Green, Color.Black); + _blue = new (Color.Blue, Color.Black); + _magenta = new (Color.Magenta, Color.Black); + _white = new (Color.White, Color.Black); _textView.SetScheme (new () { Focus = _white }); _textView.Text = @@ -287,7 +289,7 @@ public class SyntaxHighlighting : Scenario _textView.InheritsPreviousAttribute = false; } - private bool ContainsPosition (Match m, int pos) { return pos >= m.Index && pos < m.Index + m.Length; } + private bool ContainsPosition (Match m, int pos) => pos >= m.Index && pos < m.Index + m.Length; private void HighlightTextBasedOnKeywords () { @@ -385,12 +387,6 @@ public class SyntaxHighlighting : Scenario List> cells = _textView.GetAllLines (); WriteToJsonFile (_path, cells); } - - private void WordWrap () - { - _miWrap.Checked = !_miWrap.Checked; - _textView.WordWrap = (bool)_miWrap.Checked; - } } public static class EventExtensions diff --git a/Examples/UICatalog/Scenarios/TabViewExample.cs b/Examples/UICatalog/Scenarios/TabViewExample.cs index 30f55d5f4..71eede3a6 100644 --- a/Examples/UICatalog/Scenarios/TabViewExample.cs +++ b/Examples/UICatalog/Scenarios/TabViewExample.cs @@ -1,4 +1,6 @@ -using System.Linq; +#nullable enable + +using System.Linq; using System.Text; namespace UICatalog.Scenarios; @@ -8,85 +10,41 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("TabView")] public class TabViewExample : Scenario { - private MenuItem _miShowBorder; - private MenuItem _miShowTabViewBorder; - private MenuItem _miShowTopLine; - private MenuItem _miTabsOnBottom; - private TabView _tabView; + private CheckBox? _miShowBorderCheckBox; + private CheckBox? _miShowTabViewBorderCheckBox; + private CheckBox? _miShowTopLineCheckBox; + private CheckBox? _miTabsOnBottomCheckBox; + private TabView? _tabView; public override void Main () { - // Init Application.Init (); - // Setup - Create a top-level application window and configure it. - Toplevel appWindow = new (); - - var menu = new MenuBar + Window appWindow = new () { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ("_Add Blank Tab", "", AddBlankTab), - new ( - "_Clear SelectedTab", - "", - () => _tabView.SelectedTab = null - ), - new ("_Quit", "", Quit) - } - ), - new ( - "_View", - new [] - { - _miShowTopLine = - new ("_Show Top Line", "", ShowTopLine) - { - Checked = true, CheckType = MenuItemCheckStyle.Checked - }, - _miShowBorder = - new ("_Show Border", "", ShowBorder) - { - Checked = true, CheckType = MenuItemCheckStyle.Checked - }, - _miTabsOnBottom = - new ("_Tabs On Bottom", "", SetTabsOnBottom) - { - Checked = false, CheckType = MenuItemCheckStyle.Checked - }, - _miShowTabViewBorder = - new ( - "_Show TabView Border", - "", - ShowTabViewBorder - ) { Checked = true, CheckType = MenuItemCheckStyle.Checked } - } - ) - ] + BorderStyle = LineStyle.None }; - appWindow.Add (menu); - _tabView = new() + // MenuBar + MenuBar menu = new (); + + _tabView = new () { Title = "_Tab View", X = 0, - Y = 1, + Y = Pos.Bottom (menu), Width = 60, Height = 20, BorderStyle = LineStyle.Single }; - _tabView.AddTab (new() { DisplayText = "Tab_1", View = new Label { Text = "hodor!" } }, false); - _tabView.AddTab (new() { DisplayText = "Tab_2", View = new TextField { Text = "durdur", Width = 10 } }, false); - _tabView.AddTab (new() { DisplayText = "_Interactive Tab", View = GetInteractiveTab () }, false); - _tabView.AddTab (new() { DisplayText = "Big Text", View = GetBigTextFileTab () }, false); + _tabView.AddTab (new () { DisplayText = "Tab_1", View = new Label { Text = "hodor!" } }, false); + _tabView.AddTab (new () { DisplayText = "Tab_2", View = new TextField { Text = "durdur", Width = 10 } }, false); + _tabView.AddTab (new () { DisplayText = "_Interactive Tab", View = GetInteractiveTab () }, false); + _tabView.AddTab (new () { DisplayText = "Big Text", View = GetBigTextFileTab () }, false); _tabView.AddTab ( - new() + new () { DisplayText = "Long name Tab, I mean seriously long. Like you would not believe how long this tab's name is its just too much really woooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooowwww thats long", @@ -100,15 +58,16 @@ public class TabViewExample : Scenario ); _tabView.AddTab ( - new() + new () { - DisplayText = "Les Mise" + '\u0301' + "rables", View = new Label { Text = "This tab name is unicode" } + DisplayText = "Les Mise" + '\u0301' + "rables", + View = new Label { Text = "This tab name is unicode" } }, false ); _tabView.AddTab ( - new() + new () { DisplayText = "Les Mise" + '\u0328' + '\u0301' + "rables", View = new Label @@ -123,19 +82,17 @@ public class TabViewExample : Scenario for (var i = 0; i < 100; i++) { _tabView.AddTab ( - new() { DisplayText = $"Tab{i}", View = new Label { Text = $"Welcome to tab {i}" } }, + new () { DisplayText = $"Tab{i}", View = new Label { Text = $"Welcome to tab {i}" } }, false ); } _tabView.SelectedTab = _tabView.Tabs.First (); - appWindow.Add (_tabView); - - var frameRight = new View + View frameRight = new () { X = Pos.Right (_tabView), - Y = 1, + Y = Pos.Top (_tabView), Width = Dim.Fill (), Height = Dim.Fill (1), Title = "_About", @@ -147,16 +104,15 @@ public class TabViewExample : Scenario frameRight.Add ( new TextView { - Text = "This demos the tabs control\nSwitch between tabs using cursor keys.\nThis TextView has AllowsTab = false, so tab should nav too.", + Text = + "This demos the tabs control\nSwitch between tabs using cursor keys.\nThis TextView has AllowsTab = false, so tab should nav too.", Width = Dim.Fill (), Height = Dim.Fill (), - AllowsTab = false, + AllowsTab = false } ); - appWindow.Add (frameRight); - - var frameBelow = new View + View frameBelow = new () { X = 0, Y = Pos.Bottom (_tabView), @@ -166,7 +122,6 @@ public class TabViewExample : Scenario BorderStyle = LineStyle.Single, TabStop = TabBehavior.TabStop, CanFocus = true - }; frameBelow.Add ( @@ -175,31 +130,112 @@ public class TabViewExample : Scenario Text = "This frame exists to check that you can still tab here\nand that the tab control doesn't overspill it's bounds\nAllowsTab is true.", Width = Dim.Fill (), - Height = Dim.Fill (), + Height = Dim.Fill () } ); - appWindow.Add (frameBelow); + // StatusBar + StatusBar statusBar = new ( + [ + new (Application.QuitKey, "Quit", Quit) + ] + ); - var statusBar = new StatusBar ([new (Application.QuitKey, "Quit", Quit)]); - appWindow.Add (statusBar); + // Setup menu checkboxes + _miShowTopLineCheckBox = new () + { + Title = "_Show Top Line", + CheckedState = CheckState.Checked + }; + _miShowTopLineCheckBox.CheckedStateChanged += (s, e) => ShowTopLine (); + + _miShowBorderCheckBox = new () + { + Title = "_Show Border", + CheckedState = CheckState.Checked + }; + _miShowBorderCheckBox.CheckedStateChanged += (s, e) => ShowBorder (); + + _miTabsOnBottomCheckBox = new () + { + Title = "_Tabs On Bottom" + }; + _miTabsOnBottomCheckBox.CheckedStateChanged += (s, e) => SetTabsOnBottom (); + + _miShowTabViewBorderCheckBox = new () + { + Title = "_Show TabView Border", + CheckedState = CheckState.Checked + }; + _miShowTabViewBorderCheckBox.CheckedStateChanged += (s, e) => ShowTabViewBorder (); + + menu.Add ( + new MenuBarItem ( + "_File", + [ + new MenuItem + { + Title = "_Add Blank Tab", + Action = AddBlankTab + }, + new MenuItem + { + Title = "_Clear SelectedTab", + Action = () => + { + if (_tabView is { }) + { + _tabView.SelectedTab = null; + } + } + }, + new MenuItem + { + Title = "_Quit", + Action = Quit + } + ] + ) + ); + + menu.Add ( + new MenuBarItem ( + "_View", + [ + new MenuItem + { + CommandView = _miShowTopLineCheckBox + }, + new MenuItem + { + CommandView = _miShowBorderCheckBox + }, + new MenuItem + { + CommandView = _miTabsOnBottomCheckBox + }, + new MenuItem + { + CommandView = _miShowTabViewBorderCheckBox + } + ] + ) + ); + + appWindow.Add (menu, _tabView, frameRight, frameBelow, statusBar); - // Run - Start the application. Application.Run (appWindow); - appWindow.Dispose (); - - // Shutdown - Calling Application.Shutdown is required. Application.Shutdown (); } - private void AddBlankTab () { _tabView.AddTab (new (), false); } + private void AddBlankTab () { _tabView?.AddTab (new (), false); } private View GetBigTextFileTab () { - var text = new TextView { Width = Dim.Fill (), Height = Dim.Fill () }; + TextView text = new () { Width = Dim.Fill (), Height = Dim.Fill () }; - var sb = new StringBuilder (); + StringBuilder sb = new (); for (var y = 0; y < 300; y++) { @@ -218,21 +254,22 @@ public class TabViewExample : Scenario private View GetInteractiveTab () { - var interactiveTab = new View + View interactiveTab = new () { - Width = Dim.Fill (), Height = Dim.Fill (), + Width = Dim.Fill (), + Height = Dim.Fill (), CanFocus = true }; - var lblName = new Label { Text = "Name:" }; + Label lblName = new () { Text = "Name:" }; interactiveTab.Add (lblName); - var tbName = new TextField { X = Pos.Right (lblName), Width = 10 }; + TextField tbName = new () { X = Pos.Right (lblName), Width = 10 }; interactiveTab.Add (tbName); - var lblAddr = new Label { Y = 1, Text = "Address:" }; + Label lblAddr = new () { Y = 1, Text = "Address:" }; interactiveTab.Add (lblAddr); - var tbAddr = new TextField { X = Pos.Right (lblAddr), Y = 1, Width = 10 }; + TextField tbAddr = new () { X = Pos.Right (lblAddr), Y = 1, Width = 10 }; interactiveTab.Add (tbAddr); return interactiveTab; @@ -242,35 +279,47 @@ public class TabViewExample : Scenario private void SetTabsOnBottom () { - _miTabsOnBottom.Checked = !_miTabsOnBottom.Checked; + if (_tabView is null || _miTabsOnBottomCheckBox is null) + { + return; + } - _tabView.Style.TabsOnBottom = (bool)_miTabsOnBottom.Checked; + _tabView.Style.TabsOnBottom = _miTabsOnBottomCheckBox.CheckedState == CheckState.Checked; _tabView.ApplyStyleChanges (); } private void ShowBorder () { - _miShowBorder.Checked = !_miShowBorder.Checked; + if (_tabView is null || _miShowBorderCheckBox is null) + { + return; + } - _tabView.Style.ShowBorder = (bool)_miShowBorder.Checked; + _tabView.Style.ShowBorder = _miShowBorderCheckBox.CheckedState == CheckState.Checked; _tabView.ApplyStyleChanges (); } private void ShowTabViewBorder () { - _miShowTabViewBorder.Checked = !_miShowTabViewBorder.Checked; + if (_tabView is null || _miShowTabViewBorderCheckBox is null) + { + return; + } - _tabView.BorderStyle = _miShowTabViewBorder.Checked == true - ? _tabView.BorderStyle = LineStyle.Single + _tabView.BorderStyle = _miShowTabViewBorderCheckBox.CheckedState == CheckState.Checked + ? LineStyle.Single : LineStyle.None; _tabView.ApplyStyleChanges (); } private void ShowTopLine () { - _miShowTopLine.Checked = !_miShowTopLine.Checked; + if (_tabView is null || _miShowTopLineCheckBox is null) + { + return; + } - _tabView.Style.ShowTopLine = (bool)_miShowTopLine.Checked; + _tabView.Style.ShowTopLine = _miShowTopLineCheckBox.CheckedState == CheckState.Checked; _tabView.ApplyStyleChanges (); } } diff --git a/Examples/UICatalog/Scenarios/TableEditor.cs b/Examples/UICatalog/Scenarios/TableEditor.cs index 57b1f9a60..12ab5e9d8 100644 --- a/Examples/UICatalog/Scenarios/TableEditor.cs +++ b/Examples/UICatalog/Scenarios/TableEditor.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Data; using System.Globalization; using System.Text; @@ -458,22 +458,6 @@ public class TableEditor : Scenario private Scheme? _alternatingScheme; private DataTable? _currentTable; - private MenuItem? _miAlternatingColors; - private MenuItem? _miAlwaysShowHeaders; - private MenuItem? _miAlwaysUseNormalColorForVerticalCellLines; - private MenuItem? _miBottomline; - private MenuItem? _miCellLines; - private MenuItem? _miCheckboxes; - private MenuItem? _miCursor; - private MenuItem? _miExpandLastColumn; - private MenuItem? _miFullRowSelect; - private MenuItem? _miHeaderMidline; - private MenuItem? _miHeaderOverline; - private MenuItem? _miHeaderUnderline; - private MenuItem? _miRadioboxes; - private MenuItem? _miShowHeaders; - private MenuItem? _miShowHorizontalScrollIndicators; - private MenuItem? _miSmoothScrolling; private Scheme? _redScheme; private Scheme? _redSchemeAlt; private TableView? _tableView; @@ -519,239 +503,38 @@ public class TableEditor : Scenario _tableView = new () { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1) }; - var menu = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ( - "_OpenBigExample", - "", - () => OpenExample (true) - ), - new ( - "_OpenSmallExample", - "", - () => OpenExample (false) - ), - new ( - "OpenCharacter_Map", - "", - () => OpenUnicodeMap () - ), - new ( - "OpenTreeExample", - "", - () => OpenTreeExample () - ), - new ( - "_CloseExample", - "", - () => CloseExample () - ), - new ("_Quit", "", () => Quit ()) - } - ), - new ( - "_View", - new [] - { - _miShowHeaders = - new ( - "_ShowHeaders", - "", - () => ToggleShowHeaders () - ) - { - Checked = _tableView!.Style.ShowHeaders, - CheckType = MenuItemCheckStyle.Checked - }, - _miAlwaysShowHeaders = - new ( - "_AlwaysShowHeaders", - "", - () => ToggleAlwaysShowHeaders () - ) - { - Checked = _tableView!.Style.AlwaysShowHeaders, - CheckType = MenuItemCheckStyle.Checked - }, - _miHeaderOverline = - new ( - "_HeaderOverLine", - "", - () => ToggleOverline () - ) - { - Checked = _tableView!.Style - .ShowHorizontalHeaderOverline, - CheckType = MenuItemCheckStyle.Checked - }, - _miHeaderMidline = new ( - "_HeaderMidLine", - "", - () => ToggleHeaderMidline () - ) - { - Checked = _tableView!.Style - .ShowVerticalHeaderLines, - CheckType = MenuItemCheckStyle.Checked - }, - _miHeaderUnderline = new ( - "_HeaderUnderLine", - "", - () => ToggleUnderline () - ) - { - Checked = _tableView!.Style - .ShowHorizontalHeaderUnderline, - CheckType = MenuItemCheckStyle.Checked - }, - _miBottomline = new ( - "_BottomLine", - "", - () => ToggleBottomline () - ) - { - Checked = _tableView!.Style - .ShowHorizontalBottomline, - CheckType = MenuItemCheckStyle - .Checked - }, - _miShowHorizontalScrollIndicators = - new ( - "_HorizontalScrollIndicators", - "", - () => - ToggleHorizontalScrollIndicators () - ) - { - Checked = _tableView!.Style - .ShowHorizontalScrollIndicators, - CheckType = MenuItemCheckStyle.Checked - }, - _miFullRowSelect = new ( - "_FullRowSelect", - "", - () => ToggleFullRowSelect () - ) - { - Checked = _tableView!.FullRowSelect, - CheckType = MenuItemCheckStyle.Checked - }, - _miCellLines = new ( - "_CellLines", - "", - () => ToggleCellLines () - ) - { - Checked = _tableView!.Style - .ShowVerticalCellLines, - CheckType = MenuItemCheckStyle - .Checked - }, - _miExpandLastColumn = - new ( - "_ExpandLastColumn", - "", - () => ToggleExpandLastColumn () - ) - { - Checked = _tableView!.Style.ExpandLastColumn, - CheckType = MenuItemCheckStyle.Checked - }, - _miAlwaysUseNormalColorForVerticalCellLines = - new ( - "_AlwaysUseNormalColorForVerticalCellLines", - "", - () => - ToggleAlwaysUseNormalColorForVerticalCellLines () - ) - { - Checked = _tableView!.Style - .AlwaysUseNormalColorForVerticalCellLines, - CheckType = MenuItemCheckStyle.Checked - }, - _miSmoothScrolling = - new ( - "_SmoothHorizontalScrolling", - "", - () => ToggleSmoothScrolling () - ) - { - Checked = _tableView!.Style - .SmoothHorizontalScrolling, - CheckType = MenuItemCheckStyle.Checked - }, - new ("_AllLines", "", () => ToggleAllCellLines ()), - new ("_NoLines", "", () => ToggleNoCellLines ()), - _miCheckboxes = new ( - "_Checkboxes", - "", - () => ToggleCheckboxes (false) - ) - { - Checked = false, - CheckType = MenuItemCheckStyle.Checked - }, - _miRadioboxes = new ( - "_Radioboxes", - "", - () => ToggleCheckboxes (true) - ) - { - Checked = false, - CheckType = MenuItemCheckStyle.Checked - }, - _miAlternatingColors = - new ( - "Alternating Colors", - "", - () => ToggleAlternatingColors () - ) { CheckType = MenuItemCheckStyle.Checked }, - _miCursor = - new ( - "Invert Selected Cell First Character", - "", - () => - ToggleInvertSelectedCellFirstCharacter () - ) - { - Checked = _tableView!.Style - .InvertSelectedCellFirstCharacter, - CheckType = MenuItemCheckStyle.Checked - }, - new ( - "_ClearColumnStyles", - "", - () => ClearColumnStyles () - ), - new ("Sho_w All Columns", "", () => ShowAllColumns ()) - } - ), - new ( - "_Column", - new MenuItem [] - { - new ("_Set Max Width", "", SetMaxWidth), - new ("_Set Min Width", "", SetMinWidth), - new ( - "_Set MinAcceptableWidth", - "", - SetMinAcceptableWidth - ), - new ( - "_Set All MinAcceptableWidth=1", - "", - SetMinAcceptableWidthToOne - ) - } - ) - ] - }; + var menu = new MenuBar (); + + // File menu + menu.Add ( + new MenuBarItem ( + "_File", + [ + new MenuItem { Title = "_OpenBigExample", Action = () => OpenExample (true) }, + new MenuItem { Title = "_OpenSmallExample", Action = () => OpenExample (false) }, + new MenuItem { Title = "OpenCharacter_Map", Action = OpenUnicodeMap }, + new MenuItem { Title = "OpenTreeExample", Action = OpenTreeExample }, + new MenuItem { Title = "_CloseExample", Action = CloseExample }, + new MenuItem { Title = "_Quit", Action = Quit } + ] + ) + ); + + // View menu - created with helper method due to complexity + menu.Add (CreateViewMenu ()); + + // Column menu + menu.Add ( + new MenuBarItem ( + "_Column", + [ + new MenuItem { Title = "_Set Max Width", Action = SetMaxWidth }, + new MenuItem { Title = "_Set Min Width", Action = SetMinWidth }, + new MenuItem { Title = "_Set MinAcceptableWidth", Action = SetMinAcceptableWidth }, + new MenuItem { Title = "_Set All MinAcceptableWidth=1", Action = SetMinAcceptableWidthToOne } + ] + ) + ); appWindow.Add (menu); @@ -828,28 +611,28 @@ public class TableEditor : Scenario // if user clicks the mouse in TableView _tableView!.MouseClick += (s, e) => - { - if (_currentTable == null) - { - return; - } + { + if (_currentTable == null) + { + return; + } - _tableView!.ScreenToCell (e.Position, out int? clickedCol); + _tableView!.ScreenToCell (e.Position, out int? clickedCol); - if (clickedCol != null) - { - if (e.Flags.HasFlag (MouseFlags.Button1Clicked)) - { - // left click in a header - SortColumn (clickedCol.Value); - } - else if (e.Flags.HasFlag (MouseFlags.Button3Clicked)) - { - // right click in a header - ShowHeaderContextMenu (clickedCol.Value, e); - } - } - }; + if (clickedCol != null) + { + if (e.Flags.HasFlag (MouseFlags.Button1Clicked)) + { + // left click in a header + SortColumn (clickedCol.Value); + } + else if (e.Flags.HasFlag (MouseFlags.Button3Clicked)) + { + // right click in a header + ShowHeaderContextMenu (clickedCol.Value, e); + } + } + }; _tableView!.KeyBindings.ReplaceCommands (Key.Space, Command.Accept); @@ -861,6 +644,261 @@ public class TableEditor : Scenario Application.Shutdown (); } + private MenuBarItem CreateViewMenu () + { + // Store checkbox references for the toggle methods to access + Dictionary checkboxes = new (); + + MenuItem CreateCheckBoxMenuItem (string key, string title, bool initialState, Action onToggle) + { + CheckBox checkBox = new () + { + Title = title, + CheckedState = initialState ? CheckState.Checked : CheckState.UnChecked + }; + + checkBox.CheckedStateChanged += (s, e) => onToggle (checkBox.CheckedState == CheckState.Checked); + + MenuItem item = new () { CommandView = checkBox }; + + item.Accepting += (s, e) => + { + checkBox.AdvanceCheckState (); + e.Handled = true; + }; + + checkboxes [key] = checkBox; + + return item; + } + + return new ( + "_View", + [ + CreateCheckBoxMenuItem ( + "ShowHeaders", + "_ShowHeaders", + _tableView!.Style.ShowHeaders, + state => + { + _tableView!.Style.ShowHeaders = state; + _tableView!.Update (); + } + ), + CreateCheckBoxMenuItem ( + "AlwaysShowHeaders", + "_AlwaysShowHeaders", + _tableView!.Style.AlwaysShowHeaders, + state => + { + _tableView!.Style.AlwaysShowHeaders = state; + _tableView!.Update (); + } + ), + CreateCheckBoxMenuItem ( + "HeaderOverline", + "_HeaderOverLine", + _tableView!.Style.ShowHorizontalHeaderOverline, + state => + { + _tableView!.Style.ShowHorizontalHeaderOverline = state; + _tableView!.Update (); + } + ), + CreateCheckBoxMenuItem ( + "HeaderMidline", + "_HeaderMidLine", + _tableView!.Style.ShowVerticalHeaderLines, + state => + { + _tableView!.Style.ShowVerticalHeaderLines = state; + _tableView!.Update (); + } + ), + CreateCheckBoxMenuItem ( + "HeaderUnderline", + "_HeaderUnderLine", + _tableView!.Style.ShowHorizontalHeaderUnderline, + state => + { + _tableView!.Style.ShowHorizontalHeaderUnderline = state; + _tableView!.Update (); + } + ), + CreateCheckBoxMenuItem ( + "Bottomline", + "_BottomLine", + _tableView!.Style.ShowHorizontalBottomline, + state => + { + _tableView!.Style.ShowHorizontalBottomline = state; + _tableView!.Update (); + } + ), + CreateCheckBoxMenuItem ( + "HorizontalScrollIndicators", + "_HorizontalScrollIndicators", + _tableView!.Style.ShowHorizontalScrollIndicators, + state => + { + _tableView!.Style.ShowHorizontalScrollIndicators = state; + _tableView!.Update (); + } + ), + CreateCheckBoxMenuItem ( + "FullRowSelect", + "_FullRowSelect", + _tableView!.FullRowSelect, + state => + { + _tableView!.FullRowSelect = state; + _tableView!.Update (); + } + ), + CreateCheckBoxMenuItem ( + "CellLines", + "_CellLines", + _tableView!.Style.ShowVerticalCellLines, + state => + { + _tableView!.Style.ShowVerticalCellLines = state; + _tableView!.Update (); + } + ), + CreateCheckBoxMenuItem ( + "ExpandLastColumn", + "_ExpandLastColumn", + _tableView!.Style.ExpandLastColumn, + state => + { + _tableView!.Style.ExpandLastColumn = state; + _tableView!.Update (); + } + ), + CreateCheckBoxMenuItem ( + "AlwaysUseNormalColorForVerticalCellLines", + "_AlwaysUseNormalColorForVerticalCellLines", + _tableView!.Style.AlwaysUseNormalColorForVerticalCellLines, + state => + { + _tableView!.Style.AlwaysUseNormalColorForVerticalCellLines = state; + _tableView!.Update (); + } + ), + CreateCheckBoxMenuItem ( + "SmoothScrolling", + "_SmoothHorizontalScrolling", + _tableView!.Style.SmoothHorizontalScrolling, + state => + { + _tableView!.Style.SmoothHorizontalScrolling = state; + _tableView!.Update (); + } + ), + new MenuItem + { + Title = "_AllLines", + Action = () => + { + _tableView!.Style.ShowHorizontalHeaderOverline = true; + _tableView!.Style.ShowVerticalHeaderLines = true; + _tableView!.Style.ShowHorizontalHeaderUnderline = true; + _tableView!.Style.ShowVerticalCellLines = true; + + checkboxes ["HeaderOverline"].CheckedState = CheckState.Checked; + checkboxes ["HeaderMidline"].CheckedState = CheckState.Checked; + checkboxes ["HeaderUnderline"].CheckedState = CheckState.Checked; + checkboxes ["CellLines"].CheckedState = CheckState.Checked; + + _tableView!.Update (); + } + }, + new MenuItem + { + Title = "_NoLines", + Action = () => + { + _tableView!.Style.ShowHorizontalHeaderOverline = false; + _tableView!.Style.ShowVerticalHeaderLines = false; + _tableView!.Style.ShowHorizontalHeaderUnderline = false; + _tableView!.Style.ShowVerticalCellLines = false; + + checkboxes ["HeaderOverline"].CheckedState = CheckState.UnChecked; + checkboxes ["HeaderMidline"].CheckedState = CheckState.UnChecked; + checkboxes ["HeaderUnderline"].CheckedState = CheckState.UnChecked; + checkboxes ["CellLines"].CheckedState = CheckState.UnChecked; + + _tableView!.Update (); + } + }, + CreateCheckBoxMenuItem ( + "Checkboxes", + "_Checkboxes", + false, + state => + { + if (state) + { + ToggleCheckboxes (false); + checkboxes ["Radioboxes"].CheckedState = CheckState.UnChecked; + } + else if (HasCheckboxes ()) + { + ToggleCheckboxes (false); + } + } + ), + CreateCheckBoxMenuItem ( + "Radioboxes", + "_Radioboxes", + false, + state => + { + if (state) + { + ToggleCheckboxes (true); + checkboxes ["Checkboxes"].CheckedState = CheckState.UnChecked; + } + else if (HasCheckboxes ()) + { + ToggleCheckboxes (true); + } + } + ), + CreateCheckBoxMenuItem ( + "AlternatingColors", + "Alternating Colors", + false, + state => + { + if (state) + { + _tableView!.Style.RowColorGetter = a => { return a.RowIndex % 2 == 0 ? _alternatingScheme : null; }; + } + else + { + _tableView!.Style.RowColorGetter = null; + } + + _tableView!.SetNeedsDraw (); + } + ), + CreateCheckBoxMenuItem ( + "Cursor", + "Invert Selected Cell First Character", + _tableView!.Style.InvertSelectedCellFirstCharacter, + state => + { + _tableView!.Style.InvertSelectedCellFirstCharacter = state; + _tableView!.SetNeedsDraw (); + } + ), + new MenuItem { Title = "_ClearColumnStyles", Action = ClearColumnStyles }, + new MenuItem { Title = "Sho_w All Columns", Action = ShowAllColumns } + ] + ); + } + protected override void Dispose (bool disposing) { base.Dispose (disposing); @@ -1078,7 +1116,7 @@ public class TableEditor : Scenario } private string GetUnicodeCategory (uint u) { return _ranges!.FirstOrDefault (r => u >= r.Start && u <= r.End)?.Category ?? "Unknown"; } - private bool HasCheckboxes () { return _tableView!.Table is CheckBoxTableSourceWrapperBase; } + private bool HasCheckboxes () => _tableView!.Table is CheckBoxTableSourceWrapperBase; private void HideColumn (int clickedCol) { @@ -1236,7 +1274,6 @@ public class TableEditor : Scenario // color 0 and negative values red d <= 0.0000001 ? a.RowIndex % 2 == 0 - && _miAlternatingColors!.Checked == true ? _redSchemeAlt : _redScheme : @@ -1414,7 +1451,7 @@ public class TableEditor : Scenario _tableView!.Update (); } - private string StripArrows (string columnName) { return columnName.Replace ($"{Glyphs.DownArrow}", "").Replace ($"{Glyphs.UpArrow}", ""); } + private string StripArrows (string columnName) => columnName.Replace ($"{Glyphs.DownArrow}", "").Replace ($"{Glyphs.UpArrow}", ""); private void TableViewKeyPress (object? sender, Key e) { @@ -1429,9 +1466,9 @@ public class TableEditor : Scenario { // Delete button deletes all rows when in full row mode foreach (int toRemove in _tableView!.GetAllSelectedCells () - .Select (p => p.Y) - .Distinct () - .OrderByDescending (i => i)) + .Select (p => p.Y) + .Distinct () + .OrderByDescending (i => i)) { _currentTable.Rows.RemoveAt (toRemove); } @@ -1450,70 +1487,6 @@ public class TableEditor : Scenario } } - private void ToggleAllCellLines () - { - _tableView!.Style.ShowHorizontalHeaderOverline = true; - _tableView!.Style.ShowVerticalHeaderLines = true; - _tableView!.Style.ShowHorizontalHeaderUnderline = true; - _tableView!.Style.ShowVerticalCellLines = true; - - _miHeaderOverline!.Checked = true; - _miHeaderMidline!.Checked = true; - _miHeaderUnderline!.Checked = true; - _miCellLines!.Checked = true; - - _tableView!.Update (); - } - - private void ToggleAlternatingColors () - { - //toggle menu item - _miAlternatingColors!.Checked = !_miAlternatingColors.Checked; - - if (_miAlternatingColors.Checked == true) - { - _tableView!.Style.RowColorGetter = a => { return a.RowIndex % 2 == 0 ? _alternatingScheme : null; }; - } - else - { - _tableView!.Style.RowColorGetter = null; - } - - _tableView!.SetNeedsDraw (); - } - - private void ToggleAlwaysShowHeaders () - { - _miAlwaysShowHeaders!.Checked = !_miAlwaysShowHeaders.Checked; - _tableView!.Style.AlwaysShowHeaders = (bool)_miAlwaysShowHeaders.Checked!; - _tableView!.Update (); - } - - private void ToggleAlwaysUseNormalColorForVerticalCellLines () - { - _miAlwaysUseNormalColorForVerticalCellLines!.Checked = - !_miAlwaysUseNormalColorForVerticalCellLines.Checked; - - _tableView!.Style.AlwaysUseNormalColorForVerticalCellLines = - (bool)_miAlwaysUseNormalColorForVerticalCellLines.Checked!; - - _tableView!.Update (); - } - - private void ToggleBottomline () - { - _miBottomline!.Checked = !_miBottomline.Checked; - _tableView!.Style.ShowHorizontalBottomline = (bool)_miBottomline.Checked!; - _tableView!.Update (); - } - - private void ToggleCellLines () - { - _miCellLines!.Checked = !_miCellLines.Checked; - _tableView!.Style.ShowVerticalCellLines = (bool)_miCellLines.Checked!; - _tableView!.Update (); - } - private void ToggleCheckboxes (bool radio) { if (_tableView!.Table is CheckBoxTableSourceWrapperBase wrapper) @@ -1521,9 +1494,6 @@ public class TableEditor : Scenario // unwrap it to remove check boxes _tableView!.Table = wrapper.Wrapping; - _miCheckboxes!.Checked = false; - _miRadioboxes!.Checked = false; - // if toggling off checkboxes/radio if (wrapper.UseRadioButtons == radio) { @@ -1542,7 +1512,7 @@ public class TableEditor : Scenario _checkedFileSystemInfos!.Contains, CheckOrUncheckFile ) - { UseRadioButtons = radio }; + { UseRadioButtons = radio }; } else { @@ -1550,98 +1520,6 @@ public class TableEditor : Scenario } _tableView!.Table = source; - - if (radio) - { - _miRadioboxes!.Checked = true; - _miCheckboxes!.Checked = false; - } - else - { - _miRadioboxes!.Checked = false; - _miCheckboxes!.Checked = true; - } - } - - private void ToggleExpandLastColumn () - { - _miExpandLastColumn!.Checked = !_miExpandLastColumn.Checked; - _tableView!.Style.ExpandLastColumn = (bool)_miExpandLastColumn.Checked!; - - _tableView!.Update (); - } - - private void ToggleFullRowSelect () - { - _miFullRowSelect!.Checked = !_miFullRowSelect.Checked; - _tableView!.FullRowSelect = (bool)_miFullRowSelect.Checked!; - _tableView!.Update (); - } - - private void ToggleHeaderMidline () - { - _miHeaderMidline!.Checked = !_miHeaderMidline.Checked; - _tableView!.Style.ShowVerticalHeaderLines = (bool)_miHeaderMidline.Checked!; - _tableView!.Update (); - } - - private void ToggleHorizontalScrollIndicators () - { - _miShowHorizontalScrollIndicators!.Checked = !_miShowHorizontalScrollIndicators.Checked; - _tableView!.Style.ShowHorizontalScrollIndicators = (bool)_miShowHorizontalScrollIndicators.Checked!; - _tableView!.Update (); - } - - private void ToggleInvertSelectedCellFirstCharacter () - { - //toggle menu item - _miCursor!.Checked = !_miCursor.Checked; - _tableView!.Style.InvertSelectedCellFirstCharacter = (bool)_miCursor.Checked!; - _tableView!.SetNeedsDraw (); - } - - private void ToggleNoCellLines () - { - _tableView!.Style.ShowHorizontalHeaderOverline = false; - _tableView!.Style.ShowVerticalHeaderLines = false; - _tableView!.Style.ShowHorizontalHeaderUnderline = false; - _tableView!.Style.ShowVerticalCellLines = false; - - _miHeaderOverline!.Checked = false; - _miHeaderMidline!.Checked = false; - _miHeaderUnderline!.Checked = false; - _miCellLines!.Checked = false; - - _tableView!.Update (); - } - - private void ToggleOverline () - { - _miHeaderOverline!.Checked = !_miHeaderOverline.Checked; - _tableView!.Style.ShowHorizontalHeaderOverline = (bool)_miHeaderOverline.Checked!; - _tableView!.Update (); - } - - private void ToggleShowHeaders () - { - _miShowHeaders!.Checked = !_miShowHeaders.Checked; - _tableView!.Style.ShowHeaders = (bool)_miShowHeaders.Checked!; - _tableView!.Update (); - } - - private void ToggleSmoothScrolling () - { - _miSmoothScrolling!.Checked = !_miSmoothScrolling.Checked; - _tableView!.Style.SmoothHorizontalScrolling = (bool)_miSmoothScrolling.Checked!; - - _tableView!.Update (); - } - - private void ToggleUnderline () - { - _miHeaderUnderline!.Checked = !_miHeaderUnderline.Checked; - _tableView!.Style.ShowHorizontalHeaderUnderline = (bool)_miHeaderUnderline.Checked!; - _tableView!.Update (); } private int ToTableCol (int col) @@ -1654,13 +1532,11 @@ public class TableEditor : Scenario return col; } - private string TrimArrows (string columnName) - { - return columnName.TrimEnd ( - (char)Glyphs.UpArrow.Value, - (char)Glyphs.DownArrow.Value - ); - } + private string TrimArrows (string columnName) => + columnName.TrimEnd ( + (char)Glyphs.UpArrow.Value, + (char)Glyphs.DownArrow.Value + ); public class UnicodeRange (uint start, uint end, string category) { diff --git a/Examples/UICatalog/Scenarios/TextViewAutocompletePopup.cs b/Examples/UICatalog/Scenarios/TextViewAutocompletePopup.cs index 934cc60f6..a48c9d683 100644 --- a/Examples/UICatalog/Scenarios/TextViewAutocompletePopup.cs +++ b/Examples/UICatalog/Scenarios/TextViewAutocompletePopup.cs @@ -1,4 +1,5 @@ -using System.Linq; +#nullable enable + using System.Text.RegularExpressions; namespace UICatalog.Scenarios; @@ -10,77 +11,63 @@ namespace UICatalog.Scenarios; public class TextViewAutocompletePopup : Scenario { private int _height = 10; - private MenuItem _miMultiline; - private MenuItem _miWrap; - private Shortcut _siMultiline; - private Shortcut _siWrap; - private TextView _textViewBottomLeft; - private TextView _textViewBottomRight; - private TextView _textViewCentered; - private TextView _textViewTopLeft; - private TextView _textViewTopRight; + private CheckBox? _miMultilineCheckBox; + private CheckBox? _miWrapCheckBox; + private Shortcut? _siMultiline; + private Shortcut? _siWrap; + private TextView? _textViewBottomLeft; + private TextView? _textViewBottomRight; + private TextView? _textViewCentered; + private TextView? _textViewTopLeft; + private TextView? _textViewTopRight; public override void Main () { - // Init Application.Init (); - // Setup - Create a top-level application window and configure it. - Toplevel appWindow = new (); + Window appWindow = new () + { + BorderStyle = LineStyle.None + }; var width = 20; var text = " jamp jemp jimp jomp jump"; - var menu = new MenuBar - { - Menus = - [ - new ( - "_File", - new [] - { - _miMultiline = - new ( - "_Multiline", - "", - () => Multiline () - ) { CheckType = MenuItemCheckStyle.Checked }, - _miWrap = new ( - "_Word Wrap", - "", - () => WordWrap () - ) { CheckType = MenuItemCheckStyle.Checked }, - new ("_Quit", "", () => Quit ()) - } - ) - ] - }; - appWindow.Add (menu); + // MenuBar + MenuBar menu = new (); - _textViewTopLeft = new() + _textViewTopLeft = new () { - Y = 1, - Width = width, Height = _height, Text = text + Y = Pos.Bottom (menu), + Width = width, + Height = _height, + Text = text }; _textViewTopLeft.DrawingContent += TextViewTopLeft_DrawContent; appWindow.Add (_textViewTopLeft); - _textViewTopRight = new() + _textViewTopRight = new () { - X = Pos.AnchorEnd (width), Y = 1, - Width = width, Height = _height, Text = text + X = Pos.AnchorEnd (width), + Y = Pos.Bottom (menu), + Width = width, + Height = _height, + Text = text }; _textViewTopRight.DrawingContent += TextViewTopRight_DrawContent; appWindow.Add (_textViewTopRight); - _textViewBottomLeft = new() + _textViewBottomLeft = new () { - Y = Pos.AnchorEnd (_height), Width = width, Height = _height, Text = text + Y = Pos.AnchorEnd (_height), + Width = width, + Height = _height, + Text = text }; _textViewBottomLeft.DrawingContent += TextViewBottomLeft_DrawContent; appWindow.Add (_textViewBottomLeft); - _textViewBottomRight = new() + _textViewBottomRight = new () { X = Pos.AnchorEnd (width), Y = Pos.AnchorEnd (_height), @@ -91,7 +78,7 @@ public class TextViewAutocompletePopup : Scenario _textViewBottomRight.DrawingContent += TextViewBottomRight_DrawContent; appWindow.Add (_textViewBottomRight); - _textViewCentered = new() + _textViewCentered = new () { X = Pos.Center (), Y = Pos.Center (), @@ -102,73 +89,170 @@ public class TextViewAutocompletePopup : Scenario _textViewCentered.DrawingContent += TextViewCentered_DrawContent; appWindow.Add (_textViewCentered); - _miMultiline.Checked = _textViewTopLeft.Multiline; - _miWrap.Checked = _textViewTopLeft.WordWrap; + // Setup menu checkboxes + _miMultilineCheckBox = new () + { + Title = "_Multiline", + CheckedState = _textViewTopLeft.Multiline ? CheckState.Checked : CheckState.UnChecked + }; + _miMultilineCheckBox.CheckedStateChanged += (s, e) => Multiline (); - var statusBar = new StatusBar ( - new [] + _miWrapCheckBox = new () + { + Title = "_Word Wrap", + CheckedState = _textViewTopLeft.WordWrap ? CheckState.Checked : CheckState.UnChecked + }; + _miWrapCheckBox.CheckedStateChanged += (s, e) => WordWrap (); + + menu.Add ( + new MenuBarItem ( + "_File", + [ + new MenuItem { - new ( - Application.QuitKey, - "Quit", - () => Quit () - ), - _siMultiline = new (Key.Empty, "", null), - _siWrap = new (Key.Empty, "", null) + CommandView = _miMultilineCheckBox + }, + new MenuItem + { + CommandView = _miWrapCheckBox + }, + new MenuItem + { + Title = "_Quit", + Action = Quit } - ); - appWindow.Add (statusBar); + ] + ) + ); + + // StatusBar + _siMultiline = new (Key.Empty, "", null); + _siWrap = new (Key.Empty, "", null); + + StatusBar statusBar = new ( + [ + new ( + Application.QuitKey, + "Quit", + () => Quit () + ), + _siMultiline, + _siWrap + ] + ); + + appWindow.Add (menu, statusBar); appWindow.SubViewLayout += Win_LayoutStarted; - // Run - Start the application. Application.Run (appWindow); - appWindow.Dispose (); - - // Shutdown - Calling Application.Shutdown is required. Application.Shutdown (); } private void Multiline () { - _miMultiline.Checked = !_miMultiline.Checked; + if (_miMultilineCheckBox is null + || _textViewTopLeft is null + || _textViewTopRight is null + || _textViewBottomLeft is null + || _textViewBottomRight is null + || _textViewCentered is null) + { + return; + } + SetMultilineStatusText (); - _textViewTopLeft.Multiline = (bool)_miMultiline.Checked; - _textViewTopRight.Multiline = (bool)_miMultiline.Checked; - _textViewBottomLeft.Multiline = (bool)_miMultiline.Checked; - _textViewBottomRight.Multiline = (bool)_miMultiline.Checked; - _textViewCentered.Multiline = (bool)_miMultiline.Checked; + _textViewTopLeft.Multiline = _miMultilineCheckBox.CheckedState == CheckState.Checked; + _textViewTopRight.Multiline = _miMultilineCheckBox.CheckedState == CheckState.Checked; + _textViewBottomLeft.Multiline = _miMultilineCheckBox.CheckedState == CheckState.Checked; + _textViewBottomRight.Multiline = _miMultilineCheckBox.CheckedState == CheckState.Checked; + _textViewCentered.Multiline = _miMultilineCheckBox.CheckedState == CheckState.Checked; } private void Quit () { Application.RequestStop (); } private void SetAllSuggestions (TextView view) { - ((SingleWordSuggestionGenerator)view.Autocomplete.SuggestionGenerator).AllSuggestions = Regex - .Matches (view.Text, "\\w+") - .Select (s => s.Value) - .Distinct () - .ToList (); + if (view.Autocomplete.SuggestionGenerator is SingleWordSuggestionGenerator generator) + { + generator.AllSuggestions = Regex + .Matches (view.Text, "\\w+") + .Select (s => s.Value) + .Distinct () + .ToList (); + } } - private void SetMultilineStatusText () { _siMultiline.Title = $"Multiline: {_miMultiline.Checked}"; } - - private void SetWrapStatusText () { _siWrap.Title = $"WordWrap: {_miWrap.Checked}"; } - private void TextViewBottomLeft_DrawContent (object sender, DrawEventArgs e) { SetAllSuggestions (_textViewBottomLeft); } - private void TextViewBottomRight_DrawContent (object sender, DrawEventArgs e) { SetAllSuggestions (_textViewBottomRight); } - private void TextViewCentered_DrawContent (object sender, DrawEventArgs e) { SetAllSuggestions (_textViewCentered); } - private void TextViewTopLeft_DrawContent (object sender, DrawEventArgs e) { SetAllSuggestions (_textViewTopLeft); } - private void TextViewTopRight_DrawContent (object sender, DrawEventArgs e) { SetAllSuggestions (_textViewTopRight); } - - private void Win_LayoutStarted (object sender, LayoutEventArgs obj) + private void SetMultilineStatusText () { - _miMultiline.Checked = _textViewTopLeft.Multiline; - _miWrap.Checked = _textViewTopLeft.WordWrap; + if (_siMultiline is { } && _miMultilineCheckBox is { }) + { + _siMultiline.Title = $"Multiline: {_miMultilineCheckBox.CheckedState == CheckState.Checked}"; + } + } + + private void SetWrapStatusText () + { + if (_siWrap is { } && _miWrapCheckBox is { }) + { + _siWrap.Title = $"WordWrap: {_miWrapCheckBox.CheckedState == CheckState.Checked}"; + } + } + + private void TextViewBottomLeft_DrawContent (object? sender, DrawEventArgs e) + { + if (_textViewBottomLeft is { }) + { + SetAllSuggestions (_textViewBottomLeft); + } + } + + private void TextViewBottomRight_DrawContent (object? sender, DrawEventArgs e) + { + if (_textViewBottomRight is { }) + { + SetAllSuggestions (_textViewBottomRight); + } + } + + private void TextViewCentered_DrawContent (object? sender, DrawEventArgs e) + { + if (_textViewCentered is { }) + { + SetAllSuggestions (_textViewCentered); + } + } + + private void TextViewTopLeft_DrawContent (object? sender, DrawEventArgs e) + { + if (_textViewTopLeft is { }) + { + SetAllSuggestions (_textViewTopLeft); + } + } + + private void TextViewTopRight_DrawContent (object? sender, DrawEventArgs e) + { + if (_textViewTopRight is { }) + { + SetAllSuggestions (_textViewTopRight); + } + } + + private void Win_LayoutStarted (object? sender, LayoutEventArgs obj) + { + if (_textViewTopLeft is null || _miMultilineCheckBox is null || _miWrapCheckBox is null || _textViewBottomLeft is null || _textViewBottomRight is null) + { + return; + } + + _miMultilineCheckBox.CheckedState = _textViewTopLeft.Multiline ? CheckState.Checked : CheckState.UnChecked; + _miWrapCheckBox.CheckedState = _textViewTopLeft.WordWrap ? CheckState.Checked : CheckState.UnChecked; SetMultilineStatusText (); SetWrapStatusText (); - if (_miMultiline.Checked == true) + if (_miMultilineCheckBox.CheckedState == CheckState.Checked) { _height = 10; } @@ -182,13 +266,22 @@ public class TextViewAutocompletePopup : Scenario private void WordWrap () { - _miWrap.Checked = !_miWrap.Checked; - _textViewTopLeft.WordWrap = (bool)_miWrap.Checked; - _textViewTopRight.WordWrap = (bool)_miWrap.Checked; - _textViewBottomLeft.WordWrap = (bool)_miWrap.Checked; - _textViewBottomRight.WordWrap = (bool)_miWrap.Checked; - _textViewCentered.WordWrap = (bool)_miWrap.Checked; - _miWrap.Checked = _textViewTopLeft.WordWrap; + if (_miWrapCheckBox is null + || _textViewTopLeft is null + || _textViewTopRight is null + || _textViewBottomLeft is null + || _textViewBottomRight is null + || _textViewCentered is null) + { + return; + } + + _textViewTopLeft.WordWrap = _miWrapCheckBox.CheckedState == CheckState.Checked; + _textViewTopRight.WordWrap = _miWrapCheckBox.CheckedState == CheckState.Checked; + _textViewBottomLeft.WordWrap = _miWrapCheckBox.CheckedState == CheckState.Checked; + _textViewBottomRight.WordWrap = _miWrapCheckBox.CheckedState == CheckState.Checked; + _textViewCentered.WordWrap = _miWrapCheckBox.CheckedState == CheckState.Checked; + _miWrapCheckBox.CheckedState = _textViewTopLeft.WordWrap ? CheckState.Checked : CheckState.UnChecked; SetWrapStatusText (); } } diff --git a/Examples/UICatalog/Scenarios/TreeUseCases.cs b/Examples/UICatalog/Scenarios/TreeUseCases.cs index 021b5b65b..a7c8772e5 100644 --- a/Examples/UICatalog/Scenarios/TreeUseCases.cs +++ b/Examples/UICatalog/Scenarios/TreeUseCases.cs @@ -1,3 +1,5 @@ +#nullable enable + using System.Collections.Generic; using System.Linq; @@ -8,76 +10,93 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("TreeView")] public class TreeUseCases : Scenario { - private View _currentTree; + private View? _currentTree; public override void Main () { - // Init Application.Init (); - // Setup - Create a top-level application window and configure it. - Toplevel appWindow = new (); + Window appWindow = new (); - var menu = new MenuBar - { - Menus = + // MenuBar + MenuBar menu = new (); + + menu.Add ( + new MenuBarItem ( + "_File", + [ + new MenuItem + { + Title = "_Quit", + Action = Quit + } + ] + ) + ); + + menu.Add ( + new MenuBarItem ( + "_Scenarios", + [ + new MenuItem + { + Title = "_Simple Nodes", + Action = LoadSimpleNodes + }, + new MenuItem + { + Title = "_Rooms", + Action = LoadRooms + }, + new MenuItem + { + Title = "_Armies With Builder", + Action = () => LoadArmies (false) + }, + new MenuItem + { + Title = "_Armies With Delegate", + Action = () => LoadArmies (true) + } + ] + ) + ); + + // StatusBar + StatusBar statusBar = new ( [ - new MenuBarItem ("_File", new MenuItem [] { new ("_Quit", "", () => Quit ()) }), - new MenuBarItem ( - "_Scenarios", - new MenuItem [] - { - new ( - "_Simple Nodes", - "", - () => LoadSimpleNodes () - ), - new ("_Rooms", "", () => LoadRooms ()), - new ( - "_Armies With Builder", - "", - () => LoadArmies (false) - ), - new ( - "_Armies With Delegate", - "", - () => LoadArmies (true) - ) - } - ) + new (Application.QuitKey, "Quit", Quit) ] - }; + ); - appWindow.Add (menu); - - var statusBar = new StatusBar ([new (Application.QuitKey, "Quit", Quit)]); - - appWindow.Add (statusBar); + appWindow.Add (menu, statusBar); appWindow.Ready += (sender, args) => - // Start with the most basic use case - LoadSimpleNodes (); + { + // Start with the most basic use case + LoadSimpleNodes (); + }; - // Run - Start the application. Application.Run (appWindow); appWindow.Dispose (); - - // Shutdown - Calling Application.Shutdown is required. Application.Shutdown (); - } private void LoadArmies (bool useDelegate) { - var army1 = new Army + Army army1 = new () { Designation = "3rd Infantry", - Units = new List { new () { Name = "Orc" }, new () { Name = "Troll" }, new () { Name = "Goblin" } } + Units = [new () { Name = "Orc" }, new () { Name = "Troll" }, new () { Name = "Goblin" }] }; - if (_currentTree != null) + if (_currentTree is { }) { - Application.TopRunnable.Remove (_currentTree); + if (Application.TopRunnable is { }) + { + Application.TopRunnable.Remove (_currentTree); + } + _currentTree.Dispose (); } @@ -86,18 +105,21 @@ public class TreeUseCases : Scenario if (useDelegate) { tree.TreeBuilder = new DelegateTreeBuilder ( - o => - o is Army a - ? a.Units - : Enumerable.Empty () - ); + o => + o is Army a && a.Units is { } + ? a.Units + : Enumerable.Empty () + ); } else { tree.TreeBuilder = new GameObjectTreeBuilder (); } - Application.TopRunnable.Add (tree); + if (Application.TopRunnable is { }) + { + Application.TopRunnable.Add (tree); + } tree.AddObject (army1); @@ -106,24 +128,33 @@ public class TreeUseCases : Scenario private void LoadRooms () { - var myHouse = new House + House myHouse = new () { Address = "23 Nowhere Street", - Rooms = new List - { - new () { Name = "Ballroom" }, new () { Name = "Bedroom 1" }, new () { Name = "Bedroom 2" } - } + Rooms = + [ + new () { Name = "Ballroom" }, + new () { Name = "Bedroom 1" }, + new () { Name = "Bedroom 2" } + ] }; - if (_currentTree != null) + if (_currentTree is { }) { - Application.TopRunnable.Remove (_currentTree); + if (Application.TopRunnable is { }) + { + Application.TopRunnable.Remove (_currentTree); + } + _currentTree.Dispose (); } - var tree = new TreeView { X = 0, Y = 1, Width = Dim.Fill(), Height = Dim.Fill (1) }; + TreeView tree = new () { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1) }; - Application.TopRunnable.Add (tree); + if (Application.TopRunnable is { }) + { + Application.TopRunnable.Add (tree); + } tree.AddObject (myHouse); @@ -132,21 +163,28 @@ public class TreeUseCases : Scenario private void LoadSimpleNodes () { - if (_currentTree != null) + if (_currentTree is { }) { - Application.TopRunnable.Remove (_currentTree); + if (Application.TopRunnable is { }) + { + Application.TopRunnable.Remove (_currentTree); + } + _currentTree.Dispose (); } - var tree = new TreeView { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1) }; + TreeView tree = new () { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1) }; - Application.TopRunnable.Add (tree); + if (Application.TopRunnable is { }) + { + Application.TopRunnable.Add (tree); + } - var root1 = new TreeNode ("Root1"); + TreeNode root1 = new ("Root1"); root1.Children.Add (new TreeNode ("Child1.1")); root1.Children.Add (new TreeNode ("Child1.2")); - var root2 = new TreeNode ("Root2"); + TreeNode root2 = new ("Root2"); root2.Children.Add (new TreeNode ("Child2.1")); root2.Children.Add (new TreeNode ("Child2.2")); @@ -156,17 +194,21 @@ public class TreeUseCases : Scenario _currentTree = tree; } - private void Quit () { Application.RequestStop (); } + private void Quit () + { + Application.RequestStop (); + } private class Army : GameObject { - public string Designation { get; set; } - public List Units { get; set; } + public string Designation { get; set; } = string.Empty; + public List Units { get; set; } = []; public override string ToString () { return Designation; } } private abstract class GameObject - { } + { + } private class GameObjectTreeBuilder : ITreeBuilder { @@ -175,7 +217,7 @@ public class TreeUseCases : Scenario public IEnumerable GetChildren (GameObject model) { - if (model is Army a) + if (model is Army a && a.Units is { }) { return a.Units; } @@ -184,15 +226,12 @@ public class TreeUseCases : Scenario } } - // Your data class private class House : TreeNode { - // Your properties - public string Address { get; set; } + public string Address { get; set; } = string.Empty; - // ITreeNode member: public override IList Children => Rooms.Cast ().ToList (); - public List Rooms { get; set; } + public List Rooms { get; set; } = []; public override string Text { @@ -203,7 +242,7 @@ public class TreeUseCases : Scenario private class Room : TreeNode { - public string Name { get; set; } + public string Name { get; set; } = string.Empty; public override string Text { @@ -214,7 +253,7 @@ public class TreeUseCases : Scenario private class Unit : GameObject { - public string Name { get; set; } + public string Name { get; set; } = string.Empty; public override string ToString () { return Name; } } } diff --git a/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs b/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs index eeb4f8b78..64102a935 100644 --- a/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs +++ b/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs @@ -1,3 +1,5 @@ +#nullable enable + using System.IO.Abstractions; using System.Text; @@ -10,180 +12,51 @@ namespace UICatalog.Scenarios; public class TreeViewFileSystem : Scenario { private readonly FileSystemIconProvider _iconProvider = new (); - private DetailsFrame _detailsFrame; - private MenuItem _miArrowSymbols; - private MenuItem _miBasicIcons; - private MenuItem _miColoredSymbols; - private MenuItem _miCursor; - private MenuItem _miCustomColors; - private MenuItem _miFullPaths; - private MenuItem _miHighlightModelTextOnly; - private MenuItem _miInvertSymbols; - private MenuItem _miLeaveLastRow; - private MenuItem _miMultiSelect; - private MenuItem _miNerdIcons; - private MenuItem _miNoSymbols; - private MenuItem _miPlusMinus; - private MenuItem _miShowLines; - private MenuItem _miUnicodeIcons; + private DetailsFrame? _detailsFrame; + private CheckBox? _miArrowSymbolsCheckBox; + private CheckBox? _miBasicIconsCheckBox; + private CheckBox? _miColoredSymbolsCheckBox; + private CheckBox? _miCursorCheckBox; + private CheckBox? _miCustomColorsCheckBox; + private CheckBox? _miFullPathsCheckBox; + private CheckBox? _miHighlightModelTextOnlyCheckBox; + private CheckBox? _miInvertSymbolsCheckBox; + private CheckBox? _miLeaveLastRowCheckBox; + private CheckBox? _miMultiSelectCheckBox; + private CheckBox? _miNerdIconsCheckBox; + private CheckBox? _miNoSymbolsCheckBox; + private CheckBox? _miPlusMinusCheckBox; + private CheckBox? _miShowLinesCheckBox; + private CheckBox? _miUnicodeIconsCheckBox; /// A tree view where nodes are files and folders - private TreeView _treeViewFiles; + private TreeView? _treeViewFiles; public override void Main () { Application.Init (); - var win = new Window + Window win = new () { Title = GetName (), Y = 1, // menu Height = Dim.Fill () }; - var top = new Toplevel (); - var menu = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ( - "_Quit", - $"{Application.QuitKey}", - () => Quit () - ) - } - ), - new ( - "_View", - new [] - { - _miFullPaths = - new ("_Full Paths", "", () => SetFullName ()) - { - Checked = false, CheckType = MenuItemCheckStyle.Checked - }, - _miMultiSelect = new ( - "_Multi Select", - "", - () => SetMultiSelect () - ) - { - Checked = true, - CheckType = MenuItemCheckStyle - .Checked - } - } - ), - new ( - "_Style", - new [] - { - _miShowLines = - new ("_Show Lines", "", () => ShowLines ()) - { - Checked = true, CheckType = MenuItemCheckStyle.Checked - }, - null /*separator*/, - _miPlusMinus = - new ( - "_Plus Minus Symbols", - "+ -", - () => SetExpandableSymbols ( - (Rune)'+', - (Rune)'-' - ) - ) { Checked = true, CheckType = MenuItemCheckStyle.Radio }, - _miArrowSymbols = - new ( - "_Arrow Symbols", - "> v", - () => SetExpandableSymbols ( - (Rune)'>', - (Rune)'v' - ) - ) { Checked = false, CheckType = MenuItemCheckStyle.Radio }, - _miNoSymbols = - new ( - "_No Symbols", - "", - () => SetExpandableSymbols ( - default (Rune), - null - ) - ) { Checked = false, CheckType = MenuItemCheckStyle.Radio }, - null /*separator*/, - _miColoredSymbols = - new ( - "_Colored Symbols", - "", - () => ShowColoredExpandableSymbols () - ) { Checked = false, CheckType = MenuItemCheckStyle.Checked }, - _miInvertSymbols = - new ( - "_Invert Symbols", - "", - () => InvertExpandableSymbols () - ) { Checked = false, CheckType = MenuItemCheckStyle.Checked }, - null /*separator*/, - _miBasicIcons = - new ("_Basic Icons", null, SetNoIcons) - { - Checked = false, CheckType = MenuItemCheckStyle.Radio - }, - _miUnicodeIcons = - new ("_Unicode Icons", null, SetUnicodeIcons) - { - Checked = false, CheckType = MenuItemCheckStyle.Radio - }, - _miNerdIcons = - new ("_Nerd Icons", null, SetNerdIcons) - { - Checked = false, CheckType = MenuItemCheckStyle.Radio - }, - null /*separator*/, - _miLeaveLastRow = - new ( - "_Leave Last Row", - "", - () => SetLeaveLastRow () - ) { Checked = true, CheckType = MenuItemCheckStyle.Checked }, - _miHighlightModelTextOnly = - new ( - "_Highlight Model Text Only", - "", - () => SetCheckHighlightModelTextOnly () - ) { Checked = false, CheckType = MenuItemCheckStyle.Checked }, - null /*separator*/, - _miCustomColors = - new ( - "C_ustom Colors Hidden Files", - "Yellow/Red", - () => SetCustomColors () - ) { Checked = false, CheckType = MenuItemCheckStyle.Checked }, - null /*separator*/, - _miCursor = new ( - "Curs_or (MultiSelect only)", - "", - () => SetCursor () - ) { Checked = false, CheckType = MenuItemCheckStyle.Checked } - } - ) - ] - }; - top.Add (menu); + // MenuBar + MenuBar menu = new (); - _treeViewFiles = new () { X = 0, Y = 0, Width = Dim.Percent (50), Height = Dim.Fill () }; + _treeViewFiles = new () { X = 0, Y = Pos.Bottom (menu), Width = Dim.Percent (50), Height = Dim.Fill () }; _treeViewFiles.DrawLine += TreeViewFiles_DrawLine; _treeViewFiles.VerticalScrollBar.AutoShow = false; _detailsFrame = new (_iconProvider) { - X = Pos.Right (_treeViewFiles), Y = 0, Width = Dim.Fill (), Height = Dim.Fill () + X = Pos.Right (_treeViewFiles), + Y = Pos.Top (_treeViewFiles), + Width = Dim.Fill (), + Height = Dim.Fill () }; win.Add (_detailsFrame); @@ -193,29 +66,214 @@ public class TreeViewFileSystem : Scenario SetupFileTree (); - win.Add (_treeViewFiles); - top.Add (win); + // Setup menu checkboxes + _miFullPathsCheckBox = new () + { + Title = "_Full Paths" + }; + _miFullPathsCheckBox.CheckedStateChanged += (s, e) => SetFullName (); + + _miMultiSelectCheckBox = new () + { + Title = "_Multi Select", + CheckedState = CheckState.Checked + }; + _miMultiSelectCheckBox.CheckedStateChanged += (s, e) => SetMultiSelect (); + + _miShowLinesCheckBox = new () + { + Title = "_Show Lines", + CheckedState = CheckState.Checked + }; + _miShowLinesCheckBox.CheckedStateChanged += (s, e) => ShowLines (); + + _miPlusMinusCheckBox = new () + { + Title = "_Plus Minus Symbols", + CheckedState = CheckState.Checked + }; + _miPlusMinusCheckBox.CheckedStateChanged += (s, e) => SetExpandableSymbols ((Rune)'+', (Rune)'-'); + + _miArrowSymbolsCheckBox = new () + { + Title = "_Arrow Symbols" + }; + _miArrowSymbolsCheckBox.CheckedStateChanged += (s, e) => SetExpandableSymbols ((Rune)'>', (Rune)'v'); + + _miNoSymbolsCheckBox = new () + { + Title = "_No Symbols" + }; + _miNoSymbolsCheckBox.CheckedStateChanged += (s, e) => SetExpandableSymbols (default (Rune), null); + + _miColoredSymbolsCheckBox = new () + { + Title = "_Colored Symbols" + }; + _miColoredSymbolsCheckBox.CheckedStateChanged += (s, e) => ShowColoredExpandableSymbols (); + + _miInvertSymbolsCheckBox = new () + { + Title = "_Invert Symbols" + }; + _miInvertSymbolsCheckBox.CheckedStateChanged += (s, e) => InvertExpandableSymbols (); + + _miBasicIconsCheckBox = new () + { + Title = "_Basic Icons" + }; + _miBasicIconsCheckBox.CheckedStateChanged += (s, e) => SetNoIcons (); + + _miUnicodeIconsCheckBox = new () + { + Title = "_Unicode Icons" + }; + _miUnicodeIconsCheckBox.CheckedStateChanged += (s, e) => SetUnicodeIcons (); + + _miNerdIconsCheckBox = new () + { + Title = "_Nerd Icons" + }; + _miNerdIconsCheckBox.CheckedStateChanged += (s, e) => SetNerdIcons (); + + _miLeaveLastRowCheckBox = new () + { + Title = "_Leave Last Row", + CheckedState = CheckState.Checked + }; + _miLeaveLastRowCheckBox.CheckedStateChanged += (s, e) => SetLeaveLastRow (); + + _miHighlightModelTextOnlyCheckBox = new () + { + Title = "_Highlight Model Text Only" + }; + _miHighlightModelTextOnlyCheckBox.CheckedStateChanged += (s, e) => SetCheckHighlightModelTextOnly (); + + _miCustomColorsCheckBox = new () + { + Title = "C_ustom Colors Hidden Files" + }; + _miCustomColorsCheckBox.CheckedStateChanged += (s, e) => SetCustomColors (); + + _miCursorCheckBox = new () + { + Title = "Curs_or (MultiSelect only)" + }; + _miCursorCheckBox.CheckedStateChanged += (s, e) => SetCursor (); + + menu.Add ( + new MenuBarItem ( + "_File", + [ + new MenuItem + { + Title = "_Quit", + Key = Application.QuitKey, + Action = Quit + } + ] + ) + ); + + menu.Add ( + new MenuBarItem ( + "_View", + [ + new MenuItem + { + CommandView = _miFullPathsCheckBox + }, + new MenuItem + { + CommandView = _miMultiSelectCheckBox + } + ] + ) + ); + + menu.Add ( + new MenuBarItem ( + "_Style", + [ + new MenuItem + { + CommandView = _miShowLinesCheckBox + }, + new MenuItem + { + CommandView = _miPlusMinusCheckBox + }, + new MenuItem + { + CommandView = _miArrowSymbolsCheckBox + }, + new MenuItem + { + CommandView = _miNoSymbolsCheckBox + }, + new MenuItem + { + CommandView = _miColoredSymbolsCheckBox + }, + new MenuItem + { + CommandView = _miInvertSymbolsCheckBox + }, + new MenuItem + { + CommandView = _miBasicIconsCheckBox + }, + new MenuItem + { + CommandView = _miUnicodeIconsCheckBox + }, + new MenuItem + { + CommandView = _miNerdIconsCheckBox + }, + new MenuItem + { + CommandView = _miLeaveLastRowCheckBox + }, + new MenuItem + { + CommandView = _miHighlightModelTextOnlyCheckBox + }, + new MenuItem + { + CommandView = _miCustomColorsCheckBox + }, + new MenuItem + { + CommandView = _miCursorCheckBox + } + ] + ) + ); + + win.Add (menu, _treeViewFiles); _treeViewFiles.GoToFirst (); _treeViewFiles.Expand (); - //SetupScrollBar (); - _treeViewFiles.SetFocus (); UpdateIconCheckedness (); - Application.Run (top); - top.Dispose (); + Application.Run (win); + win.Dispose (); Application.Shutdown (); } - private string AspectGetter (IFileSystemInfo f) { return (_iconProvider.GetIconWithOptionalSpace (f) + f.Name).Trim (); } + private string AspectGetter (IFileSystemInfo f) => (_iconProvider.GetIconWithOptionalSpace (f) + f.Name).Trim (); private void InvertExpandableSymbols () { - _miInvertSymbols.Checked = !_miInvertSymbols.Checked; + if (_treeViewFiles is null || _miInvertSymbolsCheckBox is null) + { + return; + } - _treeViewFiles.Style.InvertExpandSymbolColors = (bool)_miInvertSymbols.Checked; + _treeViewFiles.Style.InvertExpandSymbolColors = _miInvertSymbolsCheckBox.CheckedState == CheckState.Checked; _treeViewFiles.SetNeedsDraw (); } @@ -223,24 +281,34 @@ public class TreeViewFileSystem : Scenario private void SetCheckHighlightModelTextOnly () { - _treeViewFiles.Style.HighlightModelTextOnly = !_treeViewFiles.Style.HighlightModelTextOnly; - _miHighlightModelTextOnly.Checked = _treeViewFiles.Style.HighlightModelTextOnly; + if (_treeViewFiles is null || _miHighlightModelTextOnlyCheckBox is null) + { + return; + } + + _treeViewFiles.Style.HighlightModelTextOnly = _miHighlightModelTextOnlyCheckBox.CheckedState == CheckState.Checked; _treeViewFiles.SetNeedsDraw (); } private void SetCursor () { - _miCursor.Checked = !_miCursor.Checked; + if (_treeViewFiles is null || _miCursorCheckBox is null) + { + return; + } _treeViewFiles.CursorVisibility = - _miCursor.Checked == true ? CursorVisibility.Default : CursorVisibility.Invisible; + _miCursorCheckBox.CheckedState == CheckState.Checked ? CursorVisibility.Default : CursorVisibility.Invisible; } private void SetCustomColors () { - _miCustomColors.Checked = !_miCustomColors.Checked; + if (_treeViewFiles is null || _miCustomColorsCheckBox is null) + { + return; + } - if (_miCustomColors.Checked == true) + if (_miCustomColorsCheckBox.CheckedState == CheckState.Checked) { _treeViewFiles.ColorGetter = m => { @@ -257,8 +325,6 @@ public class TreeViewFileSystem : Scenario _treeViewFiles.GetAttributeForRole (VisualRole.Normal).Background ) }; - - ; } if (m is IFileInfo && m.Attributes.HasFlag (FileAttributes.Hidden)) @@ -274,8 +340,6 @@ public class TreeViewFileSystem : Scenario _treeViewFiles.GetAttributeForRole (VisualRole.Normal).Background ) }; - - ; } return null; @@ -291,9 +355,25 @@ public class TreeViewFileSystem : Scenario private void SetExpandableSymbols (Rune expand, Rune? collapse) { - _miPlusMinus.Checked = expand.Value == '+'; - _miArrowSymbols.Checked = expand.Value == '>'; - _miNoSymbols.Checked = expand.Value == default (int); + if (_treeViewFiles is null) + { + return; + } + + if (_miPlusMinusCheckBox is { }) + { + _miPlusMinusCheckBox.CheckedState = expand.Value == '+' ? CheckState.Checked : CheckState.UnChecked; + } + + if (_miArrowSymbolsCheckBox is { }) + { + _miArrowSymbolsCheckBox.CheckedState = expand.Value == '>' ? CheckState.Checked : CheckState.UnChecked; + } + + if (_miNoSymbolsCheckBox is { }) + { + _miNoSymbolsCheckBox.CheckedState = expand.Value == default (int) ? CheckState.Checked : CheckState.UnChecked; + } _treeViewFiles.Style.ExpandableSymbol = expand; _treeViewFiles.Style.CollapseableSymbol = collapse; @@ -302,9 +382,12 @@ public class TreeViewFileSystem : Scenario private void SetFullName () { - _miFullPaths.Checked = !_miFullPaths.Checked; + if (_treeViewFiles is null || _miFullPathsCheckBox is null) + { + return; + } - if (_miFullPaths.Checked == true) + if (_miFullPathsCheckBox.CheckedState == CheckState.Checked) { _treeViewFiles.AspectGetter = f => f.FullName; } @@ -318,14 +401,22 @@ public class TreeViewFileSystem : Scenario private void SetLeaveLastRow () { - _miLeaveLastRow.Checked = !_miLeaveLastRow.Checked; - _treeViewFiles.Style.LeaveLastRow = (bool)_miLeaveLastRow.Checked; + if (_treeViewFiles is null || _miLeaveLastRowCheckBox is null) + { + return; + } + + _treeViewFiles.Style.LeaveLastRow = _miLeaveLastRowCheckBox.CheckedState == CheckState.Checked; } private void SetMultiSelect () { - _miMultiSelect.Checked = !_miMultiSelect.Checked; - _treeViewFiles.MultiSelect = (bool)_miMultiSelect.Checked; + if (_treeViewFiles is null || _miMultiSelectCheckBox is null) + { + return; + } + + _treeViewFiles.MultiSelect = _miMultiSelectCheckBox.CheckedState == CheckState.Checked; } private void SetNerdIcons () @@ -349,8 +440,13 @@ public class TreeViewFileSystem : Scenario private void SetupFileTree () { + if (_treeViewFiles is null) + { + return; + } + // setup how to build tree - var fs = new FileSystem (); + FileSystem fs = new (); IEnumerable rootDirs = DriveInfo.GetDrives ().Select (d => fs.DirectoryInfo.New (d.RootDirectory.FullName)); @@ -363,52 +459,14 @@ public class TreeViewFileSystem : Scenario _iconProvider.IsOpenGetter = _treeViewFiles.IsExpanded; } - //private void SetupScrollBar () - //{ - // // When using scroll bar leave the last row of the control free (for over-rendering with scroll bar) - // _treeViewFiles.Style.LeaveLastRow = true; - - // var scrollBar = new ScrollBarView (_treeViewFiles, true); - - // scrollBar.ChangedPosition += (s, e) => - // { - // _treeViewFiles.ScrollOffsetVertical = scrollBar.Position; - - // if (_treeViewFiles.ScrollOffsetVertical != scrollBar.Position) - // { - // scrollBar.Position = _treeViewFiles.ScrollOffsetVertical; - // } - - // _treeViewFiles.SetNeedsDraw (); - // }; - - // scrollBar.OtherScrollBarView.ChangedPosition += (s, e) => - // { - // _treeViewFiles.ScrollOffsetHorizontal = scrollBar.OtherScrollBarView.Position; - - // if (_treeViewFiles.ScrollOffsetHorizontal != scrollBar.OtherScrollBarView.Position) - // { - // scrollBar.OtherScrollBarView.Position = _treeViewFiles.ScrollOffsetHorizontal; - // } - - // _treeViewFiles.SetNeedsDraw (); - // }; - - // _treeViewFiles.DrawingContent += (s, e) => - // { - // scrollBar.Size = _treeViewFiles.ContentHeight; - // scrollBar.Position = _treeViewFiles.ScrollOffsetVertical; - // scrollBar.OtherScrollBarView.Size = _treeViewFiles.GetContentWidth (true); - // scrollBar.OtherScrollBarView.Position = _treeViewFiles.ScrollOffsetHorizontal; - // scrollBar.Refresh (); - // }; - //} - private void ShowColoredExpandableSymbols () { - _miColoredSymbols.Checked = !_miColoredSymbols.Checked; + if (_treeViewFiles is null || _miColoredSymbolsCheckBox is null) + { + return; + } - _treeViewFiles.Style.ColorExpandSymbol = (bool)_miColoredSymbols.Checked; + _treeViewFiles.Style.ColorExpandSymbol = _miColoredSymbolsCheckBox.CheckedState == CheckState.Checked; _treeViewFiles.SetNeedsDraw (); } @@ -418,22 +476,31 @@ public class TreeViewFileSystem : Scenario // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused // and the context menu is disposed when it is closed. - _detailsFrame.App?.Popover?.Register (contextMenu); + _detailsFrame?.App?.Popover?.Register (contextMenu); Application.Invoke (() => contextMenu?.MakeVisible (screenPoint)); } private void ShowLines () { - _miShowLines.Checked = !_miShowLines.Checked; + if (_treeViewFiles is null || _miShowLinesCheckBox is null) + { + return; + } - _treeViewFiles.Style.ShowBranchLines = (bool)_miShowLines.Checked!; + _treeViewFiles.Style.ShowBranchLines = _miShowLinesCheckBox.CheckedState == CheckState.Checked; _treeViewFiles.SetNeedsDraw (); } - private void ShowPropertiesOf (IFileSystemInfo fileSystemInfo) { _detailsFrame.FileInfo = fileSystemInfo; } + private void ShowPropertiesOf (IFileSystemInfo fileSystemInfo) + { + if (_detailsFrame is { }) + { + _detailsFrame.FileInfo = fileSystemInfo; + } + } - private void TreeViewFiles_DrawLine (object sender, DrawTreeViewLineEventArgs e) + private void TreeViewFiles_DrawLine (object? sender, DrawTreeViewLineEventArgs e) { // Render directory icons in yellow if (e.Model is IDirectoryInfo d) @@ -454,14 +521,19 @@ public class TreeViewFileSystem : Scenario } } - private void TreeViewFiles_KeyPress (object sender, Key obj) + private void TreeViewFiles_KeyPress (object? sender, Key obj) { + if (_treeViewFiles is null) + { + return; + } + if (obj.KeyCode == (KeyCode.R | KeyCode.CtrlMask)) { - IFileSystemInfo selected = _treeViewFiles.SelectedObject; + IFileSystemInfo? selected = _treeViewFiles.SelectedObject; // nothing is selected - if (selected == null) + if (selected is null) { return; } @@ -469,7 +541,7 @@ public class TreeViewFileSystem : Scenario int? location = _treeViewFiles.GetObjectRow (selected); //selected object is offscreen or somehow not found - if (location == null || location < 0 || location > _treeViewFiles.Frame.Height) + if (location is null || location < 0 || location > _treeViewFiles.Frame.Height) { return; } @@ -484,15 +556,20 @@ public class TreeViewFileSystem : Scenario } } - private void TreeViewFiles_MouseClick (object sender, MouseEventArgs obj) + private void TreeViewFiles_MouseClick (object? sender, MouseEventArgs obj) { + if (_treeViewFiles is null) + { + return; + } + // if user right clicks if (obj.Flags.HasFlag (MouseFlags.Button3Clicked)) { - IFileSystemInfo rightClicked = _treeViewFiles.GetObjectOnRow (obj.Position.Y); + IFileSystemInfo? rightClicked = _treeViewFiles.GetObjectOnRow (obj.Position.Y); // nothing was clicked - if (rightClicked == null) + if (rightClicked is null) { return; } @@ -507,20 +584,34 @@ public class TreeViewFileSystem : Scenario } } - private void TreeViewFiles_SelectionChanged (object sender, SelectionChangedEventArgs e) { ShowPropertiesOf (e.NewValue); } + private void TreeViewFiles_SelectionChanged (object? sender, SelectionChangedEventArgs e) { ShowPropertiesOf (e.NewValue); } private void UpdateIconCheckedness () { - _miBasicIcons.Checked = !_iconProvider.UseNerdIcons && !_iconProvider.UseUnicodeCharacters; - _miUnicodeIcons.Checked = _iconProvider.UseUnicodeCharacters; - _miNerdIcons.Checked = _iconProvider.UseNerdIcons; - _treeViewFiles.SetNeedsDraw (); + if (_miBasicIconsCheckBox is { }) + { + _miBasicIconsCheckBox.CheckedState = !_iconProvider.UseNerdIcons && !_iconProvider.UseUnicodeCharacters + ? CheckState.Checked + : CheckState.UnChecked; + } + + if (_miUnicodeIconsCheckBox is { }) + { + _miUnicodeIconsCheckBox.CheckedState = _iconProvider.UseUnicodeCharacters ? CheckState.Checked : CheckState.UnChecked; + } + + if (_miNerdIconsCheckBox is { }) + { + _miNerdIconsCheckBox.CheckedState = _iconProvider.UseNerdIcons ? CheckState.Checked : CheckState.UnChecked; + } + + _treeViewFiles?.SetNeedsDraw (); } private class DetailsFrame : FrameView { private readonly FileSystemIconProvider _iconProvider; - private IFileSystemInfo _fileInfo; + private IFileSystemInfo? _fileInfo; public DetailsFrame (FileSystemIconProvider iconProvider) { @@ -530,13 +621,13 @@ public class TreeViewFileSystem : Scenario _iconProvider = iconProvider; } - public IFileSystemInfo FileInfo + public IFileSystemInfo? FileInfo { get => _fileInfo; set { _fileInfo = value; - StringBuilder sb = null; + StringBuilder? sb = null; if (_fileInfo is IFileInfo f) { @@ -552,12 +643,12 @@ public class TreeViewFileSystem : Scenario { Title = $"{_iconProvider.GetIconWithOptionalSpace (dir)}{dir.Name}".Trim (); sb = new (); - sb.AppendLine ($"Path:\n {dir?.FullName}\n"); + sb.AppendLine ($"Path:\n {dir.FullName}\n"); sb.AppendLine ($"Modified:\n {dir.LastWriteTime}\n"); sb.AppendLine ($"Created:\n {dir.CreationTime}\n"); } - Text = sb.ToString (); + Text = sb?.ToString () ?? string.Empty; } } } diff --git a/Examples/UICatalog/Scenarios/Unicode.cs b/Examples/UICatalog/Scenarios/Unicode.cs index 412f982da..3fe347aec 100644 --- a/Examples/UICatalog/Scenarios/Unicode.cs +++ b/Examples/UICatalog/Scenarios/Unicode.cs @@ -1,5 +1,5 @@ -using System.Collections.ObjectModel; -using System.IO; +#nullable enable + using System.Text; namespace UICatalog.Scenarios; @@ -15,82 +15,75 @@ public class UnicodeInMenu : Scenario "Τὴ γλῶσσα μοῦ ἔδωσαν ἑλληνικὴ\nτὸ σπίτι φτωχικὸ στὶς ἀμμουδιὲς τοῦ Ὁμήρου.\nΜονάχη ἔγνοια ἡ γλῶσσα μου στὶς ἀμμουδιὲς τοῦ Ὁμήρου."; var gitString = - $"gui.cs 糊 (hú) { - Glyphs.IdenticalTo - } { - Glyphs.DownArrow - }18 { - Glyphs.UpArrow - }10 { - Glyphs.VerticalFourDots - }1 { - Glyphs.HorizontalEllipsis - }"; + $"gui.cs 糊 (hú) {Glyphs.IdenticalTo} {Glyphs.DownArrow}18 {Glyphs.UpArrow}10 {Glyphs.VerticalFourDots}1 {Glyphs.HorizontalEllipsis}"; - // Init Application.Init (); - // Setup - Create a top-level application window and configure it. Window appWindow = new () { - Title = GetQuitKeyAndName () + Title = GetQuitKeyAndName (), + BorderStyle = LineStyle.None }; - var menu = new MenuBar - { - Menus = - [ - new ( - "_Файл", - new MenuItem [] - { - new ( - "_Создать", - "Creates new file", - null - ), - new ("_Открыть", "", null), - new ("Со_хранить", "", null), - new ( - "_Выход", - "", - () => Application.RequestStop () - ) - } - ), - new ( - "_Edit", - new MenuItem [] - { - new ("_Copy", "", null), new ("C_ut", "", null), - new ("_糊", "hú (Paste)", null) - } - ) - ] - }; + // MenuBar + MenuBar menu = new (); + + menu.Add ( + new MenuBarItem ( + "_Файл", + [ + new MenuItem + { + Title = "_Создать", + HelpText = "Creates new file" + }, + new MenuItem + { + Title = "_Открыть" + }, + new MenuItem + { + Title = "Со_хранить" + }, + new MenuItem + { + Title = "_Выход", + Action = () => Application.RequestStop () + } + ] + ) + ); + + menu.Add ( + new MenuBarItem ( + "_Edit", + [ + new MenuItem + { + Title = "_Copy" + }, + new MenuItem + { + Title = "C_ut" + }, + new MenuItem + { + Title = "_糊", + HelpText = "hú (Paste)" + } + ] + ) + ); + appWindow.Add (menu); - var statusBar = new StatusBar ( - [ - new ( - Application.QuitKey, - "Выход", - () => Application.RequestStop () - ), - new (Key.F2, "Создать", null), - new (Key.F3, "Со_хранить", null) - ] - ); - appWindow.Add (statusBar); - - var label = new Label { X = 0, Y = 1, Text = "Label:" }; + Label label = new () { X = 0, Y = Pos.Bottom (menu), Text = "Label:" }; appWindow.Add (label); - var testlabel = new Label + Label testlabel = new () { X = 20, Y = Pos.Y (label), - Width = Dim.Percent (50), Text = gitString }; @@ -98,7 +91,8 @@ public class UnicodeInMenu : Scenario label = new () { X = Pos.X (label), Y = Pos.Bottom (label) + 1, Text = "Label (CanFocus):" }; appWindow.Add (label); - var sb = new StringBuilder (); + + StringBuilder sb = new (); sb.Append ('e'); sb.Append ('\u0301'); sb.Append ('\u0301'); @@ -107,64 +101,59 @@ public class UnicodeInMenu : Scenario { X = 20, Y = Pos.Y (label), - Width = Dim.Percent (50), CanFocus = true, HotKeySpecifier = new ('&'), Text = $"Should be [e with two accents, but isn't due to #2616]: [{sb}]" }; appWindow.Add (testlabel); + label = new () { X = Pos.X (label), Y = Pos.Bottom (label) + 1, Text = "Button:" }; appWindow.Add (label); - var button = new Button { X = 20, Y = Pos.Y (label), Text = "A123456789♥♦♣♠JQK" }; + + Button button = new () { X = 20, Y = Pos.Y (label), Text = "A123456789♥♦♣♠JQK" }; appWindow.Add (button); label = new () { X = Pos.X (label), Y = Pos.Bottom (label) + 1, Text = "CheckBox:" }; appWindow.Add (label); - var checkBox = new CheckBox + CheckBox checkBox = new () { X = 20, Y = Pos.Y (label), - Width = Dim.Percent (50), Height = 1, Text = gitString }; + appWindow.Add (checkBox); - var checkBoxRight = new CheckBox + CheckBox checkBoxRight = new () { X = 20, Y = Pos.Bottom (checkBox), - Width = Dim.Percent (50), Height = 1, TextAlignment = Alignment.End, Text = $"End - {gitString}" }; - appWindow.Add (checkBox, checkBoxRight); + appWindow.Add (checkBoxRight); - //label = new () { X = Pos.X (label), Y = Pos.Bottom (checkBoxRight) + 1, Text = "ComboBox:" }; - //appWindow.Add (label); - //var comboBox = new ComboBox { X = 20, Y = Pos.Y (label), Width = Dim.Percent (50) }; - //comboBox.SetSource (new ObservableCollection { gitString, "Со_хранить" }); - - //appWindow.Add (comboBox); - //comboBox.Text = gitString; - - label = new () { X = Pos.X (label), Y = Pos.Bottom (label) + 2, Text = "HexView:" }; + label = new () { X = Pos.X (label), Y = Pos.Bottom (checkBoxRight) + 2, Text = "HexView:" }; appWindow.Add (label); - var hexView = new HexView (new MemoryStream (Encoding.ASCII.GetBytes (gitString + " Со_хранить"))) + HexView hexView = new (new MemoryStream (Encoding.ASCII.GetBytes (gitString + " Со_хранить"))) { - X = 20, Y = Pos.Y (label), Width = Dim.Percent (60), Height = 5 + X = 20, + Y = Pos.Y (label), + Width = Dim.Percent (60), + Height = 5 }; appWindow.Add (hexView); label = new () { X = Pos.X (label), Y = Pos.Bottom (hexView) + 1, Text = "ListView:" }; appWindow.Add (label); - var listView = new ListView + ListView listView = new () { X = 20, Y = Pos.Y (label), @@ -179,28 +168,31 @@ public class UnicodeInMenu : Scenario label = new () { X = Pos.X (label), Y = Pos.Bottom (listView) + 1, Text = "OptionSelector:" }; appWindow.Add (label); - var optionSelector = new OptionSelector + OptionSelector optionSelector = new () { X = 20, Y = Pos.Y (label), Width = Dim.Percent (60), - Labels = new [] { "item #1", gitString, "Со_хранить", "𝔽𝕆𝕆𝔹𝔸ℝ" } + Labels = ["item #1", gitString, "Со_хранить", "𝔽𝕆𝕆𝔹𝔸ℝ"] }; appWindow.Add (optionSelector); label = new () { X = Pos.X (label), Y = Pos.Bottom (optionSelector) + 1, Text = "TextField:" }; appWindow.Add (label); - var textField = new TextField + TextField textField = new () { - X = 20, Y = Pos.Y (label), Width = Dim.Percent (60), Text = gitString + " = Со_хранить" + X = 20, + Y = Pos.Y (label), + Width = Dim.Percent (60), + Text = gitString + " = Со_хранить" }; appWindow.Add (textField); label = new () { X = Pos.X (label), Y = Pos.Bottom (textField) + 1, Text = "TextView:" }; appWindow.Add (label); - var textView = new TextView + TextView textView = new () { X = 20, Y = Pos.Y (label), @@ -210,12 +202,22 @@ public class UnicodeInMenu : Scenario }; appWindow.Add (textView); - // Run - Start the application. + // StatusBar + StatusBar statusBar = new ( + [ + new ( + Application.QuitKey, + "Выход", + () => Application.RequestStop () + ), + new (Key.F2, "Создать", null), + new (Key.F3, "Со_хранить", null) + ] + ); + appWindow.Add (statusBar); + Application.Run (appWindow); - appWindow.Dispose (); - - // Shutdown - Calling Application.Shutdown is required. Application.Shutdown (); } } diff --git a/Examples/UICatalog/Scenarios/WizardAsView.cs b/Examples/UICatalog/Scenarios/WizardAsView.cs index 65f417a82..9df72b835 100644 --- a/Examples/UICatalog/Scenarios/WizardAsView.cs +++ b/Examples/UICatalog/Scenarios/WizardAsView.cs @@ -1,4 +1,5 @@ - +#nullable enable + namespace UICatalog.Scenarios; [ScenarioMetadata ("WizardAsView", "Shows using the Wizard class in an non-modal way")] @@ -9,63 +10,58 @@ public class WizardAsView : Scenario { Application.Init (); - var menu = new MenuBar - { - Menus = - [ - new MenuBarItem ( - "_File", - new MenuItem [] - { - new ( - "_Restart Configuration...", - "", - () => MessageBox.Query ( - "Wizaard", - "Are you sure you want to reset the Wizard and start over?", - "Ok", - "Cancel" - ) - ), - new ( - "Re_boot Server...", - "", - () => MessageBox.Query ( - "Wizaard", - "Are you sure you want to reboot the server start over?", - "Ok", - "Cancel" - ) - ), - new ( - "_Shutdown Server...", - "", - () => MessageBox.Query ( - "Wizaard", - "Are you sure you want to cancel setup and shutdown?", - "Ok", - "Cancel" - ) - ) - } - ) - ] - }; + // MenuBar + MenuBar menu = new (); - Toplevel topLevel = new (); - topLevel.Add (menu); + menu.Add ( + new MenuBarItem ( + "_File", + [ + new MenuItem + { + Title = "_Restart Configuration...", + Action = () => MessageBox.Query ( + "Wizard", + "Are you sure you want to reset the Wizard and start over?", + "Ok", + "Cancel" + ) + }, + new MenuItem + { + Title = "Re_boot Server...", + Action = () => MessageBox.Query ( + "Wizard", + "Are you sure you want to reboot the server start over?", + "Ok", + "Cancel" + ) + }, + new MenuItem + { + Title = "_Shutdown Server...", + Action = () => MessageBox.Query ( + "Wizard", + "Are you sure you want to cancel setup and shutdown?", + "Ok", + "Cancel" + ) + } + ] + ) + ); // No need for a Title because the border is disabled - var wizard = new Wizard + Wizard wizard = new () { X = 0, - Y = 0, + Y = Pos.Bottom (menu), Width = Dim.Fill (), Height = Dim.Fill (), ShadowStyle = ShadowStyle.None }; - // Set Mdoal to false to cause the Wizard class to render without a frame and + // Set Modal to false to cause the Wizard class to render without a frame and // behave like an non-modal View (vs. a modal/pop-up Window). wizard.Modal = false; @@ -100,7 +96,7 @@ public class WizardAsView : Scenario }; // Add 1st step - var firstStep = new WizardStep { Title = "End User License Agreement" }; + WizardStep firstStep = new () { Title = "End User License Agreement" }; wizard.AddStep (firstStep); firstStep.NextButtonText = "Accept!"; @@ -108,46 +104,50 @@ public class WizardAsView : Scenario "This is the End User License Agreement.\n\n\n\n\n\nThis is a test of the emergency broadcast system. This is a test of the emergency broadcast system.\nThis is a test of the emergency broadcast system.\n\n\nThis is a test of the emergency broadcast system.\n\nThis is a test of the emergency broadcast system.\n\n\n\nThe end of the EULA."; // Add 2nd step - var secondStep = new WizardStep { Title = "Second Step" }; + WizardStep secondStep = new () { Title = "Second Step" }; wizard.AddStep (secondStep); secondStep.HelpText = "This is the help text for the Second Step.\n\nPress the button to change the Title.\n\nIf First Name is empty the step will prevent moving to the next step."; - var buttonLbl = new Label { Text = "Second Step Button: ", X = 0, Y = 0 }; + Label buttonLbl = new () { Text = "Second Step Button: ", X = 0, Y = 0 }; - var button = new Button + Button button = new () { - Text = "Press Me to Rename Step", X = Pos.Right (buttonLbl), Y = Pos.Top (buttonLbl) + Text = "Press Me to Rename Step", + X = Pos.Right (buttonLbl), + Y = Pos.Top (buttonLbl) }; button.Accepting += (s, e) => - { - secondStep.Title = "2nd Step"; + { + secondStep.Title = "2nd Step"; - MessageBox.Query ( - "Wizard Scenario", - "This Wizard Step's title was changed to '2nd Step'", - "Ok" - ); - }; + MessageBox.Query ( + "Wizard Scenario", + "This Wizard Step's title was changed to '2nd Step'", + "Ok" + ); + }; secondStep.Add (buttonLbl, button); - var lbl = new Label { Text = "First Name: ", X = Pos.Left (buttonLbl), Y = Pos.Bottom (buttonLbl) }; - var firstNameField = new TextField { Text = "Number", Width = 30, X = Pos.Right (lbl), Y = Pos.Top (lbl) }; + + Label lbl = new () { Text = "First Name: ", X = Pos.Left (buttonLbl), Y = Pos.Bottom (buttonLbl) }; + TextField firstNameField = new () { Text = "Number", Width = 30, X = Pos.Right (lbl), Y = Pos.Top (lbl) }; secondStep.Add (lbl, firstNameField); - lbl = new Label { Text = "Last Name: ", X = Pos.Left (buttonLbl), Y = Pos.Bottom (lbl) }; - var lastNameField = new TextField { Text = "Six", Width = 30, X = Pos.Right (lbl), Y = Pos.Top (lbl) }; + lbl = new () { Text = "Last Name: ", X = Pos.Left (buttonLbl), Y = Pos.Bottom (lbl) }; + TextField lastNameField = new () { Text = "Six", Width = 30, X = Pos.Right (lbl), Y = Pos.Top (lbl) }; secondStep.Add (lbl, lastNameField); // Add last step - var lastStep = new WizardStep { Title = "The last step" }; + WizardStep lastStep = new () { Title = "The last step" }; wizard.AddStep (lastStep); lastStep.HelpText = "The wizard is complete!\n\nPress the Finish button to continue.\n\nPressing Esc will cancel."; - wizard.Y = Pos.Bottom (menu); - topLevel.Add (wizard); + Window topLevel = new (); + topLevel.Add (menu, wizard); + Application.Run (topLevel); topLevel.Dispose (); Application.Shutdown (); diff --git a/Examples/UICatalog/UICatalogTop.cs b/Examples/UICatalog/UICatalogTop.cs index 304876a87..d02a73919 100644 --- a/Examples/UICatalog/UICatalogTop.cs +++ b/Examples/UICatalog/UICatalogTop.cs @@ -98,7 +98,7 @@ public class UICatalogTop : Toplevel #region MenuBar - private readonly MenuBarv2? _menuBar; + private readonly MenuBar? _menuBar; private CheckBox? _force16ColorsMenuItemCb; private OptionSelector? _themesSelector; private OptionSelector? _topSchemesSelector; @@ -106,14 +106,14 @@ public class UICatalogTop : Toplevel private FlagSelector? _diagnosticFlagsSelector; private CheckBox? _disableMouseCb; - private MenuBarv2 CreateMenuBar () + private MenuBar CreateMenuBar () { - MenuBarv2 menuBar = new ( + MenuBar menuBar = new ( [ new ( "_File", [ - new MenuItemv2 () + new MenuItem () { Title ="_Quit", HelpText = "Quit UI Catalog", @@ -128,19 +128,19 @@ public class UICatalogTop : Toplevel new ( "_Help", [ - new MenuItemv2 ( + new MenuItem ( "_Documentation", "API docs", () => OpenUrl ("https://gui-cs.github.io/Terminal.Gui"), Key.F1 ), - new MenuItemv2 ( + new MenuItem ( "_README", "Project readme", () => OpenUrl ("https://github.com/gui-cs/Terminal.Gui"), Key.F2 ), - new MenuItemv2 ( + new MenuItem ( "_About...", "About UI Catalog", () => MessageBox.Query ( @@ -192,7 +192,7 @@ public class UICatalogTop : Toplevel }; menuItems.Add ( - new MenuItemv2 + new MenuItem { CommandView = _force16ColorsMenuItemCb }); @@ -218,7 +218,7 @@ public class UICatalogTop : Toplevel }; - var menuItem = new MenuItemv2 + var menuItem = new MenuItem { CommandView = _themesSelector, HelpText = "Cycle Through Themes", @@ -263,7 +263,7 @@ public class UICatalogTop : Toplevel } else { - menuItems.Add (new MenuItemv2 () + menuItems.Add (new MenuItem () { Title = "Configuration Manager is not Enabled", Enabled = false @@ -293,7 +293,7 @@ public class UICatalogTop : Toplevel }; menuItems.Add ( - new MenuItemv2 + new MenuItem { CommandView = _diagnosticFlagsSelector, HelpText = "View Diagnostics" @@ -313,7 +313,7 @@ public class UICatalogTop : Toplevel _disableMouseCb.CheckedStateChanged += (_, args) => { Application.IsMouseDisabled = args.Value == CheckState.Checked; }; menuItems.Add ( - new MenuItemv2 + new MenuItem { CommandView = _disableMouseCb, HelpText = "Disable Mouse" @@ -345,7 +345,7 @@ public class UICatalogTop : Toplevel }; menuItems.Add ( - new MenuItemv2 + new MenuItem { CommandView = _logLevelSelector, HelpText = "Cycle Through Log Levels", @@ -356,7 +356,7 @@ public class UICatalogTop : Toplevel menuItems.Add (new Line ()); menuItems.Add ( - new MenuItemv2 ( + new MenuItem ( "_Open Log Folder", string.Empty, () => OpenUrl (UICatalog.LOGFILE_LOCATION) diff --git a/Scripts/Run-LocalCoverage.ps1 b/Scripts/Run-LocalCoverage.ps1 index 32b88053d..710bb26ea 100644 --- a/Scripts/Run-LocalCoverage.ps1 +++ b/Scripts/Run-LocalCoverage.ps1 @@ -26,8 +26,7 @@ dotnet test Tests/UnitTests ` --no-build ` --verbosity minimal ` --collect:"XPlat Code Coverage" ` - --settings Tests/UnitTests/runsettings.coverage.xml ` - --blame-hang-timeout 60s + --settings Tests/UnitTests/runsettings.coverage.xml # ------------------------------------------------------------ # 4. Run UNIT TESTS (parallel) diff --git a/Terminal.Gui/App/ApplicationImpl.Run.cs b/Terminal.Gui/App/ApplicationImpl.Run.cs index f5b785eb2..3d03f7f60 100644 --- a/Terminal.Gui/App/ApplicationImpl.Run.cs +++ b/Terminal.Gui/App/ApplicationImpl.Run.cs @@ -16,9 +16,11 @@ public partial class ApplicationImpl #region Begin->Run->Stop->End + // TODO: This API is not used anywhere; it can be deleted /// public event EventHandler? SessionBegun; + // TODO: This API is not used anywhere; it can be deleted /// public event EventHandler? SessionEnded; @@ -151,6 +153,12 @@ public partial class ApplicationImpl /// public bool StopAfterFirstIteration { get; set; } + /// + public void RaiseIteration () + { + Iteration?.Invoke (null, new ()); + } + /// public event EventHandler? Iteration; @@ -276,6 +284,9 @@ public partial class ApplicationImpl // BUGBUG: Why layout and draw here? This causes the screen to be cleared! //LayoutAndDraw (true); + // TODO: This API is not used (correctly) anywhere; it can be deleted + // TODO: Instead, callers should use the new equivalent of Toplevel.Ready + // TODO: which will be IsRunningChanged with newIsRunning == true SessionEnded?.Invoke (this, new (CachedSessionTokenToplevel)); } @@ -305,9 +316,6 @@ public partial class ApplicationImpl top.Running = false; } - /// - public void RaiseIteration () { Iteration?.Invoke (null, new ()); } - #endregion Begin->Run->Stop->End #region Timeouts and Invoke diff --git a/Terminal.Gui/App/IApplication.cs b/Terminal.Gui/App/IApplication.cs index a14f3534b..88e1d2d1f 100644 --- a/Terminal.Gui/App/IApplication.cs +++ b/Terminal.Gui/App/IApplication.cs @@ -330,6 +330,7 @@ public interface IApplication /// bool StopAfterFirstIteration { get; set; } + // TODO: This API is not used anywhere; it can be deleted /// /// Raised when has been called and has created a new . /// @@ -340,6 +341,7 @@ public interface IApplication /// public event EventHandler? SessionBegun; + // TODO: This API is not used anywhere; it can be deleted /// /// Raised when was called and the session is stopping. The event args contain a /// reference to the diff --git a/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs b/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs index e52af3f0e..3ad75c4e6 100644 --- a/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs +++ b/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs @@ -142,6 +142,11 @@ public class ApplicationMainLoop : IApplicationMainLoop /// Interface for class that handles bespoke behaviours that occur when application /// top level changes. diff --git a/Terminal.Gui/App/Toplevel/ToplevelTransitionManager.cs b/Terminal.Gui/App/Toplevel/ToplevelTransitionManager.cs index edb09d0e9..ee80619d8 100644 --- a/Terminal.Gui/App/Toplevel/ToplevelTransitionManager.cs +++ b/Terminal.Gui/App/Toplevel/ToplevelTransitionManager.cs @@ -1,5 +1,10 @@ namespace Terminal.Gui.App; +// TODO: This whole concept is bogus and over-engineered. +// TODO: Remove it and just let subscribers use the IApplication.Iteration +// TODO: If the requirement is they know if it's the first iteration, they can +// TODO: count invocations. + /// /// Handles bespoke behaviours that occur when application top level changes. /// diff --git a/Terminal.Gui/Resources/config.json b/Terminal.Gui/Resources/config.json index 20b206e38..65283b351 100644 --- a/Terminal.Gui/Resources/config.json +++ b/Terminal.Gui/Resources/config.json @@ -748,7 +748,7 @@ "Window.DefaultBorderStyle": "Single", "MessageBox.DefaultBorderStyle": "Single", "Button.DefaultShadow": "None", - "Menuv2.DefaultBorderStyle": "Single", + "Menu.DefaultBorderStyle": "Single", "Schemes": [ { "TopLevel": { @@ -885,7 +885,7 @@ "Window.DefaultBorderStyle": "Single", "MessageBox.DefaultBorderStyle": "Single", "Button.DefaultShadow": "None", - "Menuv2.DefaultBorderStyle": "Single", + "Menu.DefaultBorderStyle": "Single", "Schemes": [ { "TopLevel": { @@ -1022,7 +1022,7 @@ "Window.DefaultBorderStyle": "Single", "MessageBox.DefaultBorderStyle": "Single", "Button.DefaultShadow": "None", - "Menuv2.DefaultBorderStyle": "None", + "Menu.DefaultBorderStyle": "None", "Glyphs.LeftBracket": "[", "Glyphs.RightBracket": "]", "Glyphs.CheckStateChecked": "X", diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs index ecd1be459..f81f4c2b3 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -271,7 +271,7 @@ public class TextFormatter int size = isVertical ? screen.Height : screen.Width; int current = start + colOffset; List lastZeroWidthPos = null!; - string text = default; + string text = string.Empty; int zeroLengthCount = isVertical ? strings.EnumerateRunes ().Sum (r => r.GetColumns () == 0 ? 1 : 0) : 0; for (int idx = (isVertical ? start - y : start - x) + colOffset; diff --git a/Terminal.Gui/ViewBase/View.Layout.cs b/Terminal.Gui/ViewBase/View.Layout.cs index 76135f600..27eaaac0e 100644 --- a/Terminal.Gui/ViewBase/View.Layout.cs +++ b/Terminal.Gui/ViewBase/View.Layout.cs @@ -1187,36 +1187,10 @@ public partial class View // Layout APIs } //System.Diagnostics.Debug.WriteLine ($"nx:{nx}, rWidth:{rWidth}"); - var menuVisible = false; - var statusVisible = false; + //var menuVisible = false; + //var statusVisible = false; - if (viewToMove?.SuperView is null || viewToMove == app?.TopRunnable || viewToMove?.SuperView == app?.TopRunnable) - { - menuVisible = app?.TopRunnable?.MenuBar?.Visible == true; - } - else - { - View? t = viewToMove!.SuperView; - - while (t is { } and not Toplevel) - { - t = t.SuperView; - } - - if (t is Toplevel topLevel) - { - menuVisible = topLevel.MenuBar?.Visible == true; - } - } - - if (viewToMove?.SuperView is null || viewToMove == app?.TopRunnable || viewToMove?.SuperView == app?.TopRunnable) - { - maxDimension = menuVisible ? 1 : 0; - } - else - { - maxDimension = 0; - } + maxDimension = 0; ny = Math.Max (targetY, maxDimension); @@ -1224,7 +1198,7 @@ public partial class View // Layout APIs { if (app is { }) { - maxDimension = statusVisible ? app.Screen.Height - 1 : app.Screen.Height; + maxDimension = app.Screen.Height; } else { @@ -1233,7 +1207,7 @@ public partial class View // Layout APIs } else { - maxDimension = statusVisible ? viewToMove!.SuperView.Viewport.Height - 1 : viewToMove!.SuperView.Viewport.Height; + maxDimension = viewToMove!.SuperView.Viewport.Height; } if (superView?.Margin is { } && superView == viewToMove?.SuperView) @@ -1246,13 +1220,8 @@ public partial class View // Layout APIs if (viewToMove?.Frame.Height <= maxDimension) { ny = ny + viewToMove.Frame.Height > maxDimension - ? Math.Max (maxDimension - viewToMove.Frame.Height, menuVisible ? 1 : 0) + ? Math.Max (maxDimension - viewToMove.Frame.Height, 0) : ny; - - //if (ny > viewToMove.Frame.Y + viewToMove.Frame.Height) - //{ - // ny = Math.Max (viewToMove.Frame.Bottom, 0); - //} } else { diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.cs b/Terminal.Gui/Views/FileDialogs/FileDialog.cs index 4650730aa..a24c82f0a 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialog.cs @@ -42,9 +42,11 @@ public class FileDialog : Dialog, IDesignable private readonly TextField _tbFind; private readonly TextField _tbPath; private readonly TreeView _treeView; +#if MENU_V1 private MenuBarItem? _allowedTypeMenu; private MenuBar? _allowedTypeMenuBar; private MenuItem []? _allowedTypeMenuItems; +#endif private int _currentSortColumn; private bool _currentSortIsAsc = true; private bool _disposed; @@ -466,6 +468,7 @@ public class FileDialog : Dialog, IDesignable Style.IconProvider.IsOpenGetter = _treeView.IsExpanded; _treeView.AddObjects (_treeRoots.Keys); +#if MENU_V1 // if filtering on file type is configured then create the ComboBox and establish // initial filtering by extension(s) @@ -510,6 +513,7 @@ public class FileDialog : Dialog, IDesignable Add (_allowedTypeMenuBar); } +#endif // if no path has been provided if (_tbPath.Text.Length <= 0) @@ -728,6 +732,7 @@ public class FileDialog : Dialog, IDesignable Accept (false); } } +#if MENU_V1 private void AllowedTypeMenuClicked (int idx) { @@ -748,6 +753,7 @@ public class FileDialog : Dialog, IDesignable State?.RefreshChildren (); WriteStateToTableView (); } +#endif private string AspectGetter (object o) { diff --git a/Terminal.Gui/Views/Line.cs b/Terminal.Gui/Views/Line.cs index 8da81b99d..04ba5703f 100644 --- a/Terminal.Gui/Views/Line.cs +++ b/Terminal.Gui/Views/Line.cs @@ -161,10 +161,7 @@ public class Line : View, IOrientation public event EventHandler>? OrientationChanged; #pragma warning restore CS0067 // The event is never used - /// - /// Called when has changed. - /// - /// The new orientation value. + /// public void OnOrientationChanged (Orientation newOrientation) { // Set dimensions based on new orientation: diff --git a/Terminal.Gui/Views/Menu/Menuv2.cs b/Terminal.Gui/Views/Menu/Menu.cs similarity index 90% rename from Terminal.Gui/Views/Menu/Menuv2.cs rename to Terminal.Gui/Views/Menu/Menu.cs index 2cd610f41..b9efaa91f 100644 --- a/Terminal.Gui/Views/Menu/Menuv2.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -3,18 +3,18 @@ namespace Terminal.Gui.Views; /// -/// A -derived object to be used as a vertically-oriented menu. Each subview is a . +/// A -derived object to be used as a vertically-oriented menu. Each subview is a . /// -public class Menuv2 : Bar +public class Menu : Bar { /// - public Menuv2 () : this ([]) { } + public Menu () : this ([]) { } /// - public Menuv2 (IEnumerable? menuItems) : this (menuItems?.Cast ()) { } + public Menu (IEnumerable? menuItems) : this (menuItems?.Cast ()) { } /// - public Menuv2 (IEnumerable? shortcuts) : base (shortcuts) + public Menu (IEnumerable? shortcuts) : base (shortcuts) { // Do this to support debugging traces where Title gets set base.HotKeySpecifier = (Rune)'\xffff'; @@ -51,14 +51,14 @@ public class Menuv2 : Bar /// /// Gets or sets the menu item that opened this menu as a sub-menu. /// - public MenuItemv2? SuperMenuItem { get; set; } + public MenuItem? SuperMenuItem { get; set; } /// protected override void OnVisibleChanged () { if (Visible) { - SelectedMenuItem = SubViews.Where (mi => mi is MenuItemv2).ElementAtOrDefault (0) as MenuItemv2; + SelectedMenuItem = SubViews.Where (mi => mi is MenuItem).ElementAtOrDefault (0) as MenuItem; } } @@ -69,7 +69,7 @@ public class Menuv2 : Bar switch (view) { - case MenuItemv2 menuItem: + case MenuItem menuItem: { menuItem.CanFocus = true; @@ -176,7 +176,7 @@ public class Menuv2 : Bar { base.OnFocusedChanged (previousFocused, focused); - SelectedMenuItem = focused as MenuItemv2; + SelectedMenuItem = focused as MenuItem; RaiseSelectedMenuItemChanged (SelectedMenuItem); } @@ -184,9 +184,9 @@ public class Menuv2 : Bar /// Gets or set the currently selected menu item. This is a helper that /// tracks . /// - public MenuItemv2? SelectedMenuItem + public MenuItem? SelectedMenuItem { - get => Focused as MenuItemv2; + get => Focused as MenuItem; set { if (value == Focused) @@ -198,7 +198,7 @@ public class Menuv2 : Bar } } - internal void RaiseSelectedMenuItemChanged (MenuItemv2? selected) + internal void RaiseSelectedMenuItemChanged (MenuItem? selected) { // Logging.Debug ($"{Title} ({selected?.Title})"); @@ -210,14 +210,14 @@ public class Menuv2 : Bar /// Called when the selected menu item has changed. /// /// - protected virtual void OnSelectedMenuItemChanged (MenuItemv2? selected) + protected virtual void OnSelectedMenuItemChanged (MenuItem? selected) { } /// /// Raised when the selected menu item has changed. /// - public event EventHandler? SelectedMenuItemChanged; + public event EventHandler? SelectedMenuItemChanged; /// protected override void Dispose (bool disposing) diff --git a/Terminal.Gui/Views/Menu/MenuBarv2.cs b/Terminal.Gui/Views/Menu/MenuBar.cs similarity index 88% rename from Terminal.Gui/Views/Menu/MenuBarv2.cs rename to Terminal.Gui/Views/Menu/MenuBar.cs index 746cdb44d..67312b01e 100644 --- a/Terminal.Gui/Views/Menu/MenuBarv2.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -5,20 +5,20 @@ using System.Diagnostics; namespace Terminal.Gui.Views; /// -/// A horizontal list of s. Each can have a -/// that is shown when the is selected. +/// A horizontal list of s. Each can have a +/// that is shown when the is selected. /// /// /// MenuBars may be hosted by any View and will, by default, be positioned the full width across the top of the View's /// Viewport. /// -public class MenuBarv2 : Menuv2, IDesignable +public class MenuBar : Menu, IDesignable { /// - public MenuBarv2 () : this ([]) { } + public MenuBar () : this ([]) { } /// - public MenuBarv2 (IEnumerable menuBarItems) : base (menuBarItems) + public MenuBar (IEnumerable menuBarItems) : base (menuBarItems) { CanFocus = false; TabStop = TabBehavior.TabGroup; @@ -45,7 +45,7 @@ public class MenuBarv2 : Menuv2, IDesignable return true; } - if (SubViews.OfType ().FirstOrDefault (mbi => mbi.PopoverMenu is { }) is { } first) + if (SubViews.OfType ().FirstOrDefault (mbi => mbi.PopoverMenu is { }) is { } first) { Active = true; ShowItem (first); @@ -152,7 +152,7 @@ public class MenuBarv2 : Menuv2, IDesignable /// This is a convenience property to help porting from the v1 MenuBar. /// /// - public MenuBarItemv2 []? Menus + public MenuBarItem []? Menus { set { @@ -163,7 +163,7 @@ public class MenuBarv2 : Menuv2, IDesignable return; } - foreach (MenuBarItemv2 mbi in value) + foreach (MenuBarItem mbi in value) { Add (mbi); } @@ -175,7 +175,7 @@ public class MenuBarv2 : Menuv2, IDesignable { base.OnSubViewAdded (view); - if (view is MenuBarItemv2 mbi) + if (view is MenuBarItem mbi) { mbi.Accepted += OnMenuBarItemAccepted; mbi.PopoverMenuOpenChanged += OnMenuBarItemPopoverMenuOpenChanged; @@ -187,7 +187,7 @@ public class MenuBarv2 : Menuv2, IDesignable { base.OnSubViewRemoved (view); - if (view is MenuBarItemv2 mbi) + if (view is MenuBarItem mbi) { mbi.Accepted -= OnMenuBarItemAccepted; mbi.PopoverMenuOpenChanged -= OnMenuBarItemPopoverMenuOpenChanged; @@ -196,7 +196,7 @@ public class MenuBarv2 : Menuv2, IDesignable private void OnMenuBarItemPopoverMenuOpenChanged (object? sender, EventArgs e) { - if (sender is MenuBarItemv2 mbi) + if (sender is MenuBarItem mbi) { if (e.Value) { @@ -223,7 +223,7 @@ public class MenuBarv2 : Menuv2, IDesignable /// Gets whether any of the menu bar items have a visible . /// /// - public bool IsOpen () { return SubViews.OfType ().Count (sv => sv is { PopoverMenuOpen: true }) > 0; } + public bool IsOpen () { return SubViews.OfType ().Count (sv => sv is { PopoverMenuOpen: true }) > 0; } private bool _active; @@ -297,11 +297,11 @@ public class MenuBarv2 : Menuv2, IDesignable } /// - protected override void OnSelectedMenuItemChanged (MenuItemv2? selected) + protected override void OnSelectedMenuItemChanged (MenuItem? selected) { // Logging.Debug ($"{Title} ({selected?.Title}) - IsOpen: {IsOpen ()}"); - if (IsOpen () && selected is MenuBarItemv2 { PopoverMenuOpen: false } selectedMenuBarItem) + if (IsOpen () && selected is MenuBarItem { PopoverMenuOpen: false } selectedMenuBarItem) { ShowItem (selectedMenuBarItem); } @@ -319,7 +319,7 @@ public class MenuBarv2 : Menuv2, IDesignable } // TODO: This needs to be done whenever a menuitem in any MenuBarItem changes - foreach (MenuBarItemv2? mbi in SubViews.Select (s => s as MenuBarItemv2)) + foreach (MenuBarItem? mbi in SubViews.Select (s => s as MenuBarItem)) { App?.Popover?.Register (mbi?.PopoverMenu); } @@ -331,7 +331,7 @@ public class MenuBarv2 : Menuv2, IDesignable // Logging.Debug ($"{Title} ({args.Context?.Source?.Title})"); // TODO: Ensure sourceMenuBar is actually one of our bar items - if (Visible && Enabled && args.Context?.Source is MenuBarItemv2 { PopoverMenuOpen: false } sourceMenuBarItem) + if (Visible && Enabled && args.Context?.Source is MenuBarItem { PopoverMenuOpen: false } sourceMenuBarItem) { if (!CanFocus) { @@ -365,7 +365,7 @@ public class MenuBarv2 : Menuv2, IDesignable // Logging.Debug ($"{Title} ({args.Context?.Source?.Title}) Command: {args.Context?.Command}"); base.OnAccepted (args); - if (SubViews.OfType ().Contains (args.Context?.Source)) + if (SubViews.OfType ().Contains (args.Context?.Source)) { return; } @@ -377,7 +377,7 @@ public class MenuBarv2 : Menuv2, IDesignable /// Shows the specified popover, but only if the menu bar is active. /// /// - private void ShowItem (MenuBarItemv2? menuBarItem) + private void ShowItem (MenuBarItem? menuBarItem) { // Logging.Debug ($"{Title} - {menuBarItem?.Id}"); @@ -446,7 +446,7 @@ public class MenuBarv2 : Menuv2, IDesignable } } - private MenuBarItemv2? GetActiveItem () { return SubViews.OfType ().FirstOrDefault (sv => sv is { PopoverMenu: { Visible: true } }); } + private MenuBarItem? GetActiveItem () { return SubViews.OfType ().FirstOrDefault (sv => sv is { PopoverMenu: { Visible: true } }); } /// /// Hides the popover menu associated with the active menu bar item and updates the focus state. @@ -459,7 +459,7 @@ public class MenuBarv2 : Menuv2, IDesignable /// /// /// if the popover was hidden - public bool HideItem (MenuBarItemv2? activeItem) + public bool HideItem (MenuBarItem? activeItem) { // Logging.Debug ($"{Title} ({activeItem?.Title}) - Active: {Active}, CanFocus: {CanFocus}, HasFocus: {HasFocus}"); @@ -484,16 +484,16 @@ public class MenuBarv2 : Menuv2, IDesignable /// /// /// - public IEnumerable GetMenuItemsWithTitle (string title) + public IEnumerable GetMenuItemsWithTitle (string title) { - List menuItems = new (); + List menuItems = new (); if (string.IsNullOrEmpty (title)) { return menuItems; } - foreach (MenuBarItemv2 mbi in SubViews.OfType ()) + foreach (MenuBarItem mbi in SubViews.OfType ()) { if (mbi.PopoverMenu is { }) { @@ -558,15 +558,15 @@ public class MenuBarv2 : Menuv2, IDesignable }; Add ( - new MenuBarItemv2 ( + new MenuBarItem ( "_File", [ - new MenuItemv2 (targetView as View, Command.New), - new MenuItemv2 (targetView as View, Command.Open), - new MenuItemv2 (targetView as View, Command.Save), - new MenuItemv2 (targetView as View, Command.SaveAs), + new MenuItem (targetView as View, Command.New), + new MenuItem (targetView as View, Command.Open), + new MenuItem (targetView as View, Command.Save), + new MenuItem (targetView as View, Command.SaveAs), new Line (), - new MenuItemv2 + new MenuItem { Title = "_File Options", SubMenu = new ( @@ -601,25 +601,25 @@ public class MenuBarv2 : Menuv2, IDesignable ) }, new Line (), - new MenuItemv2 + new MenuItem { Title = "_Preferences", SubMenu = new ( [ - new MenuItemv2 + new MenuItem { CommandView = bordersCb, HelpText = "Toggle Menu Borders", Action = ToggleMenuBorders }, - new MenuItemv2 + new MenuItem { HelpText = "3 Mutually Exclusive Options", CommandView = mutuallyExclusiveOptionsSelector, Key = Key.F7 }, new Line (), - new MenuItemv2 + new MenuItem { HelpText = "MenuBar BG Color", CommandView = menuBgColorCp, @@ -629,7 +629,7 @@ public class MenuBarv2 : Menuv2, IDesignable ) }, new Line (), - new MenuItemv2 + new MenuItem { TargetView = targetView as View, Key = Application.QuitKey, @@ -640,16 +640,16 @@ public class MenuBarv2 : Menuv2, IDesignable ); Add ( - new MenuBarItemv2 ( + new MenuBarItem ( "_Edit", [ - new MenuItemv2 (targetView as View, Command.Cut), - new MenuItemv2 (targetView as View, Command.Copy), - new MenuItemv2 (targetView as View, Command.Paste), + new MenuItem (targetView as View, Command.Cut), + new MenuItem (targetView as View, Command.Copy), + new MenuItem (targetView as View, Command.Paste), new Line (), - new MenuItemv2 (targetView as View, Command.SelectAll), + new MenuItem (targetView as View, Command.SelectAll), new Line (), - new MenuItemv2 + new MenuItem { Title = "_Details", SubMenu = new (ConfigureDetailsSubMenu ()) @@ -659,15 +659,15 @@ public class MenuBarv2 : Menuv2, IDesignable ); Add ( - new MenuBarItemv2 ( + new MenuBarItem ( "_Help", [ - new MenuItemv2 + new MenuItem { Title = "_Online Help...", Action = () => MessageBox.Query ("Online Help", "https://gui-cs.github.io/Terminal.Gui", "Ok") }, - new MenuItemv2 + new MenuItem { Title = "About...", Action = () => MessageBox.Query ("About", "Something About Mary.", "Ok") @@ -680,14 +680,14 @@ public class MenuBarv2 : Menuv2, IDesignable void ToggleMenuBorders () { - foreach (MenuBarItemv2 mbi in SubViews.OfType ()) + foreach (MenuBarItem mbi in SubViews.OfType ()) { if (mbi is not { PopoverMenu: { } }) { continue; } - foreach (Menuv2? subMenu in mbi.PopoverMenu.GetAllSubMenus ()) + foreach (Menu? subMenu in mbi.PopoverMenu.GetAllSubMenus ()) { if (bordersCb.CheckedState == CheckState.Checked) { @@ -701,21 +701,21 @@ public class MenuBarv2 : Menuv2, IDesignable } } - MenuItemv2 [] ConfigureDetailsSubMenu () + MenuItem [] ConfigureDetailsSubMenu () { - var detail = new MenuItemv2 + var detail = new MenuItem { Title = "_Detail 1", Text = "Some detail #1" }; - var nestedSubMenu = new MenuItemv2 + var nestedSubMenu = new MenuItem { Title = "_Moar Details", SubMenu = new (ConfigureMoreDetailsSubMenu ()) }; - var editMode = new MenuItemv2 + var editMode = new MenuItem { Text = "App Binding to Command.Edit", Id = "EditMode", @@ -730,14 +730,14 @@ public class MenuBarv2 : Menuv2, IDesignable View [] ConfigureMoreDetailsSubMenu () { - var deeperDetail = new MenuItemv2 + var deeperDetail = new MenuItem { Title = "_Deeper Detail", Text = "Deeper Detail", Action = () => { MessageBox.Query ("Deeper Detail", "Lots of details", "_Ok"); } }; - var belowLineDetail = new MenuItemv2 + var belowLineDetail = new MenuItem { Title = "_Even more detail", Text = "Below the line" diff --git a/Terminal.Gui/Views/Menu/MenuBarItemv2.cs b/Terminal.Gui/Views/Menu/MenuBarItem.cs similarity index 86% rename from Terminal.Gui/Views/Menu/MenuBarItemv2.cs rename to Terminal.Gui/Views/Menu/MenuBarItem.cs index 8d04c3e2b..fb353acbf 100644 --- a/Terminal.Gui/Views/Menu/MenuBarItemv2.cs +++ b/Terminal.Gui/Views/Menu/MenuBarItem.cs @@ -5,18 +5,18 @@ using System.Diagnostics; namespace Terminal.Gui.Views; /// -/// A -derived object to be used as items in a . +/// A -derived object to be used as items in a . /// MenuBarItems hold a instead of a . /// -public class MenuBarItemv2 : MenuItemv2 +public class MenuBarItem : MenuItem { /// - /// Creates a new instance of . + /// Creates a new instance of . /// - public MenuBarItemv2 () : base (null, Command.NotBound) { } + public MenuBarItem () : base (null, Command.NotBound) { } /// - /// Creates a new instance of . Each MenuBarItem typically has a + /// Creates a new instance of . Each MenuBarItem typically has a /// that is /// shown when the item is selected. /// @@ -32,7 +32,7 @@ public class MenuBarItemv2 : MenuItemv2 /// /// The text to display for the command. /// The Popover Menu that will be displayed when this item is selected. - public MenuBarItemv2 (View? targetView, Command command, string? commandText, PopoverMenu? popoverMenu = null) + public MenuBarItem (View? targetView, Command command, string? commandText, PopoverMenu? popoverMenu = null) : base ( targetView, command, @@ -44,14 +44,14 @@ public class MenuBarItemv2 : MenuItemv2 } /// - /// Creates a new instance of with the specified . This is a + /// Creates a new instance of with the specified . This is a /// helper for the most common MenuBar use-cases. /// /// /// /// The text to display for the command. /// The Popover Menu that will be displayed when this item is selected. - public MenuBarItemv2 (string commandText, PopoverMenu? popoverMenu = null) + public MenuBarItem (string commandText, PopoverMenu? popoverMenu = null) : this ( null, Command.NotBound, @@ -60,7 +60,7 @@ public class MenuBarItemv2 : MenuItemv2 { } /// - /// Creates a new instance of with the automatcialy added to a + /// Creates a new instance of with the automatcialy added to a /// . /// This is a helper for the most common MenuBar use-cases. /// @@ -71,7 +71,7 @@ public class MenuBarItemv2 : MenuItemv2 /// The menu items that will be added to the Popover Menu that will be displayed when this item is /// selected. /// - public MenuBarItemv2 (string commandText, IEnumerable menuItems) + public MenuBarItem (string commandText, IEnumerable menuItems) : this ( null, Command.NotBound, @@ -83,7 +83,7 @@ public class MenuBarItemv2 : MenuItemv2 /// Do not use this property. MenuBarItem does not support SubMenu. Use instead. /// /// - public new Menuv2? SubMenu + public new Menu? SubMenu { get => null; set => throw new InvalidOperationException ("MenuBarItem does not support SubMenu. Use PopoverMenu instead."); @@ -185,7 +185,7 @@ public class MenuBarItemv2 : MenuItemv2 { // If the user presses the hotkey for a menu item that is already open, // it should close the menu item (Test: MenuBarItem_HotKey_DeActivates) - if (SuperView is MenuBarv2 { } menuBar) + if (SuperView is MenuBar { } menuBar) { menuBar.HideActiveItem (); } diff --git a/Terminal.Gui/Views/Menu/MenuItemv2.cs b/Terminal.Gui/Views/Menu/MenuItem.cs similarity index 90% rename from Terminal.Gui/Views/Menu/MenuItemv2.cs rename to Terminal.Gui/Views/Menu/MenuItem.cs index 7533881cd..01ca14922 100644 --- a/Terminal.Gui/Views/Menu/MenuItemv2.cs +++ b/Terminal.Gui/Views/Menu/MenuItem.cs @@ -4,19 +4,19 @@ using System.ComponentModel; namespace Terminal.Gui.Views; /// -/// A -derived object to be used as a menu item in a . Has title, an -/// A -derived object to be used as a menu item in a . Has title, an +/// A -derived object to be used as a menu item in a . Has title, an +/// A -derived object to be used as a menu item in a . Has title, an /// associated help text, and an action to execute on activation. /// -public class MenuItemv2 : Shortcut +public class MenuItem : Shortcut { /// - /// Creates a new instance of . + /// Creates a new instance of . /// - public MenuItemv2 () : base (Key.Empty, null, null) { } + public MenuItem () : base (Key.Empty, null, null) { } /// - /// Creates a new instance of , binding it to and + /// Creates a new instance of , binding it to and /// . The Key /// has bound to will be used as . /// @@ -34,7 +34,7 @@ public class MenuItemv2 : Shortcut /// The text to display for the command. /// The help text to display. /// The submenu to display when the user selects this menu item. - public MenuItemv2 (View? targetView, Command command, string? commandText = null, string? helpText = null, Menuv2? subMenu = null) + public MenuItem (View? targetView, Command command, string? commandText = null, string? helpText = null, Menu? subMenu = null) : base ( targetView?.HotKeyBindings.GetFirstFromCommands (command)!, string.IsNullOrEmpty (commandText) ? GlobalResources.GetString ($"cmd.{command}") : commandText, @@ -48,17 +48,17 @@ public class MenuItemv2 : Shortcut } /// - public MenuItemv2 (string? commandText = null, string? helpText = null, Action? action = null, Key? key = null) + public MenuItem (string? commandText = null, string? helpText = null, Action? action = null, Key? key = null) : base (key ?? Key.Empty, commandText, action, helpText) { } /// - public MenuItemv2 (string commandText, Key key, Action? action = null) + public MenuItem (string commandText, Key key, Action? action = null) : base (key ?? Key.Empty, commandText, action, null) { } /// - public MenuItemv2 (string? commandText = null, string? helpText = null, Menuv2? subMenu = null) + public MenuItem (string? commandText = null, string? helpText = null, Menu? subMenu = null) : base (Key.Empty, commandText, null, helpText) { SubMenu = subMenu; @@ -171,12 +171,12 @@ public class MenuItemv2 : Shortcut // return ret is true; //} - private Menuv2? _subMenu; + private Menu? _subMenu; /// /// The submenu to display when the user selects this menu item. /// - public Menuv2? SubMenu + public Menu? SubMenu { get => _subMenu; set diff --git a/Terminal.Gui/Views/Menu/PopoverMenu.cs b/Terminal.Gui/Views/Menu/PopoverMenu.cs index c04041bf5..ef0767218 100644 --- a/Terminal.Gui/Views/Menu/PopoverMenu.cs +++ b/Terminal.Gui/Views/Menu/PopoverMenu.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui.Views; /// /// Provides a cascading menu that pops over all other content. Can be used as a context menu or a drop-down /// all other content. Can be used as a context menu or a drop-down -/// menu as part of as part of . +/// menu as part of as part of . /// /// /// @@ -18,7 +18,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable /// /// Initializes a new instance of the class. /// - public PopoverMenu () : this ((Menuv2?)null) { } + public PopoverMenu () : this ((Menu?)null) { } /// /// Initializes a new instance of the class. If any of the elements of @@ -26,24 +26,24 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable /// a see will be created instead. /// public PopoverMenu (IEnumerable? menuItems) : this ( - new Menuv2 (menuItems?.Select (item => item ?? new Line ())) + new Menu (menuItems?.Select (item => item ?? new Line ())) { Title = "Popover Root" }) { } /// - public PopoverMenu (IEnumerable? menuItems) : this ( - new Menuv2 (menuItems) + public PopoverMenu (IEnumerable? menuItems) : this ( + new Menu (menuItems) { Title = "Popover Root" }) { } /// - /// Initializes a new instance of the class with the specified root . + /// Initializes a new instance of the class with the specified root . /// - public PopoverMenu (Menuv2? root) + public PopoverMenu (Menu? root) { // Do this to support debugging traces where Title gets set base.HotKeySpecifier = (Rune)'\xffff'; @@ -107,7 +107,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable return false; } - if (MostFocused is MenuItemv2 { SuperView: Menuv2 focusedMenu }) + if (MostFocused is MenuItem { SuperView: Menu focusedMenu }) { focusedMenu.SuperMenuItem?.SetFocus (); @@ -119,7 +119,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable bool? MoveRight (ICommandContext? ctx) { - if (MostFocused is MenuItemv2 { SubMenu.Visible: true } focused) + if (MostFocused is MenuItem { SubMenu.Visible: true } focused) { focused.SubMenu.SetFocus (); @@ -228,12 +228,12 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable } } - private Menuv2? _root; + private Menu? _root; /// - /// Gets or sets the that is the root of the Popover Menu. + /// Gets or sets the that is the root of the Popover Menu. /// - public Menuv2? Root + public Menu? Root { get => _root; set @@ -257,9 +257,9 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable UpdateKeyBindings (); // TODO: This needs to be done whenever any MenuItem in the menu tree changes to support dynamic menus - IEnumerable allMenus = GetAllSubMenus (); + IEnumerable allMenus = GetAllSubMenus (); - foreach (Menuv2 menu in allMenus) + foreach (Menu menu in allMenus) { menu.App = App; menu.Visible = false; @@ -272,9 +272,9 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable private void UpdateKeyBindings () { - IEnumerable all = GetMenuItemsOfAllSubMenus (); + IEnumerable all = GetMenuItemsOfAllSubMenus (); - foreach (MenuItemv2 menuItem in all.Where (mi => mi.Command != Command.NotBound)) + foreach (MenuItem menuItem in all.Where (mi => mi.Command != Command.NotBound)) { Key? key; @@ -309,9 +309,9 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable protected override bool OnKeyDownNotHandled (Key key) { // See if any of our MenuItems have this key as Key - IEnumerable all = GetMenuItemsOfAllSubMenus (); + IEnumerable all = GetMenuItemsOfAllSubMenus (); - foreach (MenuItemv2 menuItem in all) + foreach (MenuItem menuItem in all) { if (key != Application.QuitKey && menuItem.Key == key) { @@ -328,26 +328,26 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable /// Gets all the submenus in the PopoverMenu. /// /// - public IEnumerable GetAllSubMenus () + public IEnumerable GetAllSubMenus () { - List result = []; + List result = []; if (Root == null) { return result; } - Stack stack = new (); + Stack stack = new (); stack.Push (Root); while (stack.Count > 0) { - Menuv2 currentMenu = stack.Pop (); + Menu currentMenu = stack.Pop (); result.Add (currentMenu); foreach (View subView in currentMenu.SubViews) { - if (subView is MenuItemv2 { SubMenu: { } } menuItem) + if (subView is MenuItem { SubMenu: { } } menuItem) { stack.Push (menuItem.SubMenu); } @@ -361,15 +361,15 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable /// Gets all the MenuItems in the PopoverMenu. /// /// - internal IEnumerable GetMenuItemsOfAllSubMenus () + internal IEnumerable GetMenuItemsOfAllSubMenus () { - List result = []; + List result = []; - foreach (Menuv2 menu in GetAllSubMenus ()) + foreach (Menu menu in GetAllSubMenus ()) { foreach (View subView in menu.SubViews) { - if (subView is MenuItemv2 menuItem) + if (subView is MenuItem menuItem) { result.Add (menuItem); } @@ -383,16 +383,16 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable /// Pops up the submenu of the specified MenuItem, if there is one. /// /// - internal void ShowSubMenu (MenuItemv2? menuItem) + internal void ShowSubMenu (MenuItem? menuItem) { - var menu = menuItem?.SuperView as Menuv2; + var menu = menuItem?.SuperView as Menu; // Logging.Debug ($"{Title} - menuItem: {menuItem?.Title}, menu: {menu?.Title}"); menu?.Layout (); // If there's a visible peer, remove / hide it - if (menu?.SubViews.FirstOrDefault (v => v is MenuItemv2 { SubMenu.Visible: true }) is MenuItemv2 visiblePeer) + if (menu?.SubViews.FirstOrDefault (v => v is MenuItem { SubMenu.Visible: true }) is MenuItem visiblePeer) { HideAndRemoveSubMenu (visiblePeer.SubMenu); visiblePeer.ForceFocusColors = false; @@ -421,7 +421,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable /// The menu to locate. /// Ideal screen-relative location. /// - internal Point GetMostVisibleLocationForSubMenu (Menuv2 menu, Point idealLocation) + internal Point GetMostVisibleLocationForSubMenu (Menu menu, Point idealLocation) { var pos = Point.Empty; @@ -436,7 +436,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable return new (nx, ny); } - private void AddAndShowSubMenu (Menuv2? menu) + private void AddAndShowSubMenu (Menu? menu) { if (menu is { SuperView: null, Visible: false }) { @@ -462,14 +462,14 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable } } - private void HideAndRemoveSubMenu (Menuv2? menu) + private void HideAndRemoveSubMenu (Menu? menu) { if (menu is { Visible: true }) { // Logging.Debug ($"{Title} ({menu?.Title}) - menu.Visible: {menu?.Visible}"); // If there's a visible submenu, remove / hide it - if (menu.SubViews.FirstOrDefault (v => v is MenuItemv2 { SubMenu.Visible: true }) is MenuItemv2 visiblePeer) + if (menu.SubViews.FirstOrDefault (v => v is MenuItem { SubMenu.Visible: true }) is MenuItem visiblePeer) { HideAndRemoveSubMenu (visiblePeer.SubMenu); visiblePeer.ForceFocusColors = false; @@ -511,11 +511,11 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable { // Logging.Debug ($"{Title} ({e.Context?.Source?.Title}) Command: {e.Context?.Command}"); - if (e.Context?.Source is MenuItemv2 { SubMenu: null }) + if (e.Context?.Source is MenuItem { SubMenu: null }) { HideAndRemoveSubMenu (_root); } - else if (e.Context?.Source is MenuItemv2 { SubMenu: { } } menuItemWithSubMenu) + else if (e.Context?.Source is MenuItem { SubMenu: { } } menuItemWithSubMenu) { ShowSubMenu (menuItemWithSubMenu); } @@ -595,7 +595,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable /// public event EventHandler? Accepted; - private void MenuOnSelectedMenuItemChanged (object? sender, MenuItemv2? e) + private void MenuOnSelectedMenuItemChanged (object? sender, MenuItem? e) { // Logging.Debug ($"{Title} - e.Title: {e?.Title}"); ShowSubMenu (e); @@ -604,7 +604,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable /// protected override void OnSubViewAdded (View view) { - if (Root is null && (view is Menuv2 || view is MenuItemv2)) + if (Root is null && (view is Menu || view is MenuItem)) { throw new InvalidOperationException ("Do not add MenuItems or Menus directly to a PopoverMenu. Use the Root property."); } @@ -617,9 +617,9 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable { if (disposing) { - IEnumerable allMenus = GetAllSubMenus (); + IEnumerable allMenus = GetAllSubMenus (); - foreach (Menuv2 menu in allMenus) + foreach (Menu menu in allMenus) { menu.Accepting -= MenuOnAccepting; menu.Accepted -= MenuAccepted; @@ -641,13 +641,13 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable Root = new ( [ - new MenuItemv2 (targetView as View, Command.Cut), - new MenuItemv2 (targetView as View, Command.Copy), - new MenuItemv2 (targetView as View, Command.Paste), + new MenuItem (targetView as View, Command.Cut), + new MenuItem (targetView as View, Command.Copy), + new MenuItem (targetView as View, Command.Paste), new Line (), - new MenuItemv2 (targetView as View, Command.SelectAll), + new MenuItem (targetView as View, Command.SelectAll), new Line (), - new MenuItemv2 (targetView as View, Command.Quit) + new MenuItem (targetView as View, Command.Quit) ]) { Title = "Popover Demo Root" diff --git a/Terminal.Gui/Views/Menuv1/Menu.cs b/Terminal.Gui/Views/Menuv1/Menu.cs deleted file mode 100644 index 61b58649a..000000000 --- a/Terminal.Gui/Views/Menuv1/Menu.cs +++ /dev/null @@ -1,1009 +0,0 @@ -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. - - -namespace Terminal.Gui.Views; - -#pragma warning disable CS0618 // Type or member is obsolete - -/// -/// An internal class used to represent a menu pop-up menu. Created and managed by . -/// -internal sealed class Menu : View -{ - public Menu () - { - if (Application.TopRunnable is { }) - { - Application.TopRunnable.DrawComplete += Top_DrawComplete; - Application.TopRunnable.SizeChanging += Current_TerminalResized; - } - - Application.MouseEvent += Application_RootMouseEvent; - Application.Mouse.UnGrabbedMouse += Application_UnGrabbedMouse; - - // Things this view knows how to do - AddCommand (Command.Up, () => MoveUp ()); - AddCommand (Command.Down, () => MoveDown ()); - - AddCommand ( - Command.Left, - () => - { - _host!.PreviousMenu (true); - - return true; - } - ); - - AddCommand ( - Command.Cancel, - () => - { - CloseAllMenus (); - - return true; - } - ); - - AddCommand ( - Command.Accept, - () => - { - RunSelected (); - - return true; - } - ); - - AddCommand ( - Command.Select, - ctx => - { - if (ctx is not CommandContext keyCommandContext) - { - return false; - } - - return _host?.SelectItem ((keyCommandContext.Binding.Data as MenuItem)!); - }); - - AddCommand ( - Command.Toggle, - ctx => - { - if (ctx is not CommandContext keyCommandContext) - { - return false; - } - - return ExpandCollapse ((keyCommandContext.Binding.Data as MenuItem)!); - }); - - AddCommand ( - Command.HotKey, - ctx => - { - if (ctx is not CommandContext keyCommandContext) - { - return false; - } - - return _host?.SelectItem ((keyCommandContext.Binding.Data as MenuItem)!); - }); - - // Default key bindings for this view - KeyBindings.Add (Key.CursorUp, Command.Up); - KeyBindings.Add (Key.CursorDown, Command.Down); - KeyBindings.Add (Key.CursorLeft, Command.Left); - KeyBindings.Add (Key.CursorRight, Command.Right); - KeyBindings.Add (Key.Esc, Command.Cancel); - } - - internal int _currentChild; - internal View? _previousSubFocused; - private readonly MenuBarItem? _barItems; - private readonly MenuBar _host; - - public override void BeginInit () - { - base.BeginInit (); - - Rectangle frame = MakeFrame (Frame.X, Frame.Y, _barItems!.Children!, Parent); - - if (Frame.X != frame.X) - { - X = frame.X; - } - - if (Frame.Y != frame.Y) - { - Y = frame.Y; - } - - Width = frame.Width; - Height = frame.Height; - - if (_barItems.Children is { }) - { - foreach (MenuItem? menuItem in _barItems.Children) - { - if (menuItem is { }) - { - menuItem._menuBar = Host; - - if (menuItem.ShortcutKey != Key.Empty) - { - KeyBinding keyBinding = new ([Command.Select], this, data: menuItem); - - // Remove an existent ShortcutKey - menuItem._menuBar.HotKeyBindings.Remove (menuItem.ShortcutKey!); - menuItem._menuBar.HotKeyBindings.Add (menuItem.ShortcutKey!, keyBinding); - } - } - } - } - - if (_barItems is { IsTopLevel: true }) - { - // This is a standalone MenuItem on a MenuBar - SetScheme (_host.GetScheme ()); - CanFocus = true; - } - else - { - _currentChild = -1; - - for (var i = 0; i < _barItems.Children?.Length; i++) - { - if (_barItems.Children [i]?.IsEnabled () == true) - { - _currentChild = i; - - break; - } - } - - SetScheme (_host.GetScheme ()); - CanFocus = true; - WantMousePositionReports = _host.WantMousePositionReports; - } - - BorderStyle = _host.MenusBorderStyle; - - AddCommand ( - Command.Right, - () => - { - _host.NextMenu ( - !_barItems.IsTopLevel - || (_barItems.Children is { Length: > 0 } - && _currentChild > -1 - && _currentChild < _barItems.Children.Length - && _barItems.Children [_currentChild]!.IsFromSubMenu), - _barItems.Children is { Length: > 0 } - && _currentChild > -1 - && _host.UseSubMenusSingleFrame - && _barItems.SubMenu ( - _barItems.Children [_currentChild]! - ) - != null! - ); - - return true; - } - ); - - AddKeyBindingsHotKey (_barItems); - } - - public override Point? PositionCursor () - { - if (_host.IsMenuOpen) - { - if (_barItems!.IsTopLevel) - { - return _host.PositionCursor (); - } - - Move (2, 1 + _currentChild); - - return null; // Don't show the cursor - } - - return _host.PositionCursor (); - } - - public void Run (Action? action) - { - if (action is null) - { - return; - } - - Application.Mouse.UngrabMouse (); - _host.CloseAllMenus (); - Application.LayoutAndDraw (true); - - _host.Run (action); - } - - protected override void Dispose (bool disposing) - { - RemoveKeyBindingsHotKey (_barItems); - - if (Application.TopRunnable is { }) - { - Application.TopRunnable.DrawComplete -= Top_DrawComplete; - Application.TopRunnable.SizeChanging -= Current_TerminalResized; - } - - Application.MouseEvent -= Application_RootMouseEvent; - Application.Mouse.UnGrabbedMouse -= Application_UnGrabbedMouse; - base.Dispose (disposing); - } - - protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? view) - { - if (!newHasFocus) - { - _host.LostFocus (previousFocusedView!); - } - } - - /// - protected override bool OnKeyDownNotHandled (Key keyEvent) - { - // We didn't handle the key, pass it on to host - return _host.InvokeCommandsBoundToHotKey (keyEvent) is true; - } - - protected override bool OnMouseEvent (MouseEventArgs me) - { - if (!_host._handled && !_host.HandleGrabView (me, this)) - { - return false; - } - - _host._handled = false; - bool disabled; - - if (me.Flags == MouseFlags.Button1Clicked) - { - disabled = false; - - if (me.Position.Y < 0) - { - return me.Handled = true; - } - - if (me.Position.Y >= _barItems!.Children!.Length) - { - return me.Handled = true; - } - - MenuItem item = _barItems.Children [me.Position.Y]!; - - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (item is null || !item.IsEnabled ()) - { - disabled = true; - } - - if (disabled) - { - return me.Handled = true; - } - - _currentChild = me.Position.Y; - RunSelected (); - - return me.Handled = true; - } - - if (me.Flags != MouseFlags.Button1Pressed - && me.Flags != MouseFlags.Button1DoubleClicked - && me.Flags != MouseFlags.Button1TripleClicked - && me.Flags != MouseFlags.ReportMousePosition - && !me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) - { - return false; - } - - { - disabled = false; - - if (me.Position.Y < 0 || me.Position.Y >= _barItems!.Children!.Length) - { - return me.Handled = true; - } - - MenuItem item = _barItems.Children [me.Position.Y]!; - - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (item is null) - { - return me.Handled = true; - } - - if (item.IsEnabled () != true) - { - disabled = true; - } - - if (!disabled) - { - _currentChild = me.Position.Y; - } - - if (_host.UseSubMenusSingleFrame || !CheckSubMenu ()) - { - SetNeedsDraw (); - SetParentSetNeedsDisplay (); - - return me.Handled = true; - } - - _host.OnMenuOpened (); - - return me.Handled = true; - } - } - - /// - protected override void OnVisibleChanged () - { - base.OnVisibleChanged (); - - if (Visible) - { - Application.MouseEvent += Application_RootMouseEvent; - } - else - { - Application.MouseEvent -= Application_RootMouseEvent; - } - } - - internal required MenuBarItem? BarItems - { - get => _barItems!; - init - { - ArgumentNullException.ThrowIfNull (value); - _barItems = value; - - // Debugging aid so ToString() is helpful - Text = _barItems.Title; - } - } - - internal bool CheckSubMenu () - { - if (_currentChild == -1 || _barItems?.Children? [_currentChild] is null) - { - return true; - } - - MenuBarItem? subMenu = _barItems.SubMenu (_barItems.Children [_currentChild]!); - - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (subMenu is { }) - { - int pos = -1; - - if (_host._openSubMenu is { }) - { - pos = _host._openSubMenu.FindIndex (o => o._barItems == subMenu); - } - - if (pos == -1 - && this != _host.OpenCurrentMenu - && subMenu.Children != _host.OpenCurrentMenu!._barItems!.Children - && !_host.CloseMenu (false, true)) - { - return false; - } - - _host.Activate (_host._selected, pos, subMenu); - } - else if (_host._openSubMenu?.Count == 0 || _host._openSubMenu?.Last ()._barItems!.IsSubMenuOf (_barItems.Children [_currentChild]!) == false) - { - return _host.CloseMenu (false, true); - } - else - { - SetNeedsDraw (); - SetParentSetNeedsDisplay (); - } - - return true; - } - - internal Attribute DetermineSchemeFor (MenuItem? item, int index) - { - if (item is null) - { - return GetAttributeForRole (VisualRole.Normal); - } - - if (index == _currentChild) - { - return GetAttributeForRole (VisualRole.Focus); - } - - return !item.IsEnabled () ? GetAttributeForRole (VisualRole.Disabled) : GetAttributeForRole (VisualRole.Normal); - } - - internal required MenuBar Host - { - get => _host; - init - { - ArgumentNullException.ThrowIfNull (value); - _host = value; - } - } - - internal static Rectangle MakeFrame (int x, int y, MenuItem? []? items, Menu? parent = null) - { - if (items is null || items.Length == 0) - { - return Rectangle.Empty; - } - - int minX = x; - int minY = y; - const int borderOffset = 2; // This 2 is for the space around - int maxW = (items.Max (z => z?.Width) ?? 0) + borderOffset; - int maxH = items.Length + borderOffset; - - if (parent is { } && x + maxW > Application.Screen.Width) - { - minX = Math.Max (parent.Frame.Right - parent.Frame.Width - maxW, 0); - } - - if (y + maxH > Application.Screen.Height) - { - minY = Math.Max (Application.Screen.Height - maxH, 0); - } - - return new (minX, minY, maxW, maxH); - } - - internal Menu? Parent { get; init; } - - private void AddKeyBindingsHotKey (MenuBarItem? menuBarItem) - { - if (menuBarItem is null || menuBarItem.Children is null) - { - return; - } - - IEnumerable menuItems = menuBarItem.Children.Where (m => m is { })!; - - foreach (MenuItem menuItem in menuItems) - { - KeyBinding keyBinding = new ([Command.Toggle], this, data: menuItem); - - if (menuItem.HotKey != Key.Empty) - { - HotKeyBindings.Remove (menuItem.HotKey!); - HotKeyBindings.Add (menuItem.HotKey!, keyBinding); - HotKeyBindings.Remove (menuItem.HotKey!.WithAlt); - HotKeyBindings.Add (menuItem.HotKey.WithAlt, keyBinding); - } - } - } - - private void Application_RootMouseEvent (object? sender, MouseEventArgs a) - { - if (a.View is { } and (MenuBar or not Menu)) - { - return; - } - - if (!Visible) - { - throw new InvalidOperationException ("This shouldn't running on a invisible menu!"); - } - - View view = a.View ?? this; - - Point boundsPoint = view.ScreenToViewport (new (a.Position.X, a.Position.Y)); - - var me = new MouseEventArgs - { - Position = boundsPoint, - Flags = a.Flags, - ScreenPosition = a.Position, - View = view - }; - - if (view.NewMouseEvent (me) == true || a.Flags == MouseFlags.Button1Pressed || a.Flags == MouseFlags.Button1Released) - { - a.Handled = true; - } - } - - private void Application_UnGrabbedMouse (object? sender, ViewEventArgs a) - { - if (_host is { IsMenuOpen: true }) - { - _host.CloseAllMenus (); - } - } - - private void CloseAllMenus () - { - Application.Mouse.UngrabMouse (); - _host.CloseAllMenus (); - } - - private void Current_TerminalResized (object? sender, SizeChangedEventArgs e) - { - if (_host.IsMenuOpen) - { - _host.CloseAllMenus (); - } - } - - /// Called when a key bound to Command.ToggleExpandCollapse is pressed. This means a hot key was pressed. - /// - private bool ExpandCollapse (MenuItem? menuItem) - { - if (!IsInitialized || !Visible) - { - return true; - } - - for (var c = 0; c < _barItems!.Children!.Length; c++) - { - if (_barItems.Children [c] == menuItem) - { - _currentChild = c; - - break; - } - } - - if (menuItem is { }) - { - var m = menuItem as MenuBarItem; - - if (m?.Children?.Length > 0) - { - MenuItem? item = _barItems.Children [_currentChild]; - - if (item is null) - { - return true; - } - - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - bool disabled = item is null || !item.IsEnabled (); - - if (!disabled && (_host.UseSubMenusSingleFrame || !CheckSubMenu ())) - { - SetNeedsDraw (); - SetParentSetNeedsDisplay (); - - return true; - } - - if (!disabled) - { - _host.OnMenuOpened (); - } - } - else - { - _host.SelectItem (menuItem); - } - } - else if (_host.IsMenuOpen) - { - _host.CloseAllMenus (); - } - else - { - _host.OpenMenu (); - } - - return true; - } - - private bool MoveDown () - { - if (_barItems!.IsTopLevel) - { - return true; - } - - bool disabled; - - do - { - _currentChild++; - - if (_currentChild >= _barItems?.Children?.Length) - { - _currentChild = 0; - } - - if (this != _host.OpenCurrentMenu && _barItems?.Children? [_currentChild]?.IsFromSubMenu == true && _host._selectedSub > -1) - { - _host.PreviousMenu (true); - _host.SelectEnabledItem (_barItems.Children!, _currentChild, out _currentChild); - _host.OpenCurrentMenu = this; - } - - MenuItem? item = _barItems?.Children? [_currentChild]; - - if (item?.IsEnabled () != true) - { - disabled = true; - } - else - { - disabled = false; - } - - if (_host is { UseSubMenusSingleFrame: false, UseKeysUpDownAsKeysLeftRight: true } - && _barItems?.SubMenu (_barItems?.Children? [_currentChild]!) != null - && !disabled - && _host.IsMenuOpen) - { - if (!CheckSubMenu ()) - { - return false; - } - - break; - } - - if (!_host.IsMenuOpen) - { - _host.OpenMenu (_host._selected); - } - } - while (_barItems?.Children? [_currentChild] is null || disabled); - - SetNeedsDraw (); - SetParentSetNeedsDisplay (); - - if (!_host.UseSubMenusSingleFrame) - { - _host.OnMenuOpened (); - } - - return true; - } - - private bool MoveUp () - { - if (_barItems!.IsTopLevel || _currentChild == -1) - { - return true; - } - - bool disabled; - - do - { - _currentChild--; - - if (_host.UseKeysUpDownAsKeysLeftRight && !_host.UseSubMenusSingleFrame) - { - if ((_currentChild == -1 || this != _host.OpenCurrentMenu) - && _barItems.Children! [_currentChild + 1]!.IsFromSubMenu - && _host._selectedSub > -1) - { - _currentChild++; - _host.PreviousMenu (true); - - if (_currentChild > 0) - { - _currentChild--; - _host.OpenCurrentMenu = this; - } - - break; - } - } - - if (_currentChild < 0) - { - _currentChild = _barItems.Children!.Length - 1; - } - - if (!_host.SelectEnabledItem (_barItems.Children!, _currentChild, out _currentChild, false)) - { - _currentChild = 0; - - if (!_host.SelectEnabledItem (_barItems.Children!, _currentChild, out _currentChild) && !_host.CloseMenu ()) - { - return false; - } - - break; - } - - MenuItem item = _barItems.Children! [_currentChild]!; - disabled = item.IsEnabled () != true; - - if (_host.UseSubMenusSingleFrame - || !_host.UseKeysUpDownAsKeysLeftRight - || _barItems.SubMenu (_barItems.Children [_currentChild]!) == null! - || disabled - || !_host.IsMenuOpen) - { - continue; - } - - if (!CheckSubMenu ()) - { - return false; - } - - break; - } - while (_barItems.Children [_currentChild] is null || disabled); - - SetNeedsDraw (); - SetParentSetNeedsDisplay (); - - if (!_host.UseSubMenusSingleFrame) - { - _host.OnMenuOpened (); - } - - return true; - } - - private void RemoveKeyBindingsHotKey (MenuBarItem? menuBarItem) - { - if (menuBarItem is null || menuBarItem.Children is null) - { - return; - } - - IEnumerable menuItems = menuBarItem.Children.Where (m => m is { })!; - - foreach (MenuItem menuItem in menuItems) - { - if (menuItem.HotKey != Key.Empty) - { - KeyBindings.Remove (menuItem.HotKey!); - KeyBindings.Remove (menuItem.HotKey!.WithAlt); - } - } - } - - private void RunSelected () - { - if (_barItems!.IsTopLevel) - { - Run (_barItems.Action); - } - else - { - switch (_currentChild) - { - case > -1 when _barItems.Children! [_currentChild]!.Action != null!: - Run (_barItems.Children [_currentChild]?.Action); - - break; - case 0 when _host.UseSubMenusSingleFrame && _barItems.Children [_currentChild]?.Parent!.Parent != null: - _host.PreviousMenu (_barItems.Children [_currentChild]!.Parent!.IsFromSubMenu, true); - - break; - case > -1 when _barItems.SubMenu (_barItems.Children [_currentChild]) != null!: - CheckSubMenu (); - - break; - } - } - } - - private void SetParentSetNeedsDisplay () - { - if (_host._openSubMenu is { }) - { - foreach (Menu menu in _host._openSubMenu) - { - menu.SetNeedsDraw (); - } - } - - _host._openMenu?.SetNeedsDraw (); - _host.SetNeedsDraw (); - } - - // By doing this we draw last, over everything else. - private void Top_DrawComplete (object? sender, DrawEventArgs e) - { - if (!Visible) - { - return; - } - - if (_barItems!.Children is null) - { - return; - } - - DrawAdornments (); - RenderLineCanvas (); - - // BUGBUG: Views should not change the clip. Doing so is an indcation of poor design or a bug in the framework. - Region? savedClip = SetClipToScreen (); - - SetAttribute (GetAttributeForRole (VisualRole.Normal)); - - for (int i = Viewport.Y; i < _barItems!.Children.Length; i++) - { - if (i < 0) - { - continue; - } - - if (ViewportToScreen (Viewport).Y + i >= Application.Screen.Height) - { - break; - } - - MenuItem? item = _barItems.Children [i]; - - SetAttribute ( - - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - item is null ? GetAttributeForRole (VisualRole.Normal) : - i == _currentChild ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal) - ); - - if (item is null && BorderStyle != LineStyle.None) - { - Move (-1, i); - AddRune (Glyphs.LeftTee); - } - else if (Frame.X < Application.Screen.Width) - { - Move (0, i); - } - - SetAttribute (DetermineSchemeFor (item, i)); - - for (int p = Viewport.X; p < Frame.Width - 2; p++) - { - // This - 2 is for the border - if (p < 0) - { - continue; - } - - if (ViewportToScreen (Viewport).X + p >= Application.Screen.Width) - { - break; - } - - if (item is null) - { - AddRune (Glyphs.HLine); - } - else if (i == 0 && p == 0 && _host.UseSubMenusSingleFrame && item.Parent!.Parent is { }) - { - AddRune (Glyphs.LeftArrow); - } - - // This `- 3` is left border + right border + one row in from right - else if (p == Frame.Width - 3 && _barItems?.SubMenu (_barItems.Children [i]!) is { }) - { - AddRune (Glyphs.RightArrow); - } - else - { - AddRune ((Rune)' '); - } - } - - if (item is null) - { - if (BorderStyle != LineStyle.None && SuperView?.Frame.Right - Frame.X > Frame.Width) - { - Move (Frame.Width - 2, i); - AddRune (Glyphs.RightTee); - } - - continue; - } - - string? textToDraw; - Rune nullCheckedChar = Glyphs.CheckStateNone; - Rune checkChar = Glyphs.Selected; - Rune uncheckedChar = Glyphs.UnSelected; - - if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked)) - { - checkChar = Glyphs.CheckStateChecked; - uncheckedChar = Glyphs.CheckStateUnChecked; - } - - // Support Checked even though CheckType wasn't set - if (item is { CheckType: MenuItemCheckStyle.Checked, Checked: null }) - { - textToDraw = $"{nullCheckedChar} {item.Title}"; - } - else if (item.Checked == true) - { - textToDraw = $"{checkChar} {item.Title}"; - } - else if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked) || item.CheckType.HasFlag (MenuItemCheckStyle.Radio)) - { - textToDraw = $"{uncheckedChar} {item.Title}"; - } - else - { - textToDraw = item.Title; - } - - Point screen = ViewportToScreen (new Point (0, i)); - - if (screen.X < Application.Screen.Width) - { - Move (1, i); - - if (!item.IsEnabled ()) - { - DrawHotString (textToDraw, GetAttributeForRole (VisualRole.Disabled), GetAttributeForRole (VisualRole.Disabled)); - } - else if (i == 0 && _host.UseSubMenusSingleFrame && item.Parent!.Parent is { }) - { - var tf = new TextFormatter - { - ConstrainToWidth = Frame.Width - 3, - ConstrainToHeight = 1, - Alignment = Alignment.Center, HotKeySpecifier = MenuBar.HotKeySpecifier, Text = textToDraw - }; - - // The -3 is left/right border + one space (not sure what for) - tf.Draw ( - driver: Driver, - screen: ViewportToScreen (new Rectangle (1, i, Frame.Width - 3, 1)), - normalColor: i == _currentChild ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal), - hotColor: i == _currentChild ? GetAttributeForRole (VisualRole.HotFocus) : GetAttributeForRole (VisualRole.HotNormal), - maximum: SuperView?.ViewportToScreen (SuperView.Viewport) ?? Rectangle.Empty); - } - else - { - DrawHotString ( - textToDraw, - i == _currentChild ? GetAttributeForRole (VisualRole.HotFocus) : GetAttributeForRole (VisualRole.HotNormal), - i == _currentChild ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal) - ); - } - - // The help string - int l = item.ShortcutTag.GetColumns () == 0 - ? item.Help.GetColumns () - : item.Help.GetColumns () + item.ShortcutTag.GetColumns () + 2; - int col = Frame.Width - l - 3; - screen = ViewportToScreen (new Point (col, i)); - - if (screen.X < Application.Screen.Width) - { - Move (col, i); - AddStr (item.Help); - - // The shortcut tag string - if (!string.IsNullOrEmpty (item.ShortcutTag)) - { - Move (col + l - item.ShortcutTag.GetColumns (), i); - AddStr (item.ShortcutTag); - } - } - } - } - - SetClip (savedClip); - } -} diff --git a/Terminal.Gui/Views/Menuv1/MenuBar.cs b/Terminal.Gui/Views/Menuv1/MenuBar.cs deleted file mode 100644 index 2c09e6c56..000000000 --- a/Terminal.Gui/Views/Menuv1/MenuBar.cs +++ /dev/null @@ -1,1854 +0,0 @@ - - -namespace Terminal.Gui.Views; - -/// -/// Provides a menu bar that spans the top of a View with drop-down and cascading menus. -/// -/// By default, any sub-sub-menus (sub-menus of the s added to s) -/// are displayed in a cascading manner, where each sub-sub-menu pops out of the sub-menu frame (either to the -/// right or left, depending on where the sub-menu is relative to the edge of the screen). By setting -/// to , this behavior can be changed such that all -/// sub-sub-menus are drawn within a single frame below the MenuBar. -/// -/// -/// -/// -/// The appears on the first row of the SuperView and uses the full -/// width. -/// -/// The provides global hot keys for the application. See . -/// -/// When the menu is created key bindings for each menu item and its sub-menu items are added for each menu -/// item's hot key (both alone AND with AltMask) and shortcut, if defined. -/// -/// -/// If a key press matches any of the menu item's hot keys or shortcuts, the menu item's action is invoked or -/// sub-menu opened. -/// -/// -/// * If the menu bar is not open * Any shortcut defined within the menu will be invoked * Only hot keys defined -/// for the menu bar items will be invoked, and only if Alt is pressed too. * If the menu bar is open * Un-shifted -/// hot keys defined for the menu bar items will be invoked, only if the menu they belong to is open (the menu bar -/// item's text is visible). * Alt-shifted hot keys defined for the menu bar items will be invoked, only if the -/// menu they belong to is open (the menu bar item's text is visible). * If there is a visible hot key that -/// duplicates a shortcut (e.g. _File and Alt-F), the hot key wins. -/// -/// -[Obsolete ("Use MenuBarv2 instead.", false)] -public class MenuBar : View, IDesignable -{ - // Spaces before the Title - private static readonly int _leftPadding = 1; - - // Spaces after the submenu Title, before Help - private static readonly int _parensAroundHelp = 3; - - // Spaces after the Title - private static readonly int _rightPadding = 1; - - // The column where the MenuBar starts - private static readonly int _xOrigin = 0; - internal bool _isMenuClosing; - internal bool _isMenuOpening; - - internal Menu? _openMenu; - internal List? _openSubMenu; - internal int _selected; - internal int _selectedSub; - - private bool _initialCanFocus; - private bool _isCleaning; - private View? _lastFocused; - private Menu? _ocm; - private View? _previousFocused; - private bool _reopen; - private bool _useSubMenusSingleFrame; - - /// Initializes a new instance of the . - public MenuBar () - { - TabStop = TabBehavior.NoStop; - X = 0; - Y = 0; - Width = Dim.Fill (); - Height = 1; // BUGBUG: Views should avoid setting Height as doing so implies Frame.Size == GetContentSize (). - Menus = []; - - //CanFocus = true; - _selected = -1; - _selectedSub = -1; - SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Menu); - WantMousePositionReports = true; - IsMenuOpen = false; - - SuperViewChanged += MenuBar_SuperViewChanged; - - // Things this view knows how to do - AddCommand ( - Command.Left, - () => - { - MoveLeft (); - - return true; - } - ); - - AddCommand ( - Command.Right, - () => - { - MoveRight (); - - return true; - } - ); - - AddCommand ( - Command.Cancel, - () => - { - if (IsMenuOpen) - { - CloseMenuBar (); - - return true; - } - - return false; - } - ); - - AddCommand ( - Command.Accept, - (ctx) => - { - if (Menus.Length > 0) - { - ProcessMenu (_selected, Menus [_selected]); - } - - return RaiseAccepting (ctx); - } - ); - AddCommand (Command.Toggle, ctx => - { - CloseOtherOpenedMenuBar (); - - if (ctx is not CommandContext keyCommandContext) - { - return false; - } - return Select (Menus.IndexOf (keyCommandContext.Binding.Data)); - }); - AddCommand (Command.Select, ctx => - { - if (ctx is not CommandContext keyCommandContext) - { - return false; - } - if (keyCommandContext.Binding.Data is MouseEventArgs) - { - // HACK: Work around the fact that View.MouseClick always invokes Select - return false; - } - var res = Run ((keyCommandContext.Binding.Data as MenuItem)?.Action!); - CloseAllMenus (); - - return res; - }); - - // Default key bindings for this view - KeyBindings.Add (Key.CursorLeft, Command.Left); - KeyBindings.Add (Key.CursorRight, Command.Right); - KeyBindings.Add (Key.Esc, Command.Cancel); - KeyBindings.Add (Key.CursorDown, Command.Accept); - - KeyBinding keyBinding = new ([Command.Toggle], this, data: -1); // -1 indicates Key was used - HotKeyBindings.Add (Key, keyBinding); - - // TODO: Why do we have two keybindings for opening the menu? Ctrl-Space and Key? - HotKeyBindings.Add (Key.Space.WithCtrl, keyBinding); - // This is needed for macOS because Key.Space.WithCtrl doesn't work - HotKeyBindings.Add (Key.Space.WithAlt, keyBinding); - - // TODO: Figure out how to make Alt work (on Windows) - //KeyBindings.Add (Key.WithAlt, keyBinding); - } - - /// if the menu is open; otherwise . - public bool IsMenuOpen { get; protected set; } - - /// Gets the view that was last focused before opening the menu. - public View? LastFocused { get; private set; } - - /// - /// Gets or sets the array of s for the menu. Only set this after the - /// is visible. - /// - /// The menu array. - public MenuBarItem [] Menus - { - get => _menus; - set - { - _menus = value; - - if (Menus is []) - { - return; - } - - // TODO: Hotkeys should not work for sub-menus if they are not visible! - for (var i = 0; i < Menus.Length; i++) - { - MenuBarItem menuBarItem = Menus [i]; - - if (menuBarItem.HotKey != Key.Empty) - { - HotKeyBindings.Remove (menuBarItem.HotKey!); - KeyBinding keyBinding = new ([Command.Toggle], this, menuBarItem); - HotKeyBindings.Add (menuBarItem.HotKey!, keyBinding); - HotKeyBindings.Remove (menuBarItem.HotKey!.WithAlt); - keyBinding = new ([Command.Toggle], this, data: menuBarItem); - HotKeyBindings.Add (menuBarItem.HotKey.WithAlt, keyBinding); - } - - if (menuBarItem.ShortcutKey != Key.Empty) - { - // Technically this will never run because MenuBarItems don't have shortcuts - // unless the IsTopLevel is true - HotKeyBindings.Remove (menuBarItem.ShortcutKey!); - KeyBinding keyBinding = new ([Command.Select], this, data: menuBarItem); - HotKeyBindings.Add (menuBarItem.ShortcutKey!, keyBinding); - } - - menuBarItem.AddShortcutKeyBindings (this); - } - } - } - - /// - /// The default for 's border. The default is - /// . - /// - public LineStyle MenusBorderStyle { get; set; } = LineStyle.Single; - - /// - /// Gets or sets if the sub-menus must be displayed in a single or multiple frames. - /// - /// By default, any sub-sub-menus (sub-menus of the main s) are displayed in a cascading - /// manner, where each sub-sub-menu pops out of the sub-menu frame (either to the right or left, depending on where - /// the sub-menu is relative to the edge of the screen). By setting to - /// , this behavior can be changed such that all sub-sub-menus are drawn within a single - /// frame below the MenuBar. - /// - /// - public bool UseSubMenusSingleFrame - { - get => _useSubMenusSingleFrame; - set - { - _useSubMenusSingleFrame = value; - - if (value && UseKeysUpDownAsKeysLeftRight) - { - _useKeysUpDownAsKeysLeftRight = false; - SetNeedsDraw (); - } - } - } - - /// - public override bool Visible - { - get => base.Visible; - set - { - base.Visible = value; - - if (!value) - { - CloseAllMenus (); - } - } - } - - internal Menu? OpenCurrentMenu - { - get => _ocm; - set - { - if (_ocm != value) - { - _ocm = value!; - - if (_ocm is { _currentChild: > -1 }) - { - OnMenuOpened (); - } - } - } - } - - /// Closes the Menu programmatically if open and not canceled (as though F9 were pressed). - public bool CloseMenu (bool ignoreUseSubMenusSingleFrame = false) { return CloseMenu (false, false, ignoreUseSubMenusSingleFrame); } - - /// Raised when all the menu is closed. - public event EventHandler? MenuAllClosed; - - /// Raised when a menu is closing passing . - public event EventHandler? MenuClosing; - - /// Raised when a menu is opened. - public event EventHandler? MenuOpened; - - /// Raised as a menu is opening. - public event EventHandler? MenuOpening; - - /// - protected override bool OnDrawingContent () - { - var pos = 0; - - for (var i = 0; i < Menus.Length; i++) - { - MenuBarItem menu = Menus [i]; - Move (pos, 0); - Attribute hotColor, normalColor; - - if (i == _selected && IsMenuOpen) - { - hotColor = i == _selected ? GetAttributeForRole (VisualRole.HotFocus) : GetAttributeForRole (VisualRole.HotNormal); - normalColor = i == _selected ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal); - } - else - { - hotColor = GetAttributeForRole (VisualRole.HotNormal); - normalColor = GetAttributeForRole (VisualRole.Normal); - } - - // Note Help on MenuBar is drawn with parens around it - DrawHotString ( - string.IsNullOrEmpty (menu.Help) ? $" {menu.Title} " : $" {menu.Title} ({menu.Help}) ", - hotColor, - normalColor - ); - - pos += _leftPadding - + menu.TitleLength - + (menu.Help.GetColumns () > 0 - ? _leftPadding + menu.Help.GetColumns () + _parensAroundHelp - : 0) - + _rightPadding; - } - - //PositionCursor (); - return true; - } - - /// Virtual method that will invoke the . - public virtual void OnMenuAllClosed () { MenuAllClosed?.Invoke (this, EventArgs.Empty); } - - /// Virtual method that will invoke the . - /// The current menu to be closed. - /// Whether the current menu will be reopened. - /// Whether is a sub-menu or not. - public virtual MenuClosingEventArgs OnMenuClosing (MenuBarItem currentMenu, bool reopen, bool isSubMenu) - { - var ev = new MenuClosingEventArgs (currentMenu, reopen, isSubMenu); - MenuClosing?.Invoke (this, ev); - - return ev; - } - - /// Virtual method that will invoke the event if it's defined. - public virtual void OnMenuOpened () - { - MenuItem? mi = null; - MenuBarItem? parent; - - if (OpenCurrentMenu?.BarItems?.Children is { Length: > 0 } - && OpenCurrentMenu?._currentChild > -1) - { - parent = OpenCurrentMenu.BarItems; - mi = parent.Children [OpenCurrentMenu._currentChild]; - } - else if (OpenCurrentMenu!.BarItems!.IsTopLevel) - { - parent = null; - mi = OpenCurrentMenu.BarItems; - } - else - { - parent = _openMenu?.BarItems; - - if (OpenCurrentMenu?._currentChild > -1) - { - mi = parent?.Children?.Length > 0 ? parent.Children [_openMenu!._currentChild] : null; - } - } - - MenuOpened?.Invoke (this, new (parent, mi)); - } - - /// Virtual method that will invoke the event if it's defined. - /// The current menu to be replaced. - /// Returns the - public virtual MenuOpeningEventArgs OnMenuOpening (MenuBarItem currentMenu) - { - var ev = new MenuOpeningEventArgs (currentMenu); - MenuOpening?.Invoke (this, ev); - - return ev; - } - - /// Opens the Menu programatically, as though the F9 key were pressed. - public void OpenMenu () - { - MenuBar? mbar = GetMouseGrabViewInstance (this); - - mbar?.CleanUp (); - - CloseOtherOpenedMenuBar (); - - if (!Enabled || _openMenu is { }) - { - return; - } - - _selected = 0; - SetNeedsDraw (); - - _previousFocused = (SuperView is null ? Application.TopRunnable?.Focused : SuperView.Focused)!; - OpenMenu (_selected); - - if (!SelectEnabledItem ( - OpenCurrentMenu?.BarItems?.Children, - OpenCurrentMenu!._currentChild, - out OpenCurrentMenu._currentChild - ) - && !CloseMenu ()) - { - return; - } - - if (!OpenCurrentMenu.CheckSubMenu ()) - { - return; - } - - if (_isContextMenuLoading) - { - Application.Mouse.GrabMouse (_openMenu); - _isContextMenuLoading = false; - } - else - { - Application.Mouse.GrabMouse (this); - } - } - - /// - public override Point? PositionCursor () - { - if (_selected == -1 && HasFocus && Menus.Length > 0) - { - _selected = 0; - } - - var pos = 0; - - for (var i = 0; i < Menus.Length; i++) - { - if (i == _selected) - { - pos++; - Move (pos + 1, 0); - - return null; // Don't show the cursor - } - - pos += _leftPadding - + Menus [i].TitleLength - + (Menus [i].Help.GetColumns () > 0 - ? Menus [i].Help.GetColumns () + _parensAroundHelp - : 0) - + _rightPadding; - } - - return null; // Don't show the cursor - } - - // Activates the menu, handles either first focus, or activating an entry when it was already active - // For mouse events. - internal void Activate (int idx, int sIdx = -1, MenuBarItem? subMenu = null!) - { - _selected = idx; - _selectedSub = sIdx; - - if (_openMenu is null) - { - _previousFocused = (SuperView is null ? Application.TopRunnable?.Focused ?? null : SuperView.Focused)!; - } - - OpenMenu (idx, sIdx, subMenu); - SetNeedsDraw (); - } - - internal void CleanUp () - { - if (_isCleaning) - { - return; - } - - _isCleaning = true; - - if (_openMenu is { }) - { - CloseAllMenus (); - } - - _openedByAltKey = false; - IsMenuOpen = false; - _selected = -1; - CanFocus = _initialCanFocus; - - if (_lastFocused is { }) - { - _lastFocused.SetFocus (); - } - - SetNeedsDraw (); - - if (Application.Mouse.MouseGrabView is { } && Application.Mouse.MouseGrabView is MenuBar && Application.Mouse.MouseGrabView != this) - { - var menuBar = Application.Mouse.MouseGrabView as MenuBar; - - if (menuBar!.IsMenuOpen) - { - menuBar.CleanUp (); - } - } - Application.Mouse.UngrabMouse (); - _isCleaning = false; - } - - internal void CloseAllMenus () - { - if (!_isMenuOpening && !_isMenuClosing) - { - if (_openSubMenu is { } && !CloseMenu (false, true, true)) - { - return; - } - - if (!CloseMenu ()) - { - return; - } - - if (LastFocused is { } && LastFocused != this) - { - _selected = -1; - } - - Application.Mouse.UngrabMouse (); - } - - if (OpenCurrentMenu is { }) - { - OpenCurrentMenu = null; - } - - IsMenuOpen = false; - _openedByAltKey = false; - OnMenuAllClosed (); - - CloseOtherOpenedMenuBar (); - } - - private void CloseOtherOpenedMenuBar () - { - if (SuperView is { }) - { - // Close others menu bar opened - Menu? menu = SuperView.SubViews.FirstOrDefault (v => v is Menu m && m.Host != this && m.Host.IsMenuOpen) as Menu; - menu?.Host.CleanUp (); - } - } - - internal bool CloseMenu (bool reopen, bool isSubMenu, bool ignoreUseSubMenusSingleFrame = false) - { - MenuBarItem? mbi = isSubMenu ? OpenCurrentMenu!.BarItems : _openMenu?.BarItems; - - if (UseSubMenusSingleFrame && mbi is { } && !ignoreUseSubMenusSingleFrame && mbi.Parent is { }) - { - return false; - } - - _isMenuClosing = true; - _reopen = reopen; - MenuClosingEventArgs args = OnMenuClosing (mbi!, reopen, isSubMenu); - - if (args.Cancel) - { - _isMenuClosing = false; - - if (args.CurrentMenu.Parent is { } && _openMenu is { }) - { - _openMenu._currentChild = - ((MenuBarItem)args.CurrentMenu.Parent).Children.IndexOf (args.CurrentMenu); - } - - return false; - } - - switch (isSubMenu) - { - case false: - if (_openMenu is { }) - { - SuperView?.Remove (_openMenu); - } - - SetNeedsDraw (); - - if (_previousFocused is Menu && _openMenu is { } && _previousFocused.ToString () != OpenCurrentMenu!.ToString ()) - { - _previousFocused.SetFocus (); - } - - if (Application.Mouse.MouseGrabView == _openMenu) - { - Application.Mouse.UngrabMouse (); - } - _openMenu?.Dispose (); - _openMenu = null; - - if (_lastFocused is Menu or MenuBar) - { - _lastFocused = null; - } - - LastFocused = _lastFocused; - _lastFocused = null; - - if (LastFocused is { CanFocus: true }) - { - if (!reopen) - { - _selected = -1; - } - - if (_openSubMenu is { }) - { - _openSubMenu = null; - } - - if (OpenCurrentMenu is { }) - { - SuperView?.Remove (OpenCurrentMenu); - if (Application.Mouse.MouseGrabView == OpenCurrentMenu) - { - Application.Mouse.UngrabMouse (); - } - OpenCurrentMenu.Dispose (); - OpenCurrentMenu = null; - } - - LastFocused.SetFocus (); - } - else if (_openSubMenu is null || _openSubMenu.Count == 0) - { - CloseAllMenus (); - } - else - { - SetFocus (); - } - - IsMenuOpen = false; - - break; - - case true: - _selectedSub = -1; - SetNeedsDraw (); - RemoveAllOpensSubMenus (); - OpenCurrentMenu!._previousSubFocused!.SetFocus (); - _openSubMenu = null; - IsMenuOpen = true; - - break; - } - - _reopen = false; - _isMenuClosing = false; - - return true; - } - - /// Gets the superview location offset relative to the location. - /// The location offset. - internal Point GetScreenOffset () - { - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (Application.Screen.Height == 0) - { - return Point.Empty; - } - - Rectangle superViewFrame = SuperView?.Frame ?? Application.Screen; - View? sv = SuperView ?? Application.TopRunnable; - - if (sv is null) - { - // Support Unit Tests - return Point.Empty; - } - - Point viewportOffset = sv.GetViewportOffsetFromFrame (); - - return new ( - superViewFrame.X - sv.Frame.X - viewportOffset.X, - superViewFrame.Y - sv.Frame.Y - viewportOffset.Y - ); - } - - internal void NextMenu (bool isSubMenu = false, bool ignoreUseSubMenusSingleFrame = false) - { - switch (isSubMenu) - { - case false: - if (_selected == -1) - { - _selected = 0; - } - else if (_selected + 1 == Menus.Length) - { - _selected = 0; - } - else - { - _selected++; - } - - if (_selected > -1 && !CloseMenu (true, ignoreUseSubMenusSingleFrame)) - { - return; - } - - if (_selected == -1) - { - return; - } - - OpenMenu (_selected); - - SelectEnabledItem ( - OpenCurrentMenu?.BarItems?.Children, - OpenCurrentMenu!._currentChild, - out OpenCurrentMenu._currentChild - ); - - break; - case true: - if (UseKeysUpDownAsKeysLeftRight) - { - if (CloseMenu (false, true, ignoreUseSubMenusSingleFrame)) - { - NextMenu (false, ignoreUseSubMenusSingleFrame); - } - } - else - { - MenuBarItem? subMenu = OpenCurrentMenu!._currentChild > -1 && OpenCurrentMenu.BarItems?.Children!.Length > 0 - ? OpenCurrentMenu.BarItems.SubMenu ( - OpenCurrentMenu.BarItems.Children? [OpenCurrentMenu._currentChild]! - ) - : null; - - if ((_selectedSub == -1 || _openSubMenu is null || _openSubMenu?.Count - 1 == _selectedSub) && subMenu is null) - { - if (_openSubMenu is { } && !CloseMenu (false, true)) - { - return; - } - - NextMenu (false, ignoreUseSubMenusSingleFrame); - } - else if (subMenu != null - || (OpenCurrentMenu._currentChild > -1 - && !OpenCurrentMenu.BarItems! - .Children! [OpenCurrentMenu._currentChild]! - .IsFromSubMenu)) - { - _selectedSub++; - OpenCurrentMenu.CheckSubMenu (); - } - else - { - if (CloseMenu (false, true, ignoreUseSubMenusSingleFrame)) - { - NextMenu (false, ignoreUseSubMenusSingleFrame); - } - - return; - } - - SetNeedsDraw (); - - if (UseKeysUpDownAsKeysLeftRight) - { - OpenCurrentMenu.CheckSubMenu (); - } - } - - break; - } - } - - internal void OpenMenu (int index, int sIndex = -1, MenuBarItem? subMenu = null!) - { - _isMenuOpening = true; - MenuOpeningEventArgs newMenu = OnMenuOpening (Menus [index]); - - if (newMenu.Cancel) - { - _isMenuOpening = false; - - return; - } - - if (newMenu.NewMenuBarItem is { }) - { - Menus [index] = newMenu.NewMenuBarItem; - } - - var pos = 0; - - switch (subMenu) - { - case null: - // Open a submenu below a MenuBar - _lastFocused ??= SuperView is null ? Application.TopRunnable?.MostFocused : SuperView.MostFocused; - - if (_openSubMenu is { } && !CloseMenu (false, true)) - { - return; - } - - if (_openMenu is { }) - { - SuperView?.Remove (_openMenu); - if (Application.Mouse.MouseGrabView == _openMenu) - { - Application.Mouse.UngrabMouse (); - } - _openMenu.Dispose (); - _openMenu = null; - } - - // This positions the submenu horizontally aligned with the first character of the - // text belonging to the menu - for (var i = 0; i < index; i++) - { - pos += Menus [i].TitleLength + (Menus [i].Help.GetColumns () > 0 ? Menus [i].Help.GetColumns () + 2 : 0) + _leftPadding + _rightPadding; - } - - - - - _openMenu = new () - { - Host = this, - X = Frame.X + pos, - Y = Frame.Y + 1, - BarItems = Menus [index], - Parent = null - }; - OpenCurrentMenu = _openMenu; - OpenCurrentMenu._previousSubFocused = _openMenu; - - if (SuperView is { }) - { - SuperView.Add (_openMenu); - // _openMenu.SetRelativeLayout (Application.Screen.Size); - } - else - { - _openMenu.BeginInit (); - _openMenu.EndInit (); - } - - _openMenu.SetFocus (); - - break; - default: - // Opens a submenu next to another submenu (openSubMenu) - if (_openSubMenu is null) - { - _openSubMenu = new (); - } - - if (sIndex > -1) - { - RemoveSubMenu (sIndex); - } - else - { - Menu? last = _openSubMenu.Count > 0 ? _openSubMenu.Last () : _openMenu; - - if (!UseSubMenusSingleFrame) - { - OpenCurrentMenu = new () - { - Host = this, - X = last!.Frame.Left + last.Frame.Width, - Y = last.Frame.Top + last._currentChild + 1, - BarItems = subMenu, - Parent = last - }; - } - else - { - Menu? first = _openSubMenu.Count > 0 ? _openSubMenu.First () : _openMenu; - - // 2 is for the parent and the separator - MenuItem? [] mbi = new MenuItem [2 + subMenu.Children!.Length]; - mbi [0] = new () { Title = subMenu.Title, Parent = subMenu }; - mbi [1] = null; - - for (var j = 0; j < subMenu.Children.Length; j++) - { - mbi [j + 2] = subMenu.Children [j]; - } - - var newSubMenu = new MenuBarItem (mbi!) { Parent = subMenu }; - - OpenCurrentMenu = new () - { - Host = this, X = first!.Frame.Left, Y = first.Frame.Top, BarItems = newSubMenu - }; - last!.Visible = false; - Application.Mouse.GrabMouse (OpenCurrentMenu); - } - - OpenCurrentMenu._previousSubFocused = last._previousSubFocused; - _openSubMenu.Add (OpenCurrentMenu); - SuperView?.Add (OpenCurrentMenu); - - if (!OpenCurrentMenu.IsInitialized) - { - // Supports unit tests - OpenCurrentMenu.BeginInit (); - OpenCurrentMenu.EndInit (); - } - } - - _selectedSub = _openSubMenu.Count - 1; - - if (_selectedSub > -1 - && SelectEnabledItem ( - OpenCurrentMenu!.BarItems!.Children, - OpenCurrentMenu._currentChild, - out OpenCurrentMenu._currentChild - )) - { - OpenCurrentMenu.SetFocus (); - } - - break; - } - - _isMenuOpening = false; - IsMenuOpen = true; - } - - internal void PreviousMenu (bool isSubMenu = false, bool ignoreUseSubMenusSingleFrame = false) - { - switch (isSubMenu) - { - case false: - if (_selected <= 0) - { - _selected = Menus.Length - 1; - } - else - { - _selected--; - } - - if (_selected > -1 && !CloseMenu (true, false, ignoreUseSubMenusSingleFrame)) - { - return; - } - - if (_selected == -1) - { - return; - } - - OpenMenu (_selected); - - if (!SelectEnabledItem ( - OpenCurrentMenu?.BarItems?.Children, - OpenCurrentMenu!._currentChild, - out OpenCurrentMenu._currentChild, - false - )) - { - OpenCurrentMenu._currentChild = 0; - } - - break; - case true: - if (_selectedSub > -1) - { - _selectedSub--; - RemoveSubMenu (_selectedSub, ignoreUseSubMenusSingleFrame); - SetNeedsDraw (); - } - else - { - PreviousMenu (); - } - - break; - } - } - - internal void RemoveAllOpensSubMenus () - { - if (_openSubMenu is { }) - { - foreach (Menu item in _openSubMenu) - { - SuperView?.Remove (item); - if (Application.Mouse.MouseGrabView == item) - { - Application.Mouse.UngrabMouse (); - } - item.Dispose (); - } - } - } - - internal bool Run (Action? action) - { - if (action is null) - { - return false; - } - - Application.AddTimeout (TimeSpan.Zero, - () => - { - action (); - - return false; - } - ); - - return true; - } - - internal bool SelectEnabledItem ( - MenuItem? []? children, - int current, - out int newCurrent, - bool forward = true - ) - { - if (children is null) - { - newCurrent = -1; - - return true; - } - - IEnumerable childMenuItems = forward ? children : children.Reverse (); - - int count; - - IEnumerable menuItems = childMenuItems as MenuItem [] ?? childMenuItems.ToArray (); - - if (forward) - { - count = -1; - } - else - { - count = menuItems.Count (); - } - - foreach (MenuItem? child in menuItems) - { - if (forward) - { - if (++count < current) - { - continue; - } - } - else - { - if (--count > current) - { - continue; - } - } - - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (child is null || !child.IsEnabled ()) - { - if (forward) - { - current++; - } - else - { - current--; - } - } - else - { - newCurrent = current; - - return true; - } - } - - newCurrent = -1; - - return false; - } - - /// Called when an item is selected; Runs the action. - /// - internal bool SelectItem (MenuItem? item) - { - if (item?.Action is null) - { - return false; - } - - Application.Mouse.UngrabMouse (); - CloseAllMenus (); - Application.LayoutAndDraw (true); - _openedByAltKey = true; - - return Run (item.Action); - } - - private void CloseMenuBar () - { - if (!CloseMenu ()) - { - return; - } - - if (_openedByAltKey) - { - _openedByAltKey = false; - LastFocused?.SetFocus (); - } - - SetNeedsDraw (); - } - - private Point GetLocationOffset () - { - if (MenusBorderStyle != LineStyle.None) - { - return new (0, 1); - } - - return new (-2, 0); - } - - private void MenuBar_SuperViewChanged (object? sender, SuperViewChangedEventArgs _) - { - _initialCanFocus = CanFocus; - SuperViewChanged -= MenuBar_SuperViewChanged; - } - - private void MoveLeft () - { - _selected--; - - if (_selected < 0) - { - _selected = Menus.Length - 1; - } - - OpenMenu (_selected); - SetNeedsDraw (); - } - - private void MoveRight () - { - _selected = (_selected + 1) % Menus.Length; - OpenMenu (_selected); - SetNeedsDraw (); - } - - private bool ProcessMenu (int i, MenuBarItem mi) - { - if (_selected < 0 && IsMenuOpen) - { - return false; - } - - if (mi.IsTopLevel) - { - Point screen = ViewportToScreen (new Point (0, i)); - var menu = new Menu { Host = this, X = screen.X, Y = screen.Y, BarItems = mi }; - menu.Run (mi.Action); - if (Application.Mouse.MouseGrabView == menu) - { - Application.Mouse.UngrabMouse (); - } - menu.Dispose (); - } - else - { - Application.Mouse.GrabMouse (this); - _selected = i; - OpenMenu (i); - - if (!SelectEnabledItem ( - OpenCurrentMenu?.BarItems?.Children, - OpenCurrentMenu!._currentChild, - out OpenCurrentMenu._currentChild - ) - && !CloseMenu ()) - { - return true; - } - - if (!OpenCurrentMenu.CheckSubMenu ()) - { - return true; - } - } - - SetNeedsDraw (); - - return true; - } - - private void RemoveSubMenu (int index, bool ignoreUseSubMenusSingleFrame = false) - { - if (_openSubMenu == null - || (UseSubMenusSingleFrame - && !ignoreUseSubMenusSingleFrame - && _openSubMenu.Count == 0)) - { - return; - } - - for (int i = _openSubMenu.Count - 1; i > index; i--) - { - _isMenuClosing = true; - Menu? menu; - - if (_openSubMenu!.Count - 1 > 0) - { - menu = _openSubMenu [i - 1]; - } - else - { - menu = _openMenu; - } - - if (!menu!.Visible) - { - menu.Visible = true; - } - - OpenCurrentMenu = menu; - OpenCurrentMenu.SetFocus (); - - if (_openSubMenu is { }) - { - menu = _openSubMenu [i]; - SuperView!.Remove (menu); - _openSubMenu.Remove (menu); - - if (Application.Mouse.MouseGrabView == menu) - { - Application.Mouse.GrabMouse (this); - } - - menu.Dispose (); - } - - RemoveSubMenu (i, ignoreUseSubMenusSingleFrame); - } - - if (_openSubMenu!.Count > 0) - { - OpenCurrentMenu = _openSubMenu.Last (); - } - - _isMenuClosing = false; - } - - #region Keyboard handling - - private Key _key = Key.F9; - - /// - /// The used to activate or close the menu bar by keyboard. The default is - /// . - /// - /// - /// - /// If the user presses any s defined in the s, the menu - /// bar will be activated and the sub-menu will be opened. - /// - /// will close the menu bar and any open sub-menus. - /// - public Key Key - { - get => _key; - set - { - if (_key == value) - { - return; - } - - HotKeyBindings.Remove (_key); - KeyBinding keyBinding = new ([Command.Toggle], this, data: -1); // -1 indicates Key was used - HotKeyBindings.Add (value, keyBinding); - _key = value; - } - } - - private bool _useKeysUpDownAsKeysLeftRight; - - /// Used for change the navigation key style. - public bool UseKeysUpDownAsKeysLeftRight - { - get => _useKeysUpDownAsKeysLeftRight; - set - { - _useKeysUpDownAsKeysLeftRight = value; - - if (value && UseSubMenusSingleFrame) - { - UseSubMenusSingleFrame = false; - SetNeedsDraw (); - } - } - } - - /// The specifier character for the hot keys. - public new static Rune HotKeySpecifier => (Rune)'_'; - - // TODO: This doesn't actually work. Figure out why. - private bool _openedByAltKey; - - /// - /// Called when a key bound to Command.Select is pressed. Either activates the menu item or runs it, depending on - /// whether it has a sub-menu. If the menu is open, it will close the menu bar. - /// - /// The index of the menu bar item to select. -1 if the selection was via . - /// - private bool Select (int index) - { - if (!IsInitialized || !Visible) - { - return true; - } - - // If the menubar is open and the menu that's open is 'index' then close it. Otherwise activate it. - if (IsMenuOpen) - { - if (index == -1) - { - CloseAllMenus (); - - return true; - } - - // Find the index of the open submenu and close the menu if it matches - for (var i = 0; i < Menus.Length; i++) - { - MenuBarItem open = Menus [i]; - - if (open == OpenCurrentMenu!.BarItems && i == index) - { - CloseAllMenus (); - return true; - } - } - } - - if (index == -1) - { - OpenMenu (); - } - else if (Menus [index].IsTopLevel) - { - Run (Menus [index].Action); - } - else - { - Activate (index); - } - - return true; - } - - #endregion Keyboard handling - - #region Mouse Handling - - internal void LostFocus (View view) - { - if (view is not MenuBar && view is not Menu && !_isCleaning && !_reopen) - { - CleanUp (); - } - } - - /// - protected override bool OnMouseEvent (MouseEventArgs me) - { - if (!_handled && !HandleGrabView (me, this)) - { - return false; - } - - _handled = false; - - if (me.Flags == MouseFlags.Button1Pressed - || me.Flags == MouseFlags.Button1DoubleClicked - || me.Flags == MouseFlags.Button1TripleClicked - || me.Flags == MouseFlags.Button1Clicked - || (me.Flags == MouseFlags.ReportMousePosition && _selected > -1) - || (me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && _selected > -1)) - { - int pos = _xOrigin; - Point locationOffset = default; - - if (SuperView is { }) - { - locationOffset.X += SuperView.Border!.Thickness.Left; - locationOffset.Y += SuperView.Border!.Thickness.Top; - } - - int cx = me.Position.X - locationOffset.X; - - for (var i = 0; i < Menus.Length; i++) - { - if (cx >= pos && cx < pos + _leftPadding + Menus [i].TitleLength + Menus [i].Help.GetColumns () + _rightPadding) - { - if (me.Flags == MouseFlags.Button1Clicked) - { - if (Menus [i].IsTopLevel) - { - Point screen = ViewportToScreen (new Point (0, i)); - var menu = new Menu { Host = this, X = screen.X, Y = screen.Y, BarItems = Menus [i] }; - menu.Run (Menus [i].Action); - if (Application.Mouse.MouseGrabView == menu) - { - Application.Mouse.UngrabMouse (); - } - - menu.Dispose (); - } - else if (!IsMenuOpen) - { - Activate (i); - } - } - else if (me.Flags.HasFlag (MouseFlags.Button1Pressed) - || me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) - || me.Flags.HasFlag (MouseFlags.Button1TripleClicked)) - { - if (IsMenuOpen && !Menus [i].IsTopLevel) - { - CloseAllMenus (); - } - else if (!Menus [i].IsTopLevel) - { - Activate (i); - } - } - else if (_selected != i - && _selected > -1 - && (me.Flags == MouseFlags.ReportMousePosition - || (me.Flags is MouseFlags.Button1Pressed && me.Flags == MouseFlags.ReportMousePosition))) - { - if (IsMenuOpen) - { - if (!CloseMenu (true, false)) - { - return me.Handled = true; - } - - Activate (i); - } - } - else if (IsMenuOpen) - { - if (!UseSubMenusSingleFrame - || (UseSubMenusSingleFrame - && OpenCurrentMenu is { BarItems.Parent: { } } - && OpenCurrentMenu.BarItems.Parent.Parent != Menus [i])) - { - Activate (i); - } - } - - return me.Handled = true; - } - - if (i == Menus.Length - 1 && me.Flags == MouseFlags.Button1Clicked) - { - if (IsMenuOpen && !Menus [i].IsTopLevel) - { - CloseAllMenus (); - - return me.Handled = true; - } - } - - pos += _leftPadding + Menus [i].TitleLength + _rightPadding; - } - } - - return false; - } - - internal bool _handled; - internal bool _isContextMenuLoading; - private MenuBarItem [] _menus = []; - - internal bool HandleGrabView (MouseEventArgs me, View current) - { - if (Application.Mouse.MouseGrabView is { }) - { - if (me.View is MenuBar or Menu) - { - MenuBar? mbar = GetMouseGrabViewInstance (me.View); - - if (mbar is { }) - { - if (me.Flags == MouseFlags.Button1Clicked) - { - mbar.CleanUp (); - Application.Mouse.GrabMouse (me.View); - } - else - { - _handled = false; - - return false; - } - } - - if (Application.Mouse.MouseGrabView != me.View) - { - View v = me.View; - Application.Mouse.GrabMouse (v); - - return true; - } - - if (me.View != current) - { - View v = me.View; - Application.Mouse.GrabMouse (v); - MouseEventArgs nme; - - if (me.Position.Y > -1) - { - Point frameLoc = v.ScreenToFrame (me.Position); - - nme = new () - { - Position = frameLoc, - Flags = me.Flags, - View = v - }; - } - else - { - nme = new () - { - Position = new (me.Position.X + current.Frame.X, me.Position.Y + current.Frame.Y), - Flags = me.Flags, View = v - }; - } - - v.NewMouseEvent (nme); - - return false; - } - } - else if (!(me.View is MenuBar || me.View is Menu) - && me.Flags != MouseFlags.ReportMousePosition - && me.Flags != 0) - { - Application.Mouse.UngrabMouse (); - - if (IsMenuOpen) - { - CloseAllMenus (); - } - - _handled = false; - - return false; - } - else - { - _handled = false; - - return false; - } - } - else if (!IsMenuOpen - && (me.Flags == MouseFlags.Button1Pressed - || me.Flags == MouseFlags.Button1DoubleClicked - || me.Flags == MouseFlags.Button1TripleClicked - || me.Flags.HasFlag ( - MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition - ))) - { - Application.Mouse.GrabMouse (current); - } - else if (IsMenuOpen && (me.View is MenuBar || me.View is Menu)) - { - Application.Mouse.GrabMouse (me.View); - } - else - { - _handled = false; - - return false; - } - - _handled = true; - - return true; - } - - private MenuBar? GetMouseGrabViewInstance (View? view) - { - if (view is null || Application.Mouse.MouseGrabView is null) - { - return null; - } - - MenuBar? hostView = null; - - if (view is MenuBar) - { - hostView = (MenuBar)view; - } - else if (view is Menu) - { - hostView = ((Menu)view).Host; - } - - View grabView = Application.Mouse.MouseGrabView; - MenuBar? hostGrabView = null; - - if (grabView is MenuBar bar) - { - hostGrabView = bar; - } - else if (grabView is Menu menu) - { - hostGrabView = menu.Host; - } - - return hostView != hostGrabView ? hostGrabView : null; - } - - #endregion Mouse Handling - - - /// - public bool EnableForDesign (ref TContext targetView) where TContext : notnull - { - if (targetView is not Func actionFn) - { - actionFn = (_) => true; - } - - Menus = - [ - new MenuBarItem ( - "_File", - new MenuItem [] - { - new ( - "_New", - "", - () => actionFn ("New"), - null, - null, - KeyCode.CtrlMask | KeyCode.N - ), - new ( - "_Open", - "", - () => actionFn ("Open"), - null, - null, - KeyCode.CtrlMask | KeyCode.O - ), - new ( - "_Save", - "", - () => actionFn ("Save"), - null, - null, - KeyCode.CtrlMask | KeyCode.S - ), -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - null, -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - - // Don't use Application.Quit so we can disambiguate between quitting and closing the toplevel - new ( - "_Quit", - "", - () => actionFn ("Quit"), - null, - null, - KeyCode.CtrlMask | KeyCode.Q - ) - } - ), - new MenuBarItem ( - "_Edit", - new MenuItem [] - { - new ( - "_Copy", - "", - () => actionFn ("Copy"), - null, - null, - KeyCode.CtrlMask | KeyCode.C - ), - new ( - "C_ut", - "", - () => actionFn ("Cut"), - null, - null, - KeyCode.CtrlMask | KeyCode.X - ), - new ( - "_Paste", - "", - () => actionFn ("Paste"), - null, - null, - KeyCode.CtrlMask | KeyCode.V - ), - new MenuBarItem ( - "_Find and Replace", - new MenuItem [] - { - new ( - "F_ind", - "", - () => actionFn ("Find"), - null, - null, - KeyCode.CtrlMask | KeyCode.F - ), - new ( - "_Replace", - "", - () => actionFn ("Replace"), - null, - null, - KeyCode.CtrlMask | KeyCode.H - ), - new MenuBarItem ( - "_3rd Level", - new MenuItem [] - { - new ( - "_1st", - "", - () => actionFn ( - "1" - ), - null, - null, - KeyCode.F1 - ), - new ( - "_2nd", - "", - () => actionFn ( - "2" - ), - null, - null, - KeyCode.F2 - ) - } - ), - new MenuBarItem ( - "_4th Level", - new MenuItem [] - { - new ( - "_5th", - "", - () => actionFn ( - "5" - ), - null, - null, - KeyCode.CtrlMask - | KeyCode.D5 - ), - new ( - "_6th", - "", - () => actionFn ( - "6" - ), - null, - null, - KeyCode.CtrlMask - | KeyCode.D6 - ) - } - ) - } - ), - new ( - "_Select All", - "", - () => actionFn ("Select All"), - null, - null, - KeyCode.CtrlMask - | KeyCode.ShiftMask - | KeyCode.S - ) - } - ), - new MenuBarItem ("_About", "Top-Level", () => actionFn ("About")) - ]; - return true; - } -} diff --git a/Terminal.Gui/Views/Menuv1/MenuBarItem.cs b/Terminal.Gui/Views/Menuv1/MenuBarItem.cs deleted file mode 100644 index fe96c1589..000000000 --- a/Terminal.Gui/Views/Menuv1/MenuBarItem.cs +++ /dev/null @@ -1,265 +0,0 @@ - - -namespace Terminal.Gui.Views; - -/// -/// is a menu item on . MenuBarItems do not support -/// . -/// -[Obsolete ("Use MenuBarItemv2 instead.", false)] -public class MenuBarItem : MenuItem -{ - /// Initializes a new as a . - /// Title for the menu item. - /// Help text to display. Will be displayed next to the Title surrounded by parentheses. - /// Action to invoke when the menu item is activated. - /// Function to determine if the action can currently be executed. - /// The parent of this if any. - public MenuBarItem ( - string title, - string help, - Action action, - Func? canExecute = null, - MenuItem? parent = null - ) : base (title, help, action, canExecute, parent) - { - SetInitialProperties (title, null, null, true); - } - - /// Initializes a new . - /// Title for the menu item. - /// The items in the current menu. - /// The parent of this if any. - public MenuBarItem (string title, MenuItem [] children, MenuItem? parent = null) { SetInitialProperties (title, children, parent); } - - /// Initializes a new with separate list of items. - /// Title for the menu item. - /// The list of items in the current menu. - /// The parent of this if any. - public MenuBarItem (string title, List children, MenuItem? parent = null) { SetInitialProperties (title, children, parent); } - - /// Initializes a new . - /// The items in the current menu. - public MenuBarItem (MenuItem [] children) : this ("", children) { } - - /// Initializes a new . - public MenuBarItem () : this ([]) { } - - /// - /// Gets or sets an array of objects that are the children of this - /// - /// - /// The children. - public MenuItem? []? Children { get; set; } - - internal bool IsTopLevel => Parent is null && (Children is null || Children.Length == 0) && Action != null; - - /// Get the index of a child . - /// - /// Returns a greater than -1 if the is a child. - public int GetChildrenIndex (MenuItem children) - { - var i = 0; - - if (Children is null) - { - return -1; - } - - foreach (MenuItem? child in Children) - { - if (child == children) - { - return i; - } - - i++; - } - - return -1; - } - - /// Check if a is a submenu of this MenuBar. - /// - /// Returns true if it is a submenu. false otherwise. - public bool IsSubMenuOf (MenuItem menuItem) - { - return Children!.Any (child => child == menuItem && child.Parent == menuItem.Parent); - } - - /// Check if a is a . - /// - /// Returns a or null otherwise. - public MenuBarItem? SubMenu (MenuItem? menuItem) { return menuItem as MenuBarItem; } - - internal void AddShortcutKeyBindings (MenuBar menuBar) - { - if (Children is null) - { - return; - } - - _menuBar = menuBar; - - IEnumerable menuItems = Children.Where (m => m is { })!; - - foreach (MenuItem menuItem in menuItems) - { - // Initialize MenuItem _menuBar - menuItem._menuBar = menuBar; - - // For MenuBar only add shortcuts for submenus - if (menuItem.ShortcutKey != Key.Empty) - { - menuItem.AddShortcutKeyBinding (menuBar, Key.Empty); - } - - SubMenu (menuItem)?.AddShortcutKeyBindings (menuBar); - } - } - - // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local - private void SetInitialProperties (string title, object? children, MenuItem? parent = null, bool isTopLevel = false) - { - if (!isTopLevel && children is null) - { - throw new ArgumentNullException ( - nameof (children), - @"The parameter cannot be null. Use an empty array instead." - ); - } - - SetTitle (title); - - if (parent is { }) - { - Parent = parent; - } - - switch (children) - { - case List childrenList: - { - MenuItem [] newChildren = []; - - foreach (MenuItem [] grandChild in childrenList) - { - foreach (MenuItem child in grandChild) - { - SetParent (grandChild); - Array.Resize (ref newChildren, newChildren.Length + 1); - newChildren [^1] = child; - } - } - - Children = newChildren; - - break; - } - case MenuItem [] items: - SetParent (items); - Children = items; - - break; - default: - Children = null; - - break; - } - } - - private void SetParent (MenuItem [] children) - { - foreach (MenuItem child in children) - { - if (child is { Parent: null }) - { - child.Parent = this; - } - } - } - - private void SetTitle (string? title) - { - title ??= string.Empty; - Title = title; - } - - /// - /// Add a dynamically into the .Menus. - /// - /// - /// - public void AddMenuBarItem (MenuBar menuBar, MenuItem? menuItem = null) - { - ArgumentNullException.ThrowIfNull (menuBar); - - _menuBar = menuBar; - - if (menuItem is null) - { - MenuBarItem [] menus = _menuBar.Menus; - Array.Resize (ref menus, menus.Length + 1); - menus [^1] = this; - _menuBar.Menus = menus; - } - else - { - MenuItem [] childrens = (Children ?? [])!; - Array.Resize (ref childrens, childrens.Length + 1); - menuItem._menuBar = menuBar; - childrens [^1] = menuItem; - Children = childrens; - } - } - - /// - public override void RemoveMenuItem () - { - if (Children is { }) - { - foreach (MenuItem? menuItem in Children) - { - if (menuItem?.ShortcutKey != Key.Empty) - { - // Remove an existent ShortcutKey - _menuBar?.HotKeyBindings.Remove (menuItem?.ShortcutKey!); - } - } - } - - if (ShortcutKey != Key.Empty) - { - // Remove an existent ShortcutKey - _menuBar?.HotKeyBindings.Remove (ShortcutKey!); - } - - var index = _menuBar!.Menus.IndexOf (this); - if (index > -1) - { - if (_menuBar.Menus [index].HotKey != Key.Empty) - { - // Remove an existent HotKey - _menuBar.HotKeyBindings.Remove (HotKey!.WithAlt); - } - - _menuBar.Menus [index] = null!; - } - - var i = 0; - - foreach (MenuBarItem m in _menuBar.Menus) - { - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (m != null) - { - _menuBar.Menus [i] = m; - i++; - } - } - - MenuBarItem [] menus = _menuBar.Menus; - Array.Resize (ref menus, menus.Length - 1); - _menuBar.Menus = menus; - } -} diff --git a/Terminal.Gui/Views/Menuv1/MenuClosingEventArgs.cs b/Terminal.Gui/Views/Menuv1/MenuClosingEventArgs.cs deleted file mode 100644 index e223893c8..000000000 --- a/Terminal.Gui/Views/Menuv1/MenuClosingEventArgs.cs +++ /dev/null @@ -1,34 +0,0 @@ -#nullable disable -namespace Terminal.Gui.Views; - -#pragma warning disable CS0618 // Type or member is obsolete - -/// An which allows passing a cancelable menu closing event. -public class MenuClosingEventArgs : EventArgs -{ - /// Initializes a new instance of . - /// The current parent. - /// Whether the current menu will reopen. - /// Indicates whether it is a sub-menu. - public MenuClosingEventArgs (MenuBarItem currentMenu, bool reopen, bool isSubMenu) - { - CurrentMenu = currentMenu; - Reopen = reopen; - IsSubMenu = isSubMenu; - } - - /// - /// Flag that allows the cancellation of the event. If set to in the event handler, the - /// event will be canceled. - /// - public bool Cancel { get; set; } - - /// The current parent. - public MenuBarItem CurrentMenu { get; } - - /// Indicates whether the current menu is a sub-menu. - public bool IsSubMenu { get; } - - /// Indicates whether the current menu will reopen. - public bool Reopen { get; } -} diff --git a/Terminal.Gui/Views/Menuv1/MenuItem.cs b/Terminal.Gui/Views/Menuv1/MenuItem.cs deleted file mode 100644 index 9d66c1c45..000000000 --- a/Terminal.Gui/Views/Menuv1/MenuItem.cs +++ /dev/null @@ -1,387 +0,0 @@ -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. - - -namespace Terminal.Gui.Views; - -/// -/// A has title, an associated help text, and an action to execute on activation. MenuItems -/// can also have a checked indicator (see ). -/// -[Obsolete ("Use MenuItemv2 instead.", false)] - -public class MenuItem -{ - internal MenuBar _menuBar; - - /// Initializes a new instance of - public MenuItem (Key? shortcutKey = null) : this ("", "", null, null, null, shortcutKey) { } - - /// Initializes a new instance of . - /// Title for the menu item. - /// Help text to display. - /// Action to invoke when the menu item is activated. - /// Function to determine if the action can currently be executed. - /// The of this menu item. - /// The keystroke combination. - public MenuItem ( - string? title, - string? help, - Action? action, - Func? canExecute = null, - MenuItem? parent = null, - Key? shortcutKey = null - ) - { - Title = title ?? ""; - Help = help ?? ""; - Action = action!; - CanExecute = canExecute!; - Parent = parent!; - - if (Parent is { } && Parent.ShortcutKey != Key.Empty) - { - Parent.ShortcutKey = Key.Empty; - } - // Setter will ensure Key.Empty if it's null - ShortcutKey = shortcutKey!; - } - - private bool _allowNullChecked; - private MenuItemCheckStyle _checkType; - - private string _title; - - /// Gets or sets the action to be invoked when the menu item is triggered. - /// Method to invoke. - public Action? Action { get; set; } - - /// - /// Used only if is of type. If - /// allows to be null, true or false. If only - /// allows to be true or false. - /// - public bool AllowNullChecked - { - get => _allowNullChecked; - set - { - _allowNullChecked = value; - Checked ??= false; - } - } - - /// - /// Gets or sets the action to be invoked to determine if the menu can be triggered. If - /// returns the menu item will be enabled. Otherwise, it will be disabled. - /// - /// Function to determine if the action is can be executed or not. - public Func? CanExecute { get; set; } - - /// - /// Sets or gets whether the shows a check indicator or not. See - /// . - /// - public bool? Checked { set; get; } - - /// - /// Sets or gets the of a menu item where is set to - /// . - /// - public MenuItemCheckStyle CheckType - { - get => _checkType; - set - { - _checkType = value; - - if (_checkType == MenuItemCheckStyle.Checked && !_allowNullChecked && Checked is null) - { - Checked = false; - } - } - } - - /// Gets or sets arbitrary data for the menu item. - /// This property is not used internally. - public object Data { get; set; } - - /// Gets or sets the help text for the menu item. The help text is drawn to the right of the . - /// The help text. - public string Help { get; set; } - - /// - /// Returns if the menu item is enabled. This method is a wrapper around - /// . - /// - public bool IsEnabled () { return CanExecute?.Invoke () ?? true; } - - /// Gets the parent for this . - /// The parent. - public MenuItem? Parent { get; set; } - - /// Gets or sets the title of the menu item . - /// The title. - public string Title - { - get => _title; - set - { - if (_title == value) - { - return; - } - - _title = value; - GetHotKey (); - } - } - - /// - /// Toggle the between three states if is - /// or between two states if is . - /// - public void ToggleChecked () - { - if (_checkType != MenuItemCheckStyle.Checked) - { - throw new InvalidOperationException ("This isn't a Checked MenuItemCheckStyle!"); - } - - bool? previousChecked = Checked; - - if (AllowNullChecked) - { - Checked = previousChecked switch - { - null => true, - true => false, - false => null - }; - } - else - { - Checked = !Checked; - } - } - - /// Merely a debugging aid to see the interaction with main. - internal bool GetMenuBarItem () { return IsFromSubMenu; } - - /// Merely a debugging aid to see the interaction with main. - internal MenuItem GetMenuItem () { return this; } - - /// Gets if this is from a sub-menu. - internal bool IsFromSubMenu => Parent != null; - - internal int TitleLength => GetMenuBarItemLength (Title); - - // - // ┌─────────────────────────────┐ - // │ Quit Quit UI Catalog Ctrl+Q │ - // └─────────────────────────────┘ - // ┌─────────────────┐ - // │ ◌ TopLevel Alt+T │ - // └─────────────────┘ - // TODO: Replace the `2` literals with named constants - internal int Width => 1 - + // space before Title - TitleLength - + 2 - + // space after Title - BUGBUG: This should be 1 - (Checked == true || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio) - ? 2 - : 0) - + // check glyph + space - (Help.GetColumns () > 0 ? 2 + Help.GetColumns () : 0) - + // Two spaces before Help - (ShortcutTag.GetColumns () > 0 - ? 2 + ShortcutTag.GetColumns () - : 0); // Pad two spaces before shortcut tag (which are also aligned right) - - private static int GetMenuBarItemLength (string title) - { - return title.EnumerateRunes () - .Where (ch => ch != MenuBar.HotKeySpecifier) - .Sum (ch => Math.Max (ch.GetColumns (), 1)); - } - - #region Keyboard Handling - - private Key _hotKey = Key.Empty; - - /// - /// The HotKey is used to activate a with the keyboard. HotKeys are defined by prefixing the - /// of a MenuItem with an underscore ('_'). - /// - /// Pressing Alt-Hotkey for a (menu items on the menu bar) works even if the menu is - /// not active. Once a menu has focus and is active, pressing just the HotKey will activate the MenuItem. - /// - /// - /// For example for a MenuBar with a "_File" MenuBarItem that contains a "_New" MenuItem, Alt-F will open the - /// File menu. Pressing the N key will then activate the New MenuItem. - /// - /// See also which enable global key-bindings to menu items. - /// - public Key? HotKey - { - get => _hotKey; - private set - { - var oldKey = _hotKey; - _hotKey = value ?? Key.Empty; - UpdateHotKeyBinding (oldKey); - } - } - - private void GetHotKey () - { - var nextIsHot = false; - - foreach (char x in _title) - { - if (x == MenuBar.HotKeySpecifier.Value) - { - nextIsHot = true; - } - else if (nextIsHot) - { - HotKey = char.ToLower (x); - - return; - } - } - - HotKey = Key.Empty; - } - - private Key _shortcutKey = Key.Empty; - - /// - /// Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the - /// that is the parent of the this - /// . - /// - /// The will be drawn on the MenuItem to the right of the and - /// text. See . - /// - /// - public Key? ShortcutKey - { - get => _shortcutKey; - set - { - var oldKey = _shortcutKey; - _shortcutKey = value ?? Key.Empty; - UpdateShortcutKeyBinding (oldKey); - } - } - - /// Gets the text describing the keystroke combination defined by . - public string ShortcutTag => ShortcutKey != Key.Empty ? ShortcutKey!.ToString () : string.Empty; - - internal void AddShortcutKeyBinding (MenuBar menuBar, Key key) - { - ArgumentNullException.ThrowIfNull (menuBar); - - _menuBar = menuBar; - - AddOrUpdateShortcutKeyBinding (key); - } - - private void AddOrUpdateShortcutKeyBinding (Key key) - { - if (key != Key.Empty) - { - _menuBar.HotKeyBindings.Remove (key); - } - - if (ShortcutKey != Key.Empty) - { - KeyBinding keyBinding = new ([Command.Select], null, data: this); - // Remove an existent ShortcutKey - _menuBar.HotKeyBindings.Remove (ShortcutKey!); - _menuBar.HotKeyBindings.Add (ShortcutKey!, keyBinding); - } - } - - private void UpdateHotKeyBinding (Key oldKey) - { - if (_menuBar is null or { IsInitialized: false }) - { - return; - } - - if (oldKey != Key.Empty) - { - var index = _menuBar.Menus.IndexOf (this); - - if (index > -1) - { - _menuBar.HotKeyBindings.Remove (oldKey.WithAlt); - } - } - - if (HotKey != Key.Empty) - { - var index = _menuBar.Menus.IndexOf (this); - - if (index > -1) - { - _menuBar.HotKeyBindings.Remove (HotKey!.WithAlt); - KeyBinding keyBinding = new ([Command.Toggle], null, data: this); - _menuBar.HotKeyBindings.Add (HotKey.WithAlt, keyBinding); - } - } - } - - private void UpdateShortcutKeyBinding (Key oldKey) - { - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (_menuBar is null) - { - return; - } - - AddOrUpdateShortcutKeyBinding (oldKey); - } - - #endregion Keyboard Handling - - /// - /// Removes a dynamically from the . - /// - public virtual void RemoveMenuItem () - { - if (Parent is { }) - { - MenuItem? []? childrens = ((MenuBarItem)Parent).Children; - var i = 0; - - foreach (MenuItem? c in childrens!) - { - if (c != this) - { - childrens [i] = c; - i++; - } - } - - Array.Resize (ref childrens, childrens.Length - 1); - - if (childrens.Length == 0) - { - ((MenuBarItem)Parent).Children = null; - } - else - { - ((MenuBarItem)Parent).Children = childrens; - } - } - - if (ShortcutKey != Key.Empty) - { - // Remove an existent ShortcutKey - _menuBar.HotKeyBindings.Remove (ShortcutKey!); - } - } -} diff --git a/Terminal.Gui/Views/Menuv1/MenuItemCheckStyle.cs b/Terminal.Gui/Views/Menuv1/MenuItemCheckStyle.cs deleted file mode 100644 index ad189e373..000000000 --- a/Terminal.Gui/Views/Menuv1/MenuItemCheckStyle.cs +++ /dev/null @@ -1,16 +0,0 @@ -#nullable disable -namespace Terminal.Gui.Views; - -/// Specifies how a shows selection state. -[Flags] -public enum MenuItemCheckStyle -{ - /// The menu item will be shown normally, with no check indicator. The default. - NoCheck = 0b_0000_0000, - - /// The menu item will indicate checked/un-checked state (see ). - Checked = 0b_0000_0001, - - /// The menu item is part of a menu radio group (see ) and will indicate selected state. - Radio = 0b_0000_0010 -} diff --git a/Terminal.Gui/Views/Menuv1/MenuOpenedEventArgs.cs b/Terminal.Gui/Views/Menuv1/MenuOpenedEventArgs.cs deleted file mode 100644 index 581b9b04c..000000000 --- a/Terminal.Gui/Views/Menuv1/MenuOpenedEventArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -#nullable disable -namespace Terminal.Gui.Views; -#pragma warning disable CS0618 // Type or member is obsolete - -/// Defines arguments for the event -public class MenuOpenedEventArgs : EventArgs -{ - /// Creates a new instance of the class - /// - /// - public MenuOpenedEventArgs (MenuBarItem parent, MenuItem menuItem) - { - Parent = parent; - MenuItem = menuItem; - } - - /// Gets the being opened. - public MenuItem MenuItem { get; } - - /// The parent of . Will be null if menu opening is the root. - public MenuBarItem Parent { get; } -} diff --git a/Terminal.Gui/Views/Menuv1/MenuOpeningEventArgs.cs b/Terminal.Gui/Views/Menuv1/MenuOpeningEventArgs.cs deleted file mode 100644 index c2e10d6d4..000000000 --- a/Terminal.Gui/Views/Menuv1/MenuOpeningEventArgs.cs +++ /dev/null @@ -1,27 +0,0 @@ -#nullable disable -namespace Terminal.Gui.Views; - -#pragma warning disable CS0618 // Type or member is obsolete - -/// -/// An which allows passing a cancelable menu opening event or replacing with a new -/// . -/// -public class MenuOpeningEventArgs : EventArgs -{ - /// Initializes a new instance of . - /// The current parent. - public MenuOpeningEventArgs (MenuBarItem currentMenu) { CurrentMenu = currentMenu; } - - /// - /// Flag that allows the cancellation of the event. If set to in the event handler, the - /// event will be canceled. - /// - public bool Cancel { get; set; } - - /// The current parent. - public MenuBarItem CurrentMenu { get; } - - /// The new to be replaced. - public MenuBarItem NewMenuBarItem { get; set; } -} diff --git a/Terminal.Gui/Views/TextInput/TextField.cs b/Terminal.Gui/Views/TextInput/TextField.cs index 38d123c19..c4aa19754 100644 --- a/Terminal.Gui/Views/TextInput/TextField.cs +++ b/Terminal.Gui/Views/TextInput/TextField.cs @@ -1233,7 +1233,7 @@ public class TextField : View, IDesignable DisposeContextMenu (); PopoverMenu menu = new ( - new List + new List { new (this, Command.SelectAll, Strings.ctxSelectAll), new (this, Command.DeleteAll, Strings.ctxDeleteAll), diff --git a/Terminal.Gui/Views/TextInput/TextView.cs b/Terminal.Gui/Views/TextInput/TextView.cs index c2ad7d463..5e2124d38 100644 --- a/Terminal.Gui/Views/TextInput/TextView.cs +++ b/Terminal.Gui/Views/TextInput/TextView.cs @@ -2370,13 +2370,13 @@ public class TextView : View, IDesignable PopoverMenu menu = new ( new List { - new MenuItemv2 (this, Command.SelectAll, Strings.ctxSelectAll), - new MenuItemv2 (this, Command.DeleteAll, Strings.ctxDeleteAll), - new MenuItemv2 (this, Command.Copy, Strings.ctxCopy), - new MenuItemv2 (this, Command.Cut, Strings.ctxCut), - new MenuItemv2 (this, Command.Paste, Strings.ctxPaste), - new MenuItemv2 (this, Command.Undo, Strings.ctxUndo), - new MenuItemv2 (this, Command.Redo, Strings.ctxRedo) + new MenuItem (this, Command.SelectAll, Strings.ctxSelectAll), + new MenuItem (this, Command.DeleteAll, Strings.ctxDeleteAll), + new MenuItem (this, Command.Copy, Strings.ctxCopy), + new MenuItem (this, Command.Cut, Strings.ctxCut), + new MenuItem (this, Command.Paste, Strings.ctxPaste), + new MenuItem (this, Command.Undo, Strings.ctxUndo), + new MenuItem (this, Command.Redo, Strings.ctxRedo) }); menu.KeyChanged += ContextMenu_KeyChanged; diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index 6f9bb2268..e3feb8ef9 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -68,13 +68,9 @@ public partial class Toplevel : View #region SubViews - // TODO: Deprecate - Any view can host a menubar in v2 - /// Gets the latest added into this Toplevel. - public MenuBar? MenuBar => (MenuBar?)SubViews?.LastOrDefault (s => s is MenuBar); - - //// TODO: Deprecate - Any view can host a statusbar in v2 - ///// Gets the latest added into this Toplevel. - //public StatusBar? StatusBar => (StatusBar?)SubViews?.LastOrDefault (s => s is StatusBar); + //// TODO: Deprecate - Any view can host a menubar in v2 + ///// Gets the latest added into this Toplevel. + //public MenuBar? MenuBar => (MenuBar?)SubViews?.LastOrDefault (s => s is MenuBar); #endregion @@ -85,7 +81,7 @@ public partial class Toplevel : View /// Setting this property directly is discouraged. Use instead. public bool Running { get; set; } - // TODO: IRunnable: Re-implement in IRunnable + // TODO: Deprecate. Other than a few tests, this is not used anywhere. /// /// if was already loaded by the /// , otherwise. diff --git a/Tests/IntegrationTests/FluentTests/MenuBarv2Tests.cs b/Tests/IntegrationTests/FluentTests/MenuBarvTests.cs similarity index 89% rename from Tests/IntegrationTests/FluentTests/MenuBarv2Tests.cs rename to Tests/IntegrationTests/FluentTests/MenuBarvTests.cs index e0ca97b45..499957f12 100644 --- a/Tests/IntegrationTests/FluentTests/MenuBarv2Tests.cs +++ b/Tests/IntegrationTests/FluentTests/MenuBarvTests.cs @@ -7,13 +7,13 @@ using Xunit.Abstractions; namespace IntegrationTests.FluentTests; /// -/// Tests for the MenuBarv2 class +/// Tests for the MenuBar class /// -public class MenuBarv2Tests +public class MenuBarTests { private readonly TextWriter _out; - public MenuBarv2Tests (ITestOutputHelper outputHelper) + public MenuBarTests (ITestOutputHelper outputHelper) { CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; _out = new TestOutputWriter (outputHelper); @@ -27,11 +27,11 @@ public class MenuBarv2Tests .Then ((_) => { // Create a menu bar with no items - var menuBar = new MenuBarv2 (); + var menuBar = new MenuBar (); Assert.Empty (menuBar.SubViews); Assert.False (menuBar.CanFocus); Assert.Equal (Orientation.Horizontal, menuBar.Orientation); - Assert.Equal (Key.F9, MenuBarv2.DefaultKey); + Assert.Equal (Key.F9, MenuBar.DefaultKey); }); } @@ -39,7 +39,7 @@ public class MenuBarv2Tests [ClassData (typeof (TestDrivers))] public void Initializes_WithItems (TestDriver d) { - MenuBarItemv2 [] menuItems = []; + MenuBarItem [] menuItems = []; using GuiTestContext c = With.A (80, 25, d, _out) .Then ((_) => @@ -50,25 +50,25 @@ public class MenuBarv2Tests new ( "_File", [ - new MenuItemv2 ("_Open", "Opens a file", () => { }) + new MenuItem ("_Open", "Opens a file", () => { }) ]), new ( "_Edit", [ - new MenuItemv2 ("_Copy", "Copies selection", () => { }) + new MenuItem ("_Copy", "Copies selection", () => { }) ]) ]; - var menuBar = new MenuBarv2 (menuItems); + var menuBar = new MenuBar (menuItems); Assert.Equal (2, menuBar.SubViews.Count); // First item should be the File menu - var fileMenu = menuBar.SubViews.ElementAt (0) as MenuBarItemv2; + var fileMenu = menuBar.SubViews.ElementAt (0) as MenuBarItem; Assert.NotNull (fileMenu); Assert.Equal ("_File", fileMenu.Title); // Second item should be the Edit menu - var editMenu = menuBar.SubViews.ElementAt (1) as MenuBarItemv2; + var editMenu = menuBar.SubViews.ElementAt (1) as MenuBarItem; Assert.NotNull (editMenu); Assert.Equal ("_Edit", editMenu.Title); }); @@ -81,7 +81,7 @@ public class MenuBarv2Tests using GuiTestContext c = With.A (80, 25, d, _out) .Then ((_) => { - var menuBar = new MenuBarv2 (); + var menuBar = new MenuBar (); // Set items through Menus property menuBar.Menus = @@ -102,7 +102,7 @@ public class MenuBarv2Tests using GuiTestContext c = With.A (80, 25, d, _out) .Then ((_) => { - var menuBar = new MenuBarv2 (); + var menuBar = new MenuBar (); var oldKeyValue = Key.Empty; var newKeyValue = Key.Empty; @@ -136,13 +136,13 @@ public class MenuBarv2Tests [ClassData (typeof (TestDrivers))] public void DefaultKey_Activates (TestDriver d) { - MenuBarv2? menuBar = null; + MenuBar? menuBar = null; Toplevel? top = null; using GuiTestContext c = With.A (50, 20, d, _out) .Then ((app) => { - menuBar = new MenuBarv2 (); + menuBar = new MenuBar (); top = app.TopRunnable!; top.Add ( @@ -156,11 +156,11 @@ public class MenuBarv2Tests app.TopRunnable!.Add (menuBar); }) .WaitIteration () - .AssertIsNotType (top?.App?.Navigation!.GetFocused ()) + .AssertIsNotType (top?.App?.Navigation!.GetFocused ()) .ScreenShot ("MenuBar initial state", _out) - .EnqueueKeyEvent (MenuBarv2.DefaultKey) + .EnqueueKeyEvent (MenuBar.DefaultKey) .WaitIteration () - .ScreenShot ($"After {MenuBarv2.DefaultKey}", _out) + .ScreenShot ($"After {MenuBar.DefaultKey}", _out) .AssertEqual ("_New file", top?.App?.Navigation!.GetFocused ()!.Title) .AssertTrue (top?.App?.Popover?.GetActivePopover () is PopoverMenu) .AssertTrue (menuBar?.IsOpen ()); @@ -171,13 +171,13 @@ public class MenuBarv2Tests [ClassData (typeof (TestDrivers))] public void DefaultKey_DeActivates (TestDriver d) { - MenuBarv2? menuBar = null; + MenuBar? menuBar = null; IApplication? app = null; using GuiTestContext c = With.A (50, 20, d, _out) .Then ((a) => { app = a; - menuBar = new MenuBarv2 (); + menuBar = new MenuBar (); Toplevel top = app.TopRunnable!; top.Add ( @@ -191,14 +191,14 @@ public class MenuBarv2Tests app.TopRunnable!.Add (menuBar); }) .WaitIteration () - .AssertIsNotType (app?.Navigation!.GetFocused ()) + .AssertIsNotType (app?.Navigation!.GetFocused ()) .ScreenShot ("MenuBar initial state", _out) - .EnqueueKeyEvent (MenuBarv2.DefaultKey) - .ScreenShot ($"After {MenuBarv2.DefaultKey}", _out) + .EnqueueKeyEvent (MenuBar.DefaultKey) + .ScreenShot ($"After {MenuBar.DefaultKey}", _out) .AssertEqual ("_New file", app?.Navigation!.GetFocused ()!.Title) - .EnqueueKeyEvent (MenuBarv2.DefaultKey) - .ScreenShot ($"After {MenuBarv2.DefaultKey}", _out) - .AssertIsNotType (app?.Navigation!.GetFocused ()); + .EnqueueKeyEvent (MenuBar.DefaultKey) + .ScreenShot ($"After {MenuBar.DefaultKey}", _out) + .AssertIsNotType (app?.Navigation!.GetFocused ()); } [Theory] @@ -211,14 +211,14 @@ public class MenuBarv2Tests { app = a; // Create a menu bar with items that have submenus - var fileMenuItem = new MenuBarItemv2 ( + var fileMenuItem = new MenuBarItem ( "_File", [ - new MenuItemv2 ("_Open", string.Empty, null), - new MenuItemv2 ("_Save", string.Empty, null) + new MenuItem ("_Open", string.Empty, null), + new MenuItem ("_Save", string.Empty, null) ]); - var menuBar = new MenuBarv2 ([fileMenuItem]) { App = app }; + var menuBar = new MenuBar ([fileMenuItem]) { App = app }; // Initially, no menu should be open Assert.False (menuBar.IsOpen ()); @@ -229,12 +229,12 @@ public class MenuBarv2Tests menuBar.EndInit (); // Simulate showing a popover menu by manipulating the first menu item - MethodInfo? showPopoverMethod = typeof (MenuBarv2).GetMethod ( + MethodInfo? showPopoverMethod = typeof (MenuBar).GetMethod ( "ShowPopover", BindingFlags.NonPublic | BindingFlags.Instance); // Set menu bar to active state using reflection - FieldInfo? activeField = typeof (MenuBarv2).GetField ( + FieldInfo? activeField = typeof (MenuBar).GetField ( "_active", BindingFlags.NonPublic | BindingFlags.Instance); activeField?.SetValue (menuBar, true); @@ -265,7 +265,7 @@ public class MenuBarv2Tests using GuiTestContext c = With.A (80, 25, d, _out) .Then ((app) => { - var menuBar = new MenuBarv2 (); + var menuBar = new MenuBar (); app.TopRunnable!.Add (menuBar); // Call EnableForDesign @@ -279,40 +279,40 @@ public class MenuBarv2Tests Assert.True (menuBar.SubViews.Count > 0); // Should have File, Edit and Help menus - View? fileMenu = menuBar.SubViews.FirstOrDefault (v => (v as MenuBarItemv2)?.Title == "_File"); - View? editMenu = menuBar.SubViews.FirstOrDefault (v => (v as MenuBarItemv2)?.Title == "_Edit"); - View? helpMenu = menuBar.SubViews.FirstOrDefault (v => (v as MenuBarItemv2)?.Title == "_Help"); + View? fileMenu = menuBar.SubViews.FirstOrDefault (v => (v as MenuBarItem)?.Title == "_File"); + View? editMenu = menuBar.SubViews.FirstOrDefault (v => (v as MenuBarItem)?.Title == "_Edit"); + View? helpMenu = menuBar.SubViews.FirstOrDefault (v => (v as MenuBarItem)?.Title == "_Help"); Assert.NotNull (fileMenu); Assert.NotNull (editMenu); Assert.NotNull (helpMenu); }) - .ScreenShot ("MenuBarv2 EnableForDesign", _out); + .ScreenShot ("MenuBar EnableForDesign", _out); } [Theory] [ClassData (typeof (TestDrivers))] public void Navigation_Left_Right_Wraps (TestDriver d) { - MenuBarv2? menuBar = null; + MenuBar? menuBar = null; IApplication? app = null; using GuiTestContext c = With.A (50, 20, d, _out) .Then ((a) => { app = a; - menuBar = new MenuBarv2 (); + menuBar = new MenuBar (); Toplevel top = app.TopRunnable!; menuBar.EnableForDesign (ref top); app.TopRunnable!.Add (menuBar); }) .WaitIteration () .ScreenShot ("MenuBar initial state", _out) - .EnqueueKeyEvent (MenuBarv2.DefaultKey) + .EnqueueKeyEvent (MenuBar.DefaultKey) .AssertTrue (app?.Popover?.GetActivePopover () is PopoverMenu) .AssertTrue (menuBar?.IsOpen ()) .AssertEqual ("_New file", app?.Navigation?.GetFocused ()!.Title) - .ScreenShot ($"After {MenuBarv2.DefaultKey}", _out) + .ScreenShot ($"After {MenuBar.DefaultKey}", _out) .EnqueueKeyEvent (Key.CursorRight) .AssertTrue (app?.Popover?.GetActivePopover () is PopoverMenu) .ScreenShot ("After right arrow", _out) @@ -334,14 +334,14 @@ public class MenuBarv2Tests [ClassData (typeof (TestDrivers))] public void MenuBarItem_With_QuitKey_Open_QuitKey_Restores_Focus_Correctly (TestDriver d) { - MenuBarv2? menuBar = null; + MenuBar? menuBar = null; IApplication? app = null; using GuiTestContext c = With.A (50, 20, d, _out) .Then ((a) => { app = a; - menuBar = new MenuBarv2 (); + menuBar = new MenuBar (); Toplevel top = app.TopRunnable!; top.Add ( @@ -354,24 +354,24 @@ public class MenuBarv2Tests menuBar.EnableForDesign (ref top); app.TopRunnable!.Add (menuBar); }) - .AssertIsNotType (app!.Navigation!.GetFocused ()) + .AssertIsNotType (app!.Navigation!.GetFocused ()) .ScreenShot ("MenuBar initial state", _out) - .EnqueueKeyEvent (MenuBarv2.DefaultKey) + .EnqueueKeyEvent (MenuBar.DefaultKey) .AssertEqual ("_New file", app.Navigation!.GetFocused ()!.Title) .AssertTrue (app?.Popover?.GetActivePopover () is PopoverMenu) .AssertTrue (menuBar?.IsOpen ()) .AssertEqual ("_New file", app?.Navigation?.GetFocused ()!.Title) - .ScreenShot ($"After {MenuBarv2.DefaultKey}", _out) + .ScreenShot ($"After {MenuBar.DefaultKey}", _out) .EnqueueKeyEvent (Application.QuitKey) .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) - .AssertIsNotType (app!.Navigation!.GetFocused ()); + .AssertIsNotType (app!.Navigation!.GetFocused ()); } [Theory] [ClassData (typeof (TestDrivers))] public void MenuBarItem_Without_QuitKey_Open_QuitKey_Restores_Focus_Correctly (TestDriver d) { - MenuBarv2? menuBar = null; + MenuBar? menuBar = null; IApplication? app = null; using GuiTestContext c = With.A (50, 20, d, _out) @@ -385,38 +385,38 @@ public class MenuBarv2Tests .Then ((a) => { app = a; - menuBar = new MenuBarv2 (); + menuBar = new MenuBar (); Toplevel? toplevel = app.TopRunnable; menuBar.EnableForDesign (ref toplevel!); app.TopRunnable!.Add (menuBar); }) .WaitIteration () - .AssertIsNotType (app?.Navigation!.GetFocused ()) + .AssertIsNotType (app?.Navigation!.GetFocused ()) .ScreenShot ("MenuBar initial state", _out) - .EnqueueKeyEvent (MenuBarv2.DefaultKey) + .EnqueueKeyEvent (MenuBar.DefaultKey) .EnqueueKeyEvent (Key.CursorRight) .AssertEqual ("Cu_t", app?.Navigation!.GetFocused ()!.Title) .AssertTrue (app?.Popover?.GetActivePopover () is PopoverMenu) .AssertTrue (menuBar?.IsOpen ()) .AssertEqual ("Cu_t", app?.Navigation?.GetFocused ()!.Title) - .ScreenShot ($"After {MenuBarv2.DefaultKey}", _out) + .ScreenShot ($"After {MenuBar.DefaultKey}", _out) .EnqueueKeyEvent (Application.QuitKey) .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) - .AssertIsNotType (app?.Navigation?.GetFocused ()); + .AssertIsNotType (app?.Navigation?.GetFocused ()); } [Theory] [ClassData (typeof (TestDrivers))] public void MenuBarItem_With_QuitKey_Open_QuitKey_Does_Not_Quit_App (TestDriver d) { - MenuBarv2? menuBar = null; + MenuBar? menuBar = null; IApplication? app = null; using GuiTestContext c = With.A (50, 20, d, _out) .Then ((a) => { app = a; - menuBar = new MenuBarv2 (); + menuBar = new MenuBar (); Toplevel top = app.TopRunnable!; top.Add ( @@ -430,12 +430,12 @@ public class MenuBarv2Tests app.TopRunnable!.Add (menuBar); }) .WaitIteration () - .AssertIsNotType (app!.Navigation!.GetFocused ()) + .AssertIsNotType (app!.Navigation!.GetFocused ()) .ScreenShot ("MenuBar initial state", _out) - .EnqueueKeyEvent (MenuBarv2.DefaultKey) + .EnqueueKeyEvent (MenuBar.DefaultKey) .AssertEqual ("_New file", app.Navigation!.GetFocused ()!.Title) .AssertTrue (app?.TopRunnable!.Running) - .ScreenShot ($"After {MenuBarv2.DefaultKey}", _out) + .ScreenShot ($"After {MenuBar.DefaultKey}", _out) .EnqueueKeyEvent (Application.QuitKey) .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) .AssertTrue (app!.TopRunnable!.Running); @@ -445,14 +445,14 @@ public class MenuBarv2Tests [ClassData (typeof (TestDrivers))] public void MenuBarItem_Without_QuitKey_Open_QuitKey_Does_Not_Quit_MenuBar_SuperView (TestDriver d) { - MenuBarv2? menuBar = null; + MenuBar? menuBar = null; IApplication? app = null; using GuiTestContext c = With.A (50, 20, d, _out) .Then ((a) => { app = a; - menuBar = new MenuBarv2 (); + menuBar = new MenuBar (); Toplevel top = app.TopRunnable!; top.Add ( @@ -463,9 +463,9 @@ public class MenuBarv2Tests }); menuBar.EnableForDesign (ref top); - IEnumerable items = menuBar.GetMenuItemsWithTitle ("_Quit"); + IEnumerable items = menuBar.GetMenuItemsWithTitle ("_Quit"); - foreach (MenuItemv2 item in items) + foreach (MenuItem item in items) { item.Key = Key.Empty; } @@ -473,11 +473,11 @@ public class MenuBarv2Tests app.TopRunnable!.Add (menuBar); }) .WaitIteration () - .AssertIsNotType (app?.Navigation!.GetFocused ()) + .AssertIsNotType (app?.Navigation!.GetFocused ()) .ScreenShot ("MenuBar initial state", _out) - .EnqueueKeyEvent (MenuBarv2.DefaultKey) + .EnqueueKeyEvent (MenuBar.DefaultKey) .AssertEqual ("_New file", app?.Navigation!.GetFocused ()!.Title) - .ScreenShot ($"After {MenuBarv2.DefaultKey}", _out) + .ScreenShot ($"After {MenuBar.DefaultKey}", _out) .EnqueueKeyEvent (Application.QuitKey) .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) .AssertTrue (app?.TopRunnable!.Running); @@ -505,7 +505,7 @@ public class MenuBarv2Tests using GuiTestContext c = With.A (50, 20, d, _out) .Then ((a) => { - var menuBar = new MenuBarv2 (); + var menuBar = new MenuBar (); Toplevel top = a.TopRunnable!; menuBar.EnableForDesign (ref top); a.TopRunnable!.Add (menuBar); @@ -539,7 +539,7 @@ public class MenuBarv2Tests using GuiTestContext c = With.A (50, 20, d, _out) .Then ((a) => { - var menuBar = new MenuBarv2 (); + var menuBar = new MenuBar (); Toplevel top = a.TopRunnable!; menuBar.EnableForDesign (ref top); a.TopRunnable!.Add (menuBar); diff --git a/Tests/IntegrationTests/FluentTests/PopverMenuTests.cs b/Tests/IntegrationTests/FluentTests/PopverMenuTests.cs index 09ce0417d..079411148 100644 --- a/Tests/IntegrationTests/FluentTests/PopverMenuTests.cs +++ b/Tests/IntegrationTests/FluentTests/PopverMenuTests.cs @@ -86,7 +86,7 @@ public class PopoverMenuTests view.SetFocus (); }) .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) - .AssertIsNotType (app?.Navigation!.GetFocused ()) + .AssertIsNotType (app?.Navigation!.GetFocused ()) .ScreenShot ("PopoverMenu initial state", _out) .Then ((_) => app?.Popover!.Show (app?.Popover.Popovers.First ())) .ScreenShot ("After Show", _out) @@ -179,15 +179,15 @@ public class PopoverMenuTests }) .ScreenShot ("PopoverMenu initial state", _out) .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) - .AssertIsNotType (app?.Navigation!.GetFocused ()) + .AssertIsNotType (app?.Navigation!.GetFocused ()) .Then ((_) => app?.Popover!.Show (app?.Popover.Popovers.First ())) .ScreenShot ("After Show", _out) .AssertTrue (app?.Popover?.GetActivePopover () is PopoverMenu) - .AssertIsType (app?.Navigation!.GetFocused ()) + .AssertIsType (app?.Navigation!.GetFocused ()) .EnqueueKeyEvent (Application.QuitKey) .ScreenShot ($"After {Application.QuitKey}", _out) .AssertFalse (app?.Popover?.GetActivePopover () is PopoverMenu) - .AssertIsNotType (app?.Navigation!.GetFocused ()); + .AssertIsNotType (app?.Navigation!.GetFocused ()); } [Theory] @@ -226,7 +226,7 @@ public class PopoverMenuTests view.SetFocus (); }) - .AssertIsNotType (app?.Navigation!.GetFocused ()) + .AssertIsNotType (app?.Navigation!.GetFocused ()) .ScreenShot ("PopoverMenu initial state", _out) .Then ((_) => app?.Popover!.Show (app?.Popover.Popovers.First ())) .ScreenShot ("PopoverMenu after Show", _out) @@ -361,7 +361,7 @@ public class PopoverMenuTests { var clicked = false; - MenuItemv2 [] menuItems = [new ("_New File", string.Empty, () => { clicked = true; })]; + MenuItem [] menuItems = [new ("_New File", string.Empty, () => { clicked = true; })]; IApplication? app = null; using GuiTestContext c = With.A (40, 10, d, _out) @@ -390,7 +390,7 @@ public class PopoverMenuTests { var clicked = false; - MenuItemv2 [] menuItems = + MenuItem [] menuItems = [ new ("One", "", null), new ("Two", "", null), diff --git a/Tests/UnitTests/Application/ApplicationTests.cs b/Tests/UnitTests/Application/ApplicationTests.cs index 52d9276da..4ff8ecb2b 100644 --- a/Tests/UnitTests/Application/ApplicationTests.cs +++ b/Tests/UnitTests/Application/ApplicationTests.cs @@ -661,7 +661,7 @@ public class ApplicationTests void OnApplicationOnIteration (object s, IterationEventArgs a) { Assert.True (top.Running); - top.Running = false; + top.RequestStop (); } } diff --git a/Tests/UnitTests/View/SubviewTests.cs b/Tests/UnitTests/View/SubviewTests.cs index 2da6f6065..21c320986 100644 --- a/Tests/UnitTests/View/SubviewTests.cs +++ b/Tests/UnitTests/View/SubviewTests.cs @@ -4,104 +4,104 @@ namespace UnitTests.ViewTests; public class SubViewTests { - private readonly ITestOutputHelper _output; - public SubViewTests (ITestOutputHelper output) { _output = output; } + //private readonly ITestOutputHelper _output; + //public SubViewTests (ITestOutputHelper output) { _output = output; } - // TODO: This is a poor unit tests. Not clear what it's testing. Refactor. - [Fact] - [AutoInitShutdown] - public void Initialized_Event_Will_Be_Invoked_When_Added_Dynamically () - { - var t = new Toplevel { Id = "0" }; + //// TODO: This is a poor unit tests. Not clear what it's testing. Refactor. + //[Fact] + //[AutoInitShutdown] + //public void Initialized_Event_Will_Be_Invoked_When_Added_Dynamically () + //{ + // var t = new Toplevel { Id = "0" }; - var w = new Window { Id = "t", Width = Dim.Fill (), Height = Dim.Fill () }; - var v1 = new View { Id = "v1", Width = Dim.Fill (), Height = Dim.Fill () }; - var v2 = new View { Id = "v2", Width = Dim.Fill (), Height = Dim.Fill () }; + // var w = new Window { Id = "t", Width = Dim.Fill (), Height = Dim.Fill () }; + // var v1 = new View { Id = "v1", Width = Dim.Fill (), Height = Dim.Fill () }; + // var v2 = new View { Id = "v2", Width = Dim.Fill (), Height = Dim.Fill () }; - int tc = 0, wc = 0, v1c = 0, v2c = 0, sv1c = 0; + // int tc = 0, wc = 0, v1c = 0, v2c = 0, sv1c = 0; - t.Initialized += (s, e) => - { - tc++; - Assert.Equal (1, tc); - Assert.Equal (1, wc); - Assert.Equal (1, v1c); - Assert.Equal (1, v2c); - Assert.Equal (0, sv1c); // Added after t in the Application.Iteration. + // t.Initialized += (s, e) => + // { + // tc++; + // Assert.Equal (1, tc); + // Assert.Equal (1, wc); + // Assert.Equal (1, v1c); + // Assert.Equal (1, v2c); + // Assert.Equal (0, sv1c); // Added after t in the Application.Iteration. - Assert.True (t.CanFocus); - Assert.True (w.CanFocus); - Assert.False (v1.CanFocus); - Assert.False (v2.CanFocus); + // Assert.True (t.CanFocus); + // Assert.True (w.CanFocus); + // Assert.False (v1.CanFocus); + // Assert.False (v2.CanFocus); - Application.LayoutAndDraw (); - }; + // Application.LayoutAndDraw (); + // }; - w.Initialized += (s, e) => - { - wc++; - Assert.Equal (t.Viewport.Width, w.Frame.Width); - Assert.Equal (t.Viewport.Height, w.Frame.Height); - }; + // w.Initialized += (s, e) => + // { + // wc++; + // Assert.Equal (t.Viewport.Width, w.Frame.Width); + // Assert.Equal (t.Viewport.Height, w.Frame.Height); + // }; - v1.Initialized += (s, e) => - { - v1c++; + // v1.Initialized += (s, e) => + // { + // v1c++; - //Assert.Equal (t.Viewport.Width, v1.Frame.Width); - //Assert.Equal (t.Viewport.Height, v1.Frame.Height); - }; + // //Assert.Equal (t.Viewport.Width, v1.Frame.Width); + // //Assert.Equal (t.Viewport.Height, v1.Frame.Height); + // }; - v2.Initialized += (s, e) => - { - v2c++; + // v2.Initialized += (s, e) => + // { + // v2c++; - //Assert.Equal (t.Viewport.Width, v2.Frame.Width); - //Assert.Equal (t.Viewport.Height, v2.Frame.Height); - }; - w.Add (v1, v2); - t.Add (w); + // //Assert.Equal (t.Viewport.Width, v2.Frame.Width); + // //Assert.Equal (t.Viewport.Height, v2.Frame.Height); + // }; + // w.Add (v1, v2); + // t.Add (w); - Application.Iteration += OnApplicationOnIteration; + // Application.Iteration += OnApplicationOnIteration; - Application.Run (t); - Application.Iteration -= OnApplicationOnIteration; + // Application.Run (t); + // Application.Iteration -= OnApplicationOnIteration; - t.Dispose (); - Application.Shutdown (); + // t.Dispose (); + // Application.Shutdown (); - Assert.Equal (1, tc); - Assert.Equal (1, wc); - Assert.Equal (1, v1c); - Assert.Equal (1, v2c); - Assert.Equal (1, sv1c); + // Assert.Equal (1, tc); + // Assert.Equal (1, wc); + // Assert.Equal (1, v1c); + // Assert.Equal (1, v2c); + // Assert.Equal (1, sv1c); - Assert.True (t.CanFocus); - Assert.True (w.CanFocus); - Assert.False (v1.CanFocus); - Assert.False (v2.CanFocus); + // Assert.True (t.CanFocus); + // Assert.True (w.CanFocus); + // Assert.False (v1.CanFocus); + // Assert.False (v2.CanFocus); - return; + // return; - void OnApplicationOnIteration (object s, IterationEventArgs a) - { - var sv1 = new View { Id = "sv1", Width = Dim.Fill (), Height = Dim.Fill () }; + // void OnApplicationOnIteration (object s, IterationEventArgs a) + // { + // var sv1 = new View { Id = "sv1", Width = Dim.Fill (), Height = Dim.Fill () }; - sv1.Initialized += (s, e) => - { - sv1c++; - Assert.NotEqual (t.Frame.Width, sv1.Frame.Width); - Assert.NotEqual (t.Frame.Height, sv1.Frame.Height); - Assert.False (sv1.CanFocus); + // sv1.Initialized += (s, e) => + // { + // sv1c++; + // Assert.NotEqual (t.Frame.Width, sv1.Frame.Width); + // Assert.NotEqual (t.Frame.Height, sv1.Frame.Height); + // Assert.False (sv1.CanFocus); - //Assert.Throws (() => sv1.CanFocus = true); - Assert.False (sv1.CanFocus); - }; + // //Assert.Throws (() => sv1.CanFocus = true); + // Assert.False (sv1.CanFocus); + // }; - v1.Add (sv1); + // v1.Add (sv1); - Application.LayoutAndDraw (); - t.Running = false; - } - } + // Application.LayoutAndDraw (); + // t.Running = false; + // } + //} } diff --git a/Tests/UnitTests/Views/MenuBarTests.cs b/Tests/UnitTests/Views/MenuBarTests.cs index b77ebc1ad..b15501a07 100644 --- a/Tests/UnitTests/Views/MenuBarTests.cs +++ b/Tests/UnitTests/Views/MenuBarTests.cs @@ -15,12 +15,12 @@ public class MenuBarTests () App = ApplicationImpl.Instance }; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; + var menuBar = new MenuBar () { Id = "menuBar" }; top.Add (menuBar); - var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuItem = new MenuItem { Id = "menuItem", Title = "_Item" }; + var menu = new Menu ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItem { Id = "menuBarItem", Title = "_New" }; var menuBarItemPopover = new PopoverMenu (); menuBar.Add (menuBarItem); @@ -41,7 +41,7 @@ public class MenuBarTests () Assert.False (menuBar.Active); // Act - Application.RaiseKeyDownEvent (MenuBarv2.DefaultKey); + Application.RaiseKeyDownEvent (MenuBar.DefaultKey); Assert.True (menuBar.Active); Assert.True (menuBar.IsOpen ()); Assert.True (menuBar.HasFocus); @@ -59,7 +59,7 @@ public class MenuBarTests () { // Arrange var top = new Toplevel () { App = ApplicationImpl.Instance }; - MenuBarv2 menuBar = new MenuBarv2 () { App = ApplicationImpl.Instance }; + MenuBar menuBar = new MenuBar () { App = ApplicationImpl.Instance }; menuBar.EnableForDesign (ref top); top.Add (menuBar); @@ -67,10 +67,10 @@ public class MenuBarTests () Assert.False (menuBar.Active); // Act - Application.RaiseKeyDownEvent (MenuBarv2.DefaultKey); + Application.RaiseKeyDownEvent (MenuBar.DefaultKey); Assert.True (menuBar.IsOpen ()); - Application.RaiseKeyDownEvent (MenuBarv2.DefaultKey); + Application.RaiseKeyDownEvent (MenuBar.DefaultKey); Assert.False (menuBar.Active); Assert.False (menuBar.IsOpen ()); Assert.False (menuBar.HasFocus); @@ -85,13 +85,13 @@ public class MenuBarTests () public void QuitKey_Deactivates () { // Arrange - var menuItem = new MenuItemv2 { Id = "menuItem", Title = "Menu_Item" }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_MenuBarItem" }; + var menuItem = new MenuItem { Id = "menuItem", Title = "Menu_Item" }; + var menu = new Menu ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItem { Id = "menuBarItem", Title = "_MenuBarItem" }; var menuBarItemPopover = new PopoverMenu (); menuBarItem.PopoverMenu = menuBarItemPopover; menuBarItemPopover.Root = menu; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; + var menuBar = new MenuBar () { Id = "menuBar" }; menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); @@ -99,7 +99,7 @@ public class MenuBarTests () top.Add (menuBar); SessionToken rs = Application.Begin (top); - Application.RaiseKeyDownEvent (MenuBarv2.DefaultKey); + Application.RaiseKeyDownEvent (MenuBar.DefaultKey); Assert.True (menuBar.Active); Assert.True (menuBar.IsOpen ()); Assert.True (menuBarItem.PopoverMenu.Visible); @@ -123,13 +123,13 @@ public class MenuBarTests () public void MenuBarItem_HotKey_Activates_And_Opens () { // Arrange - var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuItem = new MenuItem { Id = "menuItem", Title = "_Item" }; + var menu = new Menu ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItem { Id = "menuBarItem", Title = "_New" }; var menuBarItemPopover = new PopoverMenu (); menuBarItem.PopoverMenu = menuBarItemPopover; menuBarItemPopover.Root = menu; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; + var menuBar = new MenuBar () { Id = "menuBar" }; menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); @@ -155,13 +155,13 @@ public class MenuBarTests () public void MenuBarItem_HotKey_Deactivates () { // Arrange - var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuItem = new MenuItem { Id = "menuItem", Title = "_Item" }; + var menu = new Menu ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItem { Id = "menuBarItem", Title = "_New" }; var menuBarItemPopover = new PopoverMenu (); menuBarItem.PopoverMenu = menuBarItemPopover; menuBarItemPopover.Root = menu; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; + var menuBar = new MenuBar () { Id = "menuBar" }; menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); @@ -194,13 +194,13 @@ public class MenuBarTests () { // Arrange int action = 0; - var menuItem = new MenuItemv2 { Id = "menuItem", Title = "Menu_Item", Action = () => action++ }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_MenuBarItem" }; + var menuItem = new MenuItem { Id = "menuItem", Title = "Menu_Item", Action = () => action++ }; + var menu = new Menu ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItem { Id = "menuBarItem", Title = "_MenuBarItem" }; var menuBarItemPopover = new PopoverMenu (); menuBarItem.PopoverMenu = menuBarItemPopover; menuBarItemPopover.Root = menu; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; + var menuBar = new MenuBar () { Id = "menuBar" }; menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); @@ -227,13 +227,13 @@ public class MenuBarTests () public void MenuItems_HotKey_Deactivates () { // Arrange - var menuItem = new MenuItemv2 { Id = "menuItem", Title = "Menu_Item" }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_MenuBarItem" }; + var menuItem = new MenuItem { Id = "menuItem", Title = "Menu_Item" }; + var menu = new Menu ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItem { Id = "menuBarItem", Title = "_MenuBarItem" }; var menuBarItemPopover = new PopoverMenu (); menuBarItem.PopoverMenu = menuBarItemPopover; menuBarItemPopover.Root = menu; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; + var menuBar = new MenuBar () { Id = "menuBar" }; menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); @@ -264,13 +264,13 @@ public class MenuBarTests () public void HotKey_Makes_PopoverMenu_Visible_Only_Once () { // Arrange - var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuItem = new MenuItem { Id = "menuItem", Title = "_Item" }; + var menu = new Menu ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItem { Id = "menuBarItem", Title = "_New" }; var menuBarItemPopover = new PopoverMenu (); menuBarItem.PopoverMenu = menuBarItemPopover; menuBarItemPopover.Root = menu; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; + var menuBar = new MenuBar () { Id = "menuBar" }; menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); @@ -301,21 +301,21 @@ public class MenuBarTests () public void WhenOpen_Other_MenuBarItem_HotKey_Activates_And_Opens () { // Arrange - var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuItem = new MenuItem { Id = "menuItem", Title = "_Item" }; + var menu = new Menu ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItem { Id = "menuBarItem", Title = "_New" }; var menuBarItemPopover = new PopoverMenu (); menuBarItem.PopoverMenu = menuBarItemPopover; menuBarItemPopover.Root = menu; - var menuItem2 = new MenuItemv2 { Id = "menuItem2", Title = "_Copy" }; - var menu2 = new Menuv2 ([menuItem2]) { Id = "menu2" }; - var menuBarItem2 = new MenuBarItemv2 () { Id = "menuBarItem2", Title = "_Edit" }; + var menuItem2 = new MenuItem { Id = "menuItem2", Title = "_Copy" }; + var menu2 = new Menu ([menuItem2]) { Id = "menu2" }; + var menuBarItem2 = new MenuBarItem () { Id = "menuBarItem2", Title = "_Edit" }; var menuBarItemPopover2 = new PopoverMenu () { Id = "menuBarItemPopover2" }; menuBarItem2.PopoverMenu = menuBarItemPopover2; menuBarItemPopover2.Root = menu2; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; + var menuBar = new MenuBar () { Id = "menuBar" }; menuBar.Add (menuBarItem); menuBar.Add (menuBarItem2); @@ -346,13 +346,13 @@ public class MenuBarTests () public void Mouse_Enter_Activates_But_Does_Not_Open () { // Arrange - var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuItem = new MenuItem { Id = "menuItem", Title = "_Item" }; + var menu = new Menu ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItem { Id = "menuBarItem", Title = "_New" }; var menuBarItemPopover = new PopoverMenu (); menuBarItem.PopoverMenu = menuBarItemPopover; menuBarItemPopover.Root = menu; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; + var menuBar = new MenuBar () { Id = "menuBar" }; menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); @@ -383,7 +383,7 @@ public class MenuBarTests () { // Arrange var top = new Toplevel () { App = ApplicationImpl.Instance }; - MenuBarv2 menuBar = new MenuBarv2 () { App = ApplicationImpl.Instance }; + MenuBar menuBar = new MenuBar () { App = ApplicationImpl.Instance }; menuBar.EnableForDesign (ref top); top.Add (menuBar); @@ -414,13 +414,13 @@ public class MenuBarTests () { // Arrange // Arrange - var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuItem = new MenuItem { Id = "menuItem", Title = "_Item" }; + var menu = new Menu ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItem { Id = "menuBarItem", Title = "_New" }; var menuBarItemPopover = new PopoverMenu (); menuBarItem.PopoverMenu = menuBarItemPopover; menuBarItemPopover.Root = menu; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; + var menuBar = new MenuBar () { Id = "menuBar" }; menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); @@ -461,13 +461,13 @@ public class MenuBarTests () { // Arrange int action = 0; - var menuItem = new MenuItemv2 { Title = "_Item", Action = () => action++ }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Title = "_New" }; + var menuItem = new MenuItem { Title = "_Item", Action = () => action++ }; + var menu = new Menu ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItem { Title = "_New" }; var menuBarItemPopover = new PopoverMenu (); menuBarItem.PopoverMenu = menuBarItemPopover; menuBarItemPopover.Root = menu; - var menuBar = new MenuBarv2 (); + var menuBar = new MenuBar (); menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); @@ -503,13 +503,13 @@ public class MenuBarTests () public void Disabled_MenuBar_Is_Not_Activated () { // Arrange - var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuItem = new MenuItem { Id = "menuItem", Title = "_Item" }; + var menu = new Menu ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItem { Id = "menuBarItem", Title = "_New" }; var menuBarItemPopover = new PopoverMenu (); menuBarItem.PopoverMenu = menuBarItemPopover; menuBarItemPopover.Root = menu; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; + var menuBar = new MenuBar () { Id = "menuBar" }; menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); @@ -534,13 +534,13 @@ public class MenuBarTests () public void MenuBarItem_Disabled_MenuBarItem_HotKey_No_Activate_Or_Open () { // Arrange - var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuItem = new MenuItem { Id = "menuItem", Title = "_Item" }; + var menu = new Menu ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItem { Id = "menuBarItem", Title = "_New" }; var menuBarItemPopover = new PopoverMenu (); menuBarItem.PopoverMenu = menuBarItemPopover; menuBarItemPopover.Root = menu; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; + var menuBar = new MenuBar () { Id = "menuBar" }; menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); @@ -566,13 +566,13 @@ public class MenuBarTests () public void MenuBarItem_Disabled_Popover_Is_Activated () { // Arrange - var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuItem = new MenuItem { Id = "menuItem", Title = "_Item" }; + var menu = new Menu ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItem { Id = "menuBarItem", Title = "_New" }; var menuBarItemPopover = new PopoverMenu (); menuBarItem.PopoverMenu = menuBarItemPopover; menuBarItemPopover.Root = menu; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; + var menuBar = new MenuBar () { Id = "menuBar" }; menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); @@ -604,13 +604,13 @@ public class MenuBarTests () public void Update_MenuBarItem_HotKey_Works () { // Arrange - var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuItem = new MenuItem { Id = "menuItem", Title = "_Item" }; + var menu = new Menu ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItem { Id = "menuBarItem", Title = "_New" }; var menuBarItemPopover = new PopoverMenu (); menuBarItem.PopoverMenu = menuBarItemPopover; menuBarItemPopover.Root = menu; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; + var menuBar = new MenuBar () { Id = "menuBar" }; menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); @@ -653,13 +653,13 @@ public class MenuBarTests () public void Visible_False_HotKey_Does_Not_Activate () { // Arrange - var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuItem = new MenuItem { Id = "menuItem", Title = "_Item" }; + var menu = new Menu ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItem { Id = "menuBarItem", Title = "_New" }; var menuBarItemPopover = new PopoverMenu (); menuBarItem.PopoverMenu = menuBarItemPopover; menuBarItemPopover.Root = menu; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; + var menuBar = new MenuBar () { Id = "menuBar" }; menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); @@ -688,19 +688,19 @@ public class MenuBarTests () { // Arrange int action = 0; - var menuItem = new MenuItemv2 () + var menuItem = new MenuItem () { Id = "menuItem", Title = "_Item", Key = Key.F1, Action = () => action++ }; - var menu = new Menuv2 ([menuItem]) { Id = "menu" }; - var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menu = new Menu ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItem { Id = "menuBarItem", Title = "_New" }; var menuBarItemPopover = new PopoverMenu (); menuBarItem.PopoverMenu = menuBarItemPopover; menuBarItemPopover.Root = menu; - var menuBar = new MenuBarv2 () { Id = "menuBar" }; + var menuBar = new MenuBar () { Id = "menuBar" }; menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); diff --git a/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs b/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs deleted file mode 100644 index c51389bbc..000000000 --- a/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs +++ /dev/null @@ -1,3410 +0,0 @@ -using UnitTests; -using Xunit.Abstractions; - -namespace UnitTests.ViewsTests; - -#pragma warning disable CS0618 // Type or member is obsolete -public class MenuBarv1Tests (ITestOutputHelper output) -{ - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void AddMenuBarItem_RemoveMenuItem_Dynamically () - { - var menuBar = new MenuBar (); - var menuBarItem = new MenuBarItem { Title = "_New" }; - var action = ""; - var menuItem = new MenuItem { Title = "_Item", Action = () => action = "I", Parent = menuBarItem }; - Assert.Equal ("n", menuBarItem.HotKey); - Assert.Equal ("i", menuItem.HotKey); - Assert.Empty (menuBar.Menus); - menuBarItem.AddMenuBarItem (menuBar, menuItem); - menuBar.Menus = [menuBarItem]; - Assert.Single (menuBar.Menus); - Assert.Single (menuBar.Menus [0].Children!); - - Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.I, out _)); - - var top = new Toplevel (); - top.Add (menuBar); - Application.Begin (top); - - top.NewKeyDownEvent (Key.N.WithAlt); - AutoInitShutdownAttribute.RunIteration (); - - Assert.True (menuBar.IsMenuOpen); - Assert.Equal ("", action); - - top.NewKeyDownEvent (Key.I); - AutoInitShutdownAttribute.RunIteration (); - - Assert.False (menuBar.IsMenuOpen); - Assert.Equal ("I", action); - - menuItem.RemoveMenuItem (); - Assert.Single (menuBar.Menus); - Assert.Null (menuBar.Menus [0].Children); - Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.I, out _)); - - menuBarItem.RemoveMenuItem (); - Assert.Empty (menuBar.Menus); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); - - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void AllowNullChecked_Get_Set () - { - var mi = new MenuItem ("Check this out 你", "", null) { CheckType = MenuItemCheckStyle.Checked }; - mi.Action = mi.ToggleChecked; - - var menu = new MenuBar - { - Menus = - [ - new ("Nullable Checked", new [] { mi }) - ] - }; - - //new CheckBox (); - Toplevel top = new (); - top.Add (menu); - Application.Begin (top); - - Assert.False (mi.Checked); - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); - AutoInitShutdownAttribute.RunIteration (); - Assert.True (mi.Checked); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - - Assert.True ( - menu._openMenu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } - ) - ); - AutoInitShutdownAttribute.RunIteration (); - Assert.False (mi.Checked); - - mi.AllowNullChecked = true; - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); - AutoInitShutdownAttribute.RunIteration (); - Assert.Null (mi.Checked); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @$" - Nullable Checked -┌──────────────────────┐ -│ {Glyphs.CheckStateNone} Check this out 你 │ -└──────────────────────┘", - output - ); - - Assert.True ( - menu._openMenu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } - ) - ); - AutoInitShutdownAttribute.RunIteration (); - Assert.True (mi.Checked); - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); - AutoInitShutdownAttribute.RunIteration (); - Assert.False (mi.Checked); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - - Assert.True ( - menu._openMenu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } - ) - ); - AutoInitShutdownAttribute.RunIteration (); - Assert.Null (mi.Checked); - - mi.AllowNullChecked = false; - Assert.False (mi.Checked); - - mi.CheckType = MenuItemCheckStyle.NoCheck; - Assert.Throws (mi.ToggleChecked); - - mi.CheckType = MenuItemCheckStyle.Radio; - Assert.Throws (mi.ToggleChecked); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void CanExecute_False_Does_Not_Throws () - { - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] - { - new ("New", "", null, () => false), - null, - new ("Quit", "", null) - }) - ] - }; - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void CanExecute_HotKey () - { - Window win = null; - - var menu = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ("_New", "", New, CanExecuteNew), - new ( - "_Close", - "", - Close, - CanExecuteClose - ) - } - ) - ] - }; - Toplevel top = new (); - top.Add (menu); - - bool CanExecuteNew () { return win == null; } - - void New () { win = new (); } - - bool CanExecuteClose () { return win != null; } - - void Close () { win = null; } - - Application.Begin (top); - - Assert.Null (win); - Assert.True (CanExecuteNew ()); - Assert.False (CanExecuteClose ()); - - Assert.True (top.NewKeyDownEvent (Key.F.WithAlt)); - Assert.True (top.NewKeyDownEvent (Key.N.WithAlt)); - AutoInitShutdownAttribute.RunIteration (); - Assert.NotNull (win); - Assert.False (CanExecuteNew ()); - Assert.True (CanExecuteClose ()); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void Click_Another_View_Close_An_Open_Menu () - { - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }) - ] - }; - - var btnClicked = false; - var btn = new Button { Y = 4, Text = "Test" }; - btn.Accepting += (s, e) => btnClicked = true; - var top = new Toplevel (); - top.Add (menu, btn); - Application.Begin (top); - - Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 4), Flags = MouseFlags.Button1Clicked }); - Assert.True (btnClicked); - top.Dispose (); - } - - // TODO: Lots of tests in here really test Menu and MenuItem - Move them to MenuTests.cs - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - public void Constructors_Defaults () - { - var menuBar = new MenuBar (); - Assert.Equal (KeyCode.F9, menuBar.Key); - var menu = new Menu { Host = menuBar, X = 0, Y = 0, BarItems = new () }; - Assert.False (menu.HasScheme); - Assert.False (menu.IsInitialized); - menu.BeginInit (); - menu.EndInit (); - Assert.True (menu.CanFocus); - Assert.False (menu.WantContinuousButtonPressed); - Assert.Equal (LineStyle.Single, menuBar.MenusBorderStyle); - - menuBar = new (); - Assert.Equal (0, menuBar.X); - Assert.Equal (0, menuBar.Y); - Assert.IsType (menuBar.Width); - Assert.Equal (1, menuBar.Height); - Assert.Empty (menuBar.Menus); - Assert.True (menuBar.WantMousePositionReports); - Assert.False (menuBar.IsMenuOpen); - - menuBar = new () { Menus = [] }; - Assert.Equal (0, menuBar.X); - Assert.Equal (0, menuBar.Y); - Assert.IsType (menuBar.Width); - Assert.Equal (1, menuBar.Height); - Assert.Empty (menuBar.Menus); - Assert.True (menuBar.WantMousePositionReports); - Assert.False (menuBar.IsMenuOpen); - - var menuBarItem = new MenuBarItem (); - Assert.Equal ("", menuBarItem.Title); - Assert.Null (menuBarItem.Parent); - Assert.Empty (menuBarItem.Children); - - menuBarItem = new (new MenuBarItem [] { }); - Assert.Equal ("", menuBarItem.Title); - Assert.Null (menuBarItem.Parent); - Assert.Empty (menuBarItem.Children); - - menuBarItem = new ("Test", new MenuBarItem [] { }); - Assert.Equal ("Test", menuBarItem.Title); - Assert.Null (menuBarItem.Parent); - Assert.Empty (menuBarItem.Children); - - menuBarItem = new ("Test", new List ()); - Assert.Equal ("Test", menuBarItem.Title); - Assert.Null (menuBarItem.Parent); - Assert.Empty (menuBarItem.Children); - - menuBarItem = new ("Test", "Help", null); - Assert.Equal ("Test", menuBarItem.Title); - Assert.Equal ("Help", menuBarItem.Help); - Assert.Null (menuBarItem.Action); - Assert.Null (menuBarItem.CanExecute); - Assert.Null (menuBarItem.Parent); - Assert.Equal (Key.Empty, menuBarItem.ShortcutKey); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void Disabled_MenuBar_Is_Never_Opened () - { - Toplevel top = new (); - - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }) - ] - }; - top.Add (menu); - Application.Begin (top); - Assert.True (menu.Enabled); - menu.OpenMenu (); - Assert.True (menu.IsMenuOpen); - - menu.Enabled = false; - menu.CloseAllMenus (); - menu.OpenMenu (); - Assert.False (menu.IsMenuOpen); - top.Dispose (); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void Disabled_MenuItem_Is_Never_Selected () - { - var menu = new MenuBar - { - Menus = - [ - new ( - "Menu", - new MenuItem [] - { - new ("Enabled 1", "", null), - new ("Disabled", "", null, () => false), - null, - new ("Enabled 2", "", null) - } - ) - ] - }; - - Toplevel top = new (); - top.Add (menu); - Application.Begin (top); - - Attribute [] attributes = - { - // 0 - menu.GetAttributeForRole(VisualRole.Normal), - - // 1 - menu.GetAttributeForRole(VisualRole.Focus), - - // 2 - menu.GetAttributeForRole(VisualRole.Disabled) - }; - - DriverAssert.AssertDriverAttributesAre ( - @" -00000000000000", - output, - Application.Driver, - attributes - ); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - top.Draw (); - - DriverAssert.AssertDriverAttributesAre ( - @" -11111100000000 -00000000000000 -01111111111110 -02222222222220 -00000000000000 -00000000000000 -00000000000000", - output, - Application.Driver, - attributes - ); - - Assert.True ( - top.SubViews.ElementAt (1) - .NewMouseEvent ( - new () { Position = new (0, 2), Flags = MouseFlags.Button1Clicked, View = top.SubViews.ElementAt (1) } - ) - ); - top.SubViews.ElementAt (1).Layout (); - top.SubViews.ElementAt (1).Draw (); - - DriverAssert.AssertDriverAttributesAre ( - @" -11111100000000 -00000000000000 -01111111111110 -02222222222220 -00000000000000 -00000000000000 -00000000000000", - output, - Application.Driver, - attributes - ); - - Assert.True ( - top.SubViews.ElementAt (1) - .NewMouseEvent ( - new () { Position = new (0, 2), Flags = MouseFlags.ReportMousePosition, View = top.SubViews.ElementAt (1) } - ) - ); - top.SubViews.ElementAt (1).Draw (); - - DriverAssert.AssertDriverAttributesAre ( - @" -11111100000000 -00000000000000 -01111111111110 -02222222222220 -00000000000000 -00000000000000 -00000000000000", - output, - Application.Driver, - attributes - ); - top.Dispose (); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void Draw_A_Menu_Over_A_Dialog () - { - // Override CM - Window.DefaultBorderStyle = LineStyle.Single; - Dialog.DefaultButtonAlignment = Alignment.Center; - Dialog.DefaultBorderStyle = LineStyle.Single; - Dialog.DefaultShadow = ShadowStyle.None; - Button.DefaultShadow = ShadowStyle.None; - - Toplevel top = new (); - Window win = new (); - top.Add (win); - SessionToken rsTop = Application.Begin (top); - Application.Driver!.SetScreenSize (40, 15); - - Assert.Equal (new (0, 0, 40, 15), win.Frame); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - List items = new () - { - "New", - "Open", - "Close", - "Save", - "Save As", - "Delete" - }; - Dialog dialog = new () { X = 2, Y = 2, Width = 15, Height = 4 }; - MenuBar menu = new () { X = Pos.Center (), Width = 10 }; - - menu.Menus = new MenuBarItem [] - { - new ( - "File", - new MenuItem [] - { - new ( - items [0], - "Create a new file", - () => ChangeMenuTitle ("New"), - null, - null, - KeyCode.CtrlMask | KeyCode.N - ), - new ( - items [1], - "Open a file", - () => ChangeMenuTitle ("Open"), - null, - null, - KeyCode.CtrlMask | KeyCode.O - ), - new ( - items [2], - "Close a file", - () => ChangeMenuTitle ("Close"), - null, - null, - KeyCode.CtrlMask | KeyCode.C - ), - new ( - items [3], - "Save a file", - () => ChangeMenuTitle ("Save"), - null, - null, - KeyCode.CtrlMask | KeyCode.S - ), - new ( - items [4], - "Save a file as", - () => ChangeMenuTitle ("Save As"), - null, - null, - KeyCode.CtrlMask | KeyCode.A - ), - new ( - items [5], - "Delete a file", - () => ChangeMenuTitle ("Delete"), - null, - null, - KeyCode.CtrlMask | KeyCode.A - ) - } - ) - }; - dialog.Add (menu); - - void ChangeMenuTitle (string title) - { - menu.Menus [0].Title = title; - menu.SetNeedsDraw (); - } - - SessionToken rsDialog = Application.Begin (dialog); - AutoInitShutdownAttribute.RunIteration (); - - Assert.Equal (new (2, 2, 15, 4), dialog.Frame); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ │ -│ ┌─────────────┐ │ -│ │ File │ │ -│ │ │ │ -│ └─────────────┘ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.Equal ("File", menu.Menus [0].Title); - menu.OpenMenu (); - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ │ -│ ┌─────────────┐ │ -│ │ File │ │ -│ │ ┌──────────────────────────────────┐ -│ └─│ New Create a new file Ctrl+N │ -│ │ Open Open a file Ctrl+O │ -│ │ Close Close a file Ctrl+C │ -│ │ Save Save a file Ctrl+S │ -│ │ Save As Save a file as Ctrl+A │ -│ │ Delete Delete a file Ctrl+A │ -│ └──────────────────────────────────┘ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5), Flags = MouseFlags.Button1Clicked }); - - // Need to fool MainLoop into thinking it's running - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (items [0], menu.Menus [0].Title); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ │ -│ ┌─────────────┐ │ -│ │ New │ │ -│ │ │ │ -│ └─────────────┘ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - for (var i = 0; i < items.Count; i++) - { - menu.OpenMenu (); - - Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5 + i), Flags = MouseFlags.Button1Clicked }); - - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (items [i], menu.Menus [0].Title); - } - - Application.Driver!.SetScreenSize (20, 15); - menu.OpenMenu (); - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────┐ -│ │ -│ ┌─────────────┐ │ -│ │ Delete │ │ -│ │ ┌─────────────── -│ └─│ New Create -│ │ Open O -│ │ Close Cl -│ │ Save S -│ │ Save As Save -│ │ Delete Del -│ └─────────────── -│ │ -│ │ -└──────────────────┘", - output - ); - - Application.End (rsDialog); - Application.End (rsTop); - top.Dispose (); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void Draw_A_Menu_Over_A_Top_Dialog () - { - Application.Driver!.SetScreenSize (40, 15); - - // Override CM - Window.DefaultBorderStyle = LineStyle.Single; - Dialog.DefaultButtonAlignment = Alignment.Center; - Dialog.DefaultBorderStyle = LineStyle.Single; - Dialog.DefaultShadow = ShadowStyle.None; - Button.DefaultShadow = ShadowStyle.None; - - Assert.Equal (new (0, 0, 40, 15), Application.TopRunnable!.GetClip ()!.GetBounds ()); - DriverAssert.AssertDriverContentsWithFrameAre (@"", output); - - List items = new () - { - "New", - "Open", - "Close", - "Save", - "Save As", - "Delete" - }; - var dialog = new Dialog { X = 2, Y = 2, Width = 15, Height = 4 }; - var menu = new MenuBar { X = Pos.Center (), Width = 10 }; - - menu.Menus = new MenuBarItem [] - { - new ( - "File", - new MenuItem [] - { - new ( - items [0], - "Create a new file", - () => ChangeMenuTitle ("New"), - null, - null, - KeyCode.CtrlMask | KeyCode.N - ), - new ( - items [1], - "Open a file", - () => ChangeMenuTitle ("Open"), - null, - null, - KeyCode.CtrlMask | KeyCode.O - ), - new ( - items [2], - "Close a file", - () => ChangeMenuTitle ("Close"), - null, - null, - KeyCode.CtrlMask | KeyCode.C - ), - new ( - items [3], - "Save a file", - () => ChangeMenuTitle ("Save"), - null, - null, - KeyCode.CtrlMask | KeyCode.S - ), - new ( - items [4], - "Save a file as", - () => ChangeMenuTitle ("Save As"), - null, - null, - KeyCode.CtrlMask | KeyCode.A - ), - new ( - items [5], - "Delete a file", - () => ChangeMenuTitle ("Delete"), - null, - null, - KeyCode.CtrlMask | KeyCode.A - ) - } - ) - }; - dialog.Add (menu); - - void ChangeMenuTitle (string title) - { - menu.Menus [0].Title = title; - menu.SetNeedsDraw (); - } - - SessionToken rs = Application.Begin (dialog); - AutoInitShutdownAttribute.RunIteration (); - - Assert.Equal (new (2, 2, 15, 4), dialog.Frame); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌─────────────┐ - │ File │ - │ │ - └─────────────┘", - output - ); - - Assert.Equal ("File", menu.Menus [0].Title); - menu.OpenMenu (); - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌─────────────┐ - │ File │ - │ ┌──────────────────────────────────┐ - └─│ New Create a new file Ctrl+N │ - │ Open Open a file Ctrl+O │ - │ Close Close a file Ctrl+C │ - │ Save Save a file Ctrl+S │ - │ Save As Save a file as Ctrl+A │ - │ Delete Delete a file Ctrl+A │ - └──────────────────────────────────┘", - output - ); - - Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5), Flags = MouseFlags.Button1Clicked }); - - // Need to fool MainLoop into thinking it's running - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (items [0], menu.Menus [0].Title); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌─────────────┐ - │ New │ - │ │ - └─────────────┘", - output - ); - - for (var i = 1; i < items.Count; i++) - { - menu.OpenMenu (); - - Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5 + i), Flags = MouseFlags.Button1Clicked }); - - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (items [i], menu.Menus [0].Title); - } - - Application.Driver!.SetScreenSize (20, 15); - menu.OpenMenu (); - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌─────────────┐ - │ Delete │ - │ ┌─────────────── - └─│ New Create - │ Open O - │ Close Cl - │ Save S - │ Save As Save - │ Delete Del - └───────────────", - output - ); - - Application.End (rs); - dialog.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void DrawFrame_With_Negative_Positions () - { - var menu = new MenuBar - { - X = -1, - Y = -1, - Menus = - [ - new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) - ] - }; - menu.Layout (); - - Assert.Equal (new (-1, -1), new Point (menu.Frame.X, menu.Frame.Y)); - - Toplevel top = new (); - Application.Begin (top); - menu.OpenMenu (); - AutoInitShutdownAttribute.RunIteration (); - - var expected = @" -──────┐ - One │ - Two │ -──────┘ -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (0, 0, 7, 4), pos); - - menu.CloseAllMenus (); - menu.Frame = new (-1, -2, menu.Frame.Width, menu.Frame.Height); - menu.OpenMenu (); - AutoInitShutdownAttribute.RunIteration (); - - expected = @" - One │ - Two │ -──────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 7, 3), pos); - - menu.CloseAllMenus (); - menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); - Application.Driver!.SetScreenSize (7, 5); - menu.OpenMenu (); - AutoInitShutdownAttribute.RunIteration (); - - expected = @" -┌────── -│ One -│ Two -└────── -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (0, 1, 7, 4), pos); - - menu.CloseAllMenus (); - menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); - Application.Driver!.SetScreenSize (7, 3); - menu.OpenMenu (); - AutoInitShutdownAttribute.RunIteration (); - - expected = @" -┌────── -│ One -│ Two -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (0, 0, 7, 3), pos); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void DrawFrame_With_Negative_Positions_Disabled_Border () - { - var menu = new MenuBar - { - X = -2, - Y = -1, - MenusBorderStyle = LineStyle.None, - Menus = - [ - new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) - ] - }; - menu.Layout (); - - Assert.Equal (new (-2, -1), new Point (menu.Frame.X, menu.Frame.Y)); - - Toplevel top = new (); - Application.Begin (top); - menu.OpenMenu (); - AutoInitShutdownAttribute.RunIteration (); - - var expected = @" -ne -wo -"; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - menu.CloseAllMenus (); - menu.Frame = new (-2, -2, menu.Frame.Width, menu.Frame.Height); - menu.OpenMenu (); - AutoInitShutdownAttribute.RunIteration (); - - expected = @" -wo -"; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - menu.CloseAllMenus (); - menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); - Application.Driver!.SetScreenSize (3, 2); - menu.OpenMenu (); - AutoInitShutdownAttribute.RunIteration (); - - expected = @" - On - Tw -"; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - menu.CloseAllMenus (); - menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); - Application.Driver!.SetScreenSize (3, 1); - menu.OpenMenu (); - AutoInitShutdownAttribute.RunIteration (); - - expected = @" - On -"; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void DrawFrame_With_Positive_Positions () - { - var menu = new MenuBar - { - Menus = - [ - new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) - ] - }; - - Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); - - Toplevel top = new (); - Application.Begin (top); - menu.OpenMenu (); - AutoInitShutdownAttribute.RunIteration (); - - var expected = @" -┌──────┐ -│ One │ -│ Two │ -└──────┘ -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (0, 1, 8, 4), pos); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void DrawFrame_With_Positive_Positions_Disabled_Border () - { - var menu = new MenuBar - { - MenusBorderStyle = LineStyle.None, - Menus = - [ - new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) - ] - }; - - Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); - - Toplevel top = new (); - Application.Begin (top); - menu.OpenMenu (); - AutoInitShutdownAttribute.RunIteration (); - - var expected = @" - One - Two -"; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - public void Exceptions () - { - Assert.Throws (() => new MenuBarItem ("Test", (MenuItem [])null)); - Assert.Throws (() => new MenuBarItem ("Test", (List)null)); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void HotKey_MenuBar_OnKeyDown_OnKeyUp_ProcessKeyPressed () - { - var newAction = false; - var copyAction = false; - - var menu = new MenuBar - { - Menus = - [ - new ("_File", new MenuItem [] { new ("_New", "", () => newAction = true) }), - new ( - "_Edit", - new MenuItem [] { new ("_Copy", "", () => copyAction = true) } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.False (newAction); - Assert.False (copyAction); - -#if SUPPORT_ALT_TO_ACTIVATE_MENU - Assert.False (Application.TopRunnable.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); - Assert.False (Application.TopRunnable.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); - Assert.True (Application.TopRunnable.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); - Assert.True (menu.IsMenuOpen); - Application.TopRunnable.Draw (); - - string expected = @" - File Edit -"; - - var pos = DriverAsserts.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 11, 1), pos); - - Assert.True (Application.TopRunnable.ProcessKeyDown (new KeyEventArgs (Key.N))); - AutoInitShutdownAttribute.RunIteration (); - Assert.False (newAction); // not yet, hot keys don't work if the item is not visible - - Assert.True (Application.TopRunnable.ProcessKeyDown (new KeyEventArgs (Key.F))); - AutoInitShutdownAttribute.RunIteration (); - Assert.True (Application.TopRunnable.ProcessKeyDown (new KeyEventArgs (Key.N))); - AutoInitShutdownAttribute.RunIteration (); - Assert.True (newAction); - Application.TopRunnable.Draw (); - - expected = @" - File Edit -"; - - Assert.False (Application.TopRunnable.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); - Assert.True (Application.TopRunnable.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); - Assert.True (Application.TopRunnable.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); - Assert.True (menu.IsMenuOpen); - Application.TopRunnable.Draw (); - - expected = @" - File Edit -"; - - pos = DriverAsserts.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 11, 1), pos); - - Assert.True (Application.TopRunnable.ProcessKeyDown (new KeyEventArgs (Key.CursorRight))); - Assert.True (Application.TopRunnable.ProcessKeyDown (new KeyEventArgs (Key.C))); - AutoInitShutdownAttribute.RunIteration (); - Assert.True (copyAction); -#endif - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void HotKey_MenuBar_ProcessKeyPressed_Menu_ProcessKey () - { - var newAction = false; - var copyAction = false; - - // Define the expected menu - var expectedMenu = new ExpectedMenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ( - "Edit", - new MenuItem [] { new ("Copy", "", null) } - ) - ] - }; - - // The real menu - var menu = new MenuBar - { - Menus = - [ - new ( - "_" + expectedMenu.Menus [0].Title, - new MenuItem [] - { - new ( - "_" + expectedMenu.Menus [0].Children [0].Title, - "", - () => newAction = true - ) - } - ), - new ( - "_" + expectedMenu.Menus [1].Title, - new MenuItem [] - { - new ( - "_" - + expectedMenu.Menus [1] - .Children [0] - .Title, - "", - () => copyAction = true - ) - } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.False (newAction); - Assert.False (copyAction); - - Assert.True (menu.NewKeyDownEvent (Key.F.WithAlt)); - Assert.True (menu.IsMenuOpen); - Application.TopRunnable.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - Assert.True (Application.TopRunnable.SubViews.ElementAt (1).NewKeyDownEvent (Key.N)); - AutoInitShutdownAttribute.RunIteration (); - Assert.True (newAction); - - Assert.True (menu.NewKeyDownEvent (Key.E.WithAlt)); - Assert.True (menu.IsMenuOpen); - Application.TopRunnable.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); - - Assert.True (Application.TopRunnable.SubViews.ElementAt (1).NewKeyDownEvent (Key.C)); - AutoInitShutdownAttribute.RunIteration (); - Assert.True (copyAction); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void Key_Open_And_Close_The_MenuBar () - { - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }) - ] - }; - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.True (top.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - Assert.True (top.NewKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - - menu.Key = Key.F10.WithShift; - Assert.False (top.NewKeyDownEvent (Key.F9)); - Assert.False (menu.IsMenuOpen); - - Assert.True (top.NewKeyDownEvent (Key.F10.WithShift)); - Assert.True (menu.IsMenuOpen); - Assert.True (top.NewKeyDownEvent (Key.F10.WithShift)); - Assert.False (menu.IsMenuOpen); - top.Dispose (); - } - - [Theory (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - [InlineData ("_File", "_New", "", KeyCode.Space | KeyCode.CtrlMask)] - [InlineData ("Closed", "None", "", KeyCode.Space | KeyCode.CtrlMask, KeyCode.Space | KeyCode.CtrlMask)] - [InlineData ("_File", "_New", "", KeyCode.F9)] - [InlineData ("Closed", "None", "", KeyCode.F9, KeyCode.F9)] - [InlineData ("_File", "_Open", "", KeyCode.F9, KeyCode.CursorDown)] - [InlineData ("_File", "_Save", "", KeyCode.F9, KeyCode.CursorDown, KeyCode.CursorDown)] - [InlineData ("_File", "_Quit", "", KeyCode.F9, KeyCode.CursorDown, KeyCode.CursorDown, KeyCode.CursorDown)] - [InlineData ( - "_File", - "_New", - "", - KeyCode.F9, - KeyCode.CursorDown, - KeyCode.CursorDown, - KeyCode.CursorDown, - KeyCode.CursorDown - )] - [InlineData ("_File", "_New", "", KeyCode.F9, KeyCode.CursorDown, KeyCode.CursorUp)] - [InlineData ("_File", "_Quit", "", KeyCode.F9, KeyCode.CursorUp)] - [InlineData ("_File", "_New", "", KeyCode.F9, KeyCode.CursorUp, KeyCode.CursorDown)] - [InlineData ("Closed", "None", "Open", KeyCode.F9, KeyCode.CursorDown, KeyCode.Enter)] - [InlineData ("_Edit", "_Copy", "", KeyCode.F9, KeyCode.CursorRight)] - [InlineData ("_About", "_About", "", KeyCode.F9, KeyCode.CursorLeft)] - [InlineData ("_Edit", "_Copy", "", KeyCode.F9, KeyCode.CursorLeft, KeyCode.CursorLeft)] - [InlineData ("_Edit", "_Select All", "", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorUp)] - [InlineData ("_File", "_New", "", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorDown, KeyCode.CursorLeft)] - [InlineData ("_About", "_About", "", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorRight)] - [InlineData ("Closed", "None", "New", KeyCode.F9, KeyCode.Enter)] - [InlineData ("Closed", "None", "Quit", KeyCode.F9, KeyCode.CursorUp, KeyCode.Enter)] - [InlineData ("Closed", "None", "Copy", KeyCode.F9, KeyCode.CursorRight, KeyCode.Enter)] - [InlineData ( - "Closed", - "None", - "Find", - KeyCode.F9, - KeyCode.CursorRight, - KeyCode.CursorUp, - KeyCode.CursorUp, - KeyCode.Enter - )] - [InlineData ( - "Closed", - "None", - "Replace", - KeyCode.F9, - KeyCode.CursorRight, - KeyCode.CursorUp, - KeyCode.CursorUp, - KeyCode.CursorDown, - KeyCode.Enter - )] - [InlineData ( - "_Edit", - "F_ind", - "", - KeyCode.F9, - KeyCode.CursorRight, - KeyCode.CursorUp, - KeyCode.CursorUp, - KeyCode.CursorLeft, - KeyCode.Enter - )] - [InlineData ("Closed", "None", "About", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorRight, KeyCode.Enter)] - - //// Hotkeys - [InlineData ("_File", "_New", "", KeyCode.AltMask | KeyCode.F)] - [InlineData ("Closed", "None", "", KeyCode.AltMask | KeyCode.ShiftMask | KeyCode.F)] - [InlineData ("Closed", "None", "", KeyCode.AltMask | KeyCode.F, KeyCode.Esc)] - [InlineData ("Closed", "None", "", KeyCode.AltMask | KeyCode.F, KeyCode.AltMask | KeyCode.F)] - [InlineData ("Closed", "None", "Open", KeyCode.AltMask | KeyCode.F, KeyCode.O)] - [InlineData ("_File", "_New", "", KeyCode.AltMask | KeyCode.F, KeyCode.ShiftMask | KeyCode.O)] - [InlineData ("Closed", "None", "Open", KeyCode.AltMask | KeyCode.F, KeyCode.AltMask | KeyCode.O)] - [InlineData ("_Edit", "_Copy", "", KeyCode.AltMask | KeyCode.E)] - [InlineData ("_Edit", "F_ind", "", KeyCode.AltMask | KeyCode.E, KeyCode.F)] - [InlineData ("_Edit", "F_ind", "", KeyCode.AltMask | KeyCode.E, KeyCode.AltMask | KeyCode.F)] - [InlineData ("Closed", "None", "Replace", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.R)] - [InlineData ("Closed", "None", "Copy", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.C)] - [InlineData ("_Edit", "_1st", "", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3)] - [InlineData ("Closed", "None", "1", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.D1)] - [InlineData ("Closed", "None", "1", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.Enter)] - [InlineData ("Closed", "None", "2", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.D2)] - [InlineData ("_Edit", "_5th", "", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.D4)] - [InlineData ("Closed", "None", "5", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D4, KeyCode.D5)] - [InlineData ("Closed", "None", "About", KeyCode.AltMask | KeyCode.A)] - public void KeyBindings_Navigation_Commands ( - string expectedBarTitle, - string expectedItemTitle, - string expectedAction, - params KeyCode [] keys - ) - { - var miAction = ""; - MenuItem mbiCurrent = null; - MenuItem miCurrent = null; - - var menu = new MenuBar (); - - Func fn = s => - { - miAction = s as string; - - return true; - }; - menu.EnableForDesign (ref fn); - - menu.Key = KeyCode.F9; - menu.MenuOpening += (s, e) => mbiCurrent = e.CurrentMenu; - menu.MenuOpened += (s, e) => { miCurrent = e.MenuItem; }; - - menu.MenuClosing += (s, e) => - { - mbiCurrent = null; - miCurrent = null; - }; - menu.UseKeysUpDownAsKeysLeftRight = true; - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - foreach (Key key in keys) - { - top.NewKeyDownEvent (key); - AutoInitShutdownAttribute.RunIteration (); - } - - Assert.Equal (expectedBarTitle, mbiCurrent != null ? mbiCurrent.Title : "Closed"); - Assert.Equal (expectedItemTitle, miCurrent != null ? miCurrent.Title : "None"); - Assert.Equal (expectedAction, miAction); - top.Dispose (); - } - - [Theory (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - [InlineData ("New", KeyCode.CtrlMask | KeyCode.N)] - [InlineData ("Quit", KeyCode.CtrlMask | KeyCode.Q)] - [InlineData ("Copy", KeyCode.CtrlMask | KeyCode.C)] - [InlineData ("Replace", KeyCode.CtrlMask | KeyCode.H)] - [InlineData ("1", KeyCode.F1)] - [InlineData ("5", KeyCode.CtrlMask | KeyCode.D5)] - public void KeyBindings_Shortcut_Commands (string expectedAction, params KeyCode [] keys) - { - var miAction = ""; - MenuItem mbiCurrent = null; - MenuItem miCurrent = null; - - var menu = new MenuBar (); - - bool FnAction (string s) - { - miAction = s; - - return true; - } - - // Declare a variable for the function - Func fnActionVariable = FnAction; - - menu.EnableForDesign (ref fnActionVariable); - - menu.Key = KeyCode.F9; - menu.MenuOpening += (s, e) => mbiCurrent = e.CurrentMenu; - menu.MenuOpened += (s, e) => { miCurrent = e.MenuItem; }; - - menu.MenuClosing += (s, e) => - { - mbiCurrent = null; - miCurrent = null; - }; - menu.UseKeysUpDownAsKeysLeftRight = true; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - foreach (KeyCode key in keys) - { - Assert.True (top.NewKeyDownEvent (new (key))); - AutoInitShutdownAttribute.RunIteration (); - } - - Assert.Equal (expectedAction, miAction); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void Menu_With_Separator () - { - var menu = new MenuBar - { - Menus = - [ - new ( - "File", - new MenuItem [] - { - new ( - "_Open", - "Open a file", - () => { }, - null, - null, - KeyCode.CtrlMask | KeyCode.O - ), - null, - new ("_Quit", "", null) - } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - menu.OpenMenu (); - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - File -┌────────────────────────────┐ -│ Open Open a file Ctrl+O │ -├────────────────────────────┤ -│ Quit │ -└────────────────────────────┘", - output - ); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void Menu_With_Separator_Disabled_Border () - { - var menu = new MenuBar - { - MenusBorderStyle = LineStyle.None, - Menus = - [ - new ( - "File", - new MenuItem [] - { - new ( - "_Open", - "Open a file", - () => { }, - null, - null, - KeyCode.CtrlMask | KeyCode.O - ), - null, - new ("_Quit", "", null) - } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - menu.OpenMenu (); - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - File - Open Open a file Ctrl+O -──────────────────────────── - Quit ", - output - ); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void MenuBar_ButtonPressed_Open_The_Menu_ButtonPressed_Again_Close_The_Menu () - { - // Define the expected menu - var expectedMenu = new ExpectedMenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("Open", "", null) }), - new ( - "Edit", - new MenuItem [] { new ("Copy", "", null) } - ) - ] - }; - - // Test without HotKeys first - var menu = new MenuBar - { - Menus = - [ - new ( - "_" + expectedMenu.Menus [0].Title, - new MenuItem [] { new ("_" + expectedMenu.Menus [0].Children [0].Title, "", null) } - ), - new ( - "_" + expectedMenu.Menus [1].Title, - new MenuItem [] - { - new ( - "_" - + expectedMenu.Menus [1] - .Children [0] - .Title, - "", - null - ) - } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); - Assert.True (menu.IsMenuOpen); - top.Draw (); - - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); - Assert.False (menu.IsMenuOpen); - top.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void MenuBar_In_Window_Without_Other_Views_With_Top_Init () - { - var win = new Window (); - - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ( - "Edit", - new MenuItem [] - { - new MenuBarItem ( - "Delete", - new MenuItem [] - { new ("All", "", null), new ("Selected", "", null) } - ) - } - ) - ] - }; - win.Add (menu); - Toplevel top = new (); - top.Add (win); - Application.Begin (top); - Application.Driver!.SetScreenSize (40, 8); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (win.NewKeyDownEvent (menu.Key)); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│ │ -│ └─────────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│┌───────────┐ │ -│ └─────────┘│ All │ │ -│ │ Selected │ │ -│ └───────────┘ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - top.SetClipToScreen (); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void MenuBar_In_Window_Without_Other_Views_With_Top_Init_With_Parameterless_Run () - { - var win = new Window (); - - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ( - "Edit", - new MenuItem [] - { - new MenuBarItem ( - "Delete", - new MenuItem [] - { new ("All", "", null), new ("Selected", "", null) } - ) - } - ) - ] - }; - win.Add (menu); - Toplevel top = new (); - top.Add (win); - - Application.AddTimeout (TimeSpan.Zero, () => - { - Application.Driver!.SetScreenSize (40, 8); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (win.NewKeyDownEvent (menu.Key)); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│ │ -│ └─────────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│┌───────────┐ │ -│ └─────────┘│ All │ │ -│ │ Selected │ │ -│ └───────────┘ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - top.SetClipToScreen (); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Application.RequestStop (); - - return false; - }); - - Application.Run (top); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void MenuBar_In_Window_Without_Other_Views_Without_Top_Init () - { - var win = new Window (); - - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ( - "Edit", - new MenuItem [] - { - new MenuBarItem ( - "Delete", - new MenuItem [] - { new ("All", "", null), new ("Selected", "", null) } - ) - } - ) - ] - }; - win.Add (menu); - Application.Driver!.SetScreenSize (40, 8); - SessionToken rs = Application.Begin (win); - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (win.NewKeyDownEvent (menu.Key)); - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│ │ -│ └─────────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│┌───────────┐ │ -│ └─────────┘│ All │ │ -│ │ Selected │ │ -│ └───────────┘ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - win.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void MenuBar_In_Window_Without_Other_Views_Without_Top_Init_With_Run_T () - { - Application.Driver!.SetScreenSize (40, 8); - - Application.AddTimeout (TimeSpan.Zero, () => - { - Toplevel top = Application.TopRunnable; - - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (top.NewKeyDownEvent (Key.F9)); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (top.SubViews.ElementAt (0).NewKeyDownEvent (Key.CursorRight)); - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│ │ -│ └─────────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True ( - ((MenuBar)top.SubViews.ElementAt (0))._openMenu.NewKeyDownEvent (Key.CursorRight) - ); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│┌───────────┐ │ -│ └─────────┘│ All │ │ -│ │ Selected │ │ -│ └───────────┘ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True ( - ((MenuBar)top.SubViews.ElementAt (0))._openMenu.NewKeyDownEvent (Key.CursorRight) - ); - top.SetClipToScreen (); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Application.RequestStop (); - - return false; - }); - - Application.Run ().Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void MenuBar_Position_And_Size_With_HotKeys_Is_The_Same_As_Without_HotKeys () - { - // Define the expected menu - var expectedMenu = new ExpectedMenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("12", "", null) }), - new ( - "Edit", - new MenuItem [] { new ("Copy", "", null) } - ) - ] - }; - - // Test without HotKeys first - var menu = new MenuBar - { - Menus = - [ - new ( - expectedMenu.Menus [0].Title, - new MenuItem [] { new (expectedMenu.Menus [0].Children [0].Title, "", null) } - ), - new ( - expectedMenu.Menus [1].Title, - new MenuItem [] - { - new ( - expectedMenu.Menus [1].Children [0].Title, - "", - null - ) - } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - // Open first - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - // Open second - Assert.True (Application.TopRunnable.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorRight)); - Assert.True (menu.IsMenuOpen); - top.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); - - // Close menu - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - top.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); - - top.Remove (menu); - - // Now test WITH HotKeys - menu = new () - { - Menus = - [ - new ( - "_" + expectedMenu.Menus [0].Title, - new MenuItem [] { new ("_" + expectedMenu.Menus [0].Children [0].Title, "", null) } - ), - new ( - "_" + expectedMenu.Menus [1].Title, - new MenuItem [] - { - new ( - "_" + expectedMenu.Menus [1].Children [0].Title, - "", - null - ) - } - ) - ] - }; - - top.Add (menu); - - // Open first - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - top.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - // Open second - Assert.True (top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorRight)); - Assert.True (menu.IsMenuOpen); - top.SetClipToScreen (); - Application.TopRunnable.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); - - // Close menu - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - top.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void MenuBar_Submenus_Alignment_Correct () - { - // Define the expected menu - var expectedMenu = new ExpectedMenuBar - { - Menus = - [ - new ( - "File", - new MenuItem [] - { - new ( - "Really Long Sub Menu", - "", - null - ) - } - ), - new ( - "123", - new MenuItem [] { new ("Copy", "", null) } - ), - new ( - "Format", - new MenuItem [] { new ("Word Wrap", "", null) } - ), - new ( - "Help", - new MenuItem [] { new ("About", "", null) } - ), - new ( - "1", - new MenuItem [] { new ("2", "", null) } - ), - new ( - "3", - new MenuItem [] { new ("2", "", null) } - ), - new ( - "Last one", - new MenuItem [] { new ("Test", "", null) } - ) - ] - }; - - MenuBarItem [] items = new MenuBarItem [expectedMenu.Menus.Length]; - - for (var i = 0; i < expectedMenu.Menus.Length; i++) - { - items [i] = new ( - expectedMenu.Menus [i].Title, - new MenuItem [] { new (expectedMenu.Menus [i].Children [0].Title, "", null) } - ); - } - - var menu = new MenuBar { Menus = items }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - top.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); - - for (var i = 0; i < expectedMenu.Menus.Length; i++) - { - menu.OpenMenu (i); - Assert.True (menu.IsMenuOpen); - top.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (i), output); - } - - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void MenuBar_With_Action_But_Without_MenuItems_Not_Throw () - { - var menu = new MenuBar - { - Menus = - [ - new () { Title = "Test 1", Action = () => { } }, - - new () { Title = "Test 2", Action = () => { } } - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - -#if SUPPORT_ALT_TO_ACTIVATE_MENU - Assert.True ( - Application.OnKeyUp ( - new KeyEventArgs ( - Key.AltMask - ) - ) - ); // changed to true because Alt activates menu bar -#endif - Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); - Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void MenuBarItem_Children_Null_Does_Not_Throw () - { - var menu = new MenuBar - { - Menus = - [ - new ("Test", "", null) - ] - }; - var top = new Toplevel (); - top.Add (menu); - - Exception exception = Record.Exception (() => menu.NewKeyDownEvent (Key.Space)); - Assert.Null (exception); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void MenuOpened_On_Disabled_MenuItem () - { - MenuItem parent = null; - MenuItem miCurrent = null; - Menu mCurrent = null; - - var menu = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new MenuBarItem ( - "_New", - new MenuItem [] - { - new ( - "_New doc", - "Creates new doc.", - null, - () => false - ) - } - ), - null, - new ("_Save", "Saves the file.", null) - } - ) - ] - }; - - menu.MenuOpened += (s, e) => - { - parent = e.Parent; - miCurrent = e.MenuItem; - mCurrent = menu._openMenu; - }; - menu.UseKeysUpDownAsKeysLeftRight = true; - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - // open the menu - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.Equal ("_File", parent.Title); - Assert.Equal ("_File", miCurrent.Parent.Title); - Assert.Equal ("_New", miCurrent.Title); - - Assert.True ( - mCurrent.NewMouseEvent ( - new () { Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = mCurrent } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.Equal ("_File", parent.Title); - Assert.Equal ("_File", miCurrent.Parent.Title); - Assert.Equal ("_New", miCurrent.Title); - - Assert.True ( - mCurrent.NewMouseEvent ( - new () { Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = mCurrent } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.Equal ("_File", parent.Title); - Assert.Equal ("_File", miCurrent.Parent.Title); - Assert.Equal ("_New", miCurrent.Title); - - Assert.True ( - mCurrent.NewMouseEvent ( - new () { Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = mCurrent } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.Equal ("_File", parent.Title); - Assert.Equal ("_File", miCurrent.Parent.Title); - Assert.Equal ("_Save", miCurrent.Title); - - // close the menu - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - Assert.False (menu.IsMenuOpen); - - // open the menu - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - - // The _New doc is enabled but the sub-menu isn't enabled. Is show but can't be selected and executed - Assert.Equal ("_New", parent.Title); - Assert.Equal ("_New", miCurrent.Parent.Title); - Assert.Equal ("_New doc", miCurrent.Title); - - Assert.True (mCurrent.NewKeyDownEvent (Key.CursorDown)); - Assert.True (menu.IsMenuOpen); - Assert.Equal ("_File", parent.Title); - Assert.Equal ("_File", miCurrent.Parent.Title); - Assert.Equal ("_Save", miCurrent.Title); - - Assert.True (mCurrent.NewKeyDownEvent (Key.CursorUp)); - Assert.True (menu.IsMenuOpen); - Assert.Equal ("_File", parent.Title); - Assert.Null (miCurrent); - - // close the menu - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void MenuOpening_MenuOpened_MenuClosing_Events () - { - var miAction = ""; - var isMenuClosed = true; - var cancelClosing = false; - - var menu = new MenuBar - { - Menus = - [ - new ("_File", new MenuItem [] { new ("_New", "Creates new file.", New) }) - ] - }; - - menu.MenuOpening += (s, e) => - { - Assert.Equal ("_File", e.CurrentMenu.Title); - Assert.Equal ("_New", e.CurrentMenu.Children [0].Title); - Assert.Equal ("Creates new file.", e.CurrentMenu.Children [0].Help); - Assert.Equal (New, e.CurrentMenu.Children [0].Action); - e.CurrentMenu.Children [0].Action (); - Assert.Equal ("New", miAction); - - e.NewMenuBarItem = new ( - "_Edit", - new MenuItem [] { new ("_Copy", "Copies the selection.", Copy) } - ); - }; - - menu.MenuOpened += (s, e) => - { - MenuItem mi = e.MenuItem; - - Assert.Equal ("_Edit", mi.Parent.Title); - Assert.Equal ("_Copy", mi.Title); - Assert.Equal ("Copies the selection.", mi.Help); - Assert.Equal (Copy, mi.Action); - mi.Action (); - Assert.Equal ("Copy", miAction); - }; - - menu.MenuClosing += (s, e) => - { - Assert.False (isMenuClosed); - - if (cancelClosing) - { - e.Cancel = true; - isMenuClosed = false; - } - else - { - isMenuClosed = true; - } - }; - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - isMenuClosed = !menu.IsMenuOpen; - Assert.False (isMenuClosed); - top.Draw (); - - var expected = @" -Edit -┌──────────────────────────────┐ -│ Copy Copies the selection. │ -└──────────────────────────────┘ -"; - DriverAssert.AssertDriverContentsAre (expected, output); - - cancelClosing = true; - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - Assert.False (isMenuClosed); - top.SetClipToScreen (); - top.Draw (); - - expected = @" -Edit -┌──────────────────────────────┐ -│ Copy Copies the selection. │ -└──────────────────────────────┘ -"; - DriverAssert.AssertDriverContentsAre (expected, output); - - cancelClosing = false; - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - Assert.True (isMenuClosed); - top.SetClipToScreen (); - top.Draw (); - - expected = @" -Edit -"; - DriverAssert.AssertDriverContentsAre (expected, output); - - void New () { miAction = "New"; } - - void Copy () { miAction = "Copy"; } - - top.Dispose (); - } - - [Fact (Skip = "See Issue #4370. Not gonna try to fix menu v1.")] - [AutoInitShutdown] - public void MouseEvent_Test () - { - MenuItem miCurrent = null; - Menu mCurrent = null; - - var menuBar = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] { new ("_New", "", null), new ("_Open", "", null), new ("_Save", "", null) } - ), - new ( - "_Edit", - new MenuItem [] { new ("_Copy", "", null), new ("C_ut", "", null), new ("_Paste", "", null) } - ) - ] - }; - - menuBar.MenuOpened += (s, e) => - { - miCurrent = e.MenuItem; - mCurrent = menuBar.OpenCurrentMenu; - }; - var top = new Toplevel (); - top.Add (menuBar); - Application.Begin (top); - - // Click on Edit - Assert.True ( - menuBar.NewMouseEvent ( - new () { Position = new (10, 0), Flags = MouseFlags.Button1Pressed, View = menuBar } - ) - ); - Assert.True (menuBar.IsMenuOpen); - Assert.Equal ("_Edit", miCurrent.Parent.Title); - Assert.Equal ("_Copy", miCurrent.Title); - - // Click on Paste - Assert.True ( - mCurrent.NewMouseEvent ( - new () { Position = new (10, 2), Flags = MouseFlags.ReportMousePosition, View = mCurrent } - ) - ); - Assert.True (menuBar.IsMenuOpen); - Assert.Equal ("_Edit", miCurrent.Parent.Title); - Assert.Equal ("_Paste", miCurrent.Title); - - for (var i = 4; i >= -1; i--) - { - Application.RaiseMouseEvent ( - new () { ScreenPosition = new (10, i), Flags = MouseFlags.ReportMousePosition } - ); - - Assert.True (menuBar.IsMenuOpen); - Menu menu = (Menu)top.SubViews.First (v => v is Menu); - - if (i is < 0 or > 0) - { - Assert.Equal (menu, Application.Mouse.MouseGrabView); - } - else - { - Assert.Equal (menuBar, Application.Mouse.MouseGrabView); - } - - Assert.Equal ("_Edit", miCurrent.Parent.Title); - - if (i == 4) - { - Assert.Equal ("_Paste", miCurrent.Title); - } - else if (i == 3) - { - Assert.Equal ("C_ut", miCurrent.Title); - } - else if (i == 2) - { - Assert.Equal ("_Copy", miCurrent.Title); - } - else - { - Assert.Equal ("_Copy", miCurrent.Title); - } - } - - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void Parent_MenuItem_Stay_Focused_If_Child_MenuItem_Is_Empty_By_Keyboard () - { - var expectedMenu = new ExpectedMenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ("Edit", Array.Empty ()), - new ( - "Format", - new MenuItem [] { new ("Wrap", "", null) } - ) - ] - }; - - MenuBarItem [] items = new MenuBarItem [expectedMenu.Menus.Length]; - - for (var i = 0; i < expectedMenu.Menus.Length; i++) - { - items [i] = new ( - expectedMenu.Menus [i].Title, - expectedMenu.Menus [i].Children.Length > 0 - ? new MenuItem [] { new (expectedMenu.Menus [i].Children [0].Title, "", null) } - : Array.Empty () - ); - } - - var menu = new MenuBar { Menus = items }; - - var tf = new TextField { Y = 2, Width = 10 }; - var top = new Toplevel (); - top.Add (menu, tf); - - Application.Begin (top); - Assert.True (tf.HasFocus); - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - // Right - Edit has no sub menu; this tests that no sub menu shows - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - Assert.Equal (1, menu._selected); - Assert.Equal (-1, menu._selectedSub); - Assert.Null (menu._openSubMenu); - top.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); - - // Right - Format - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - top.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (2), output); - - // Left - Edit - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorLeft)); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - top.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorLeft)); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - top.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - Assert.True (Application.RaiseKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - Assert.True (tf.HasFocus); - top.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void Parent_MenuItem_Stay_Focused_If_Child_MenuItem_Is_Empty_By_Mouse () - { - // File Edit Format - //┌──────┐ ┌───────┐ - //│ New │ │ Wrap │ - //└──────┘ └───────┘ - - // Define the expected menu - var expectedMenu = new ExpectedMenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ("Edit", new MenuItem [] { }), - new ( - "Format", - new MenuItem [] { new ("Wrap", "", null) } - ) - ] - }; - - var menu = new MenuBar - { - Menus = - [ - new ( - expectedMenu.Menus [0].Title, - new MenuItem [] { new (expectedMenu.Menus [0].Children [0].Title, "", null) } - ), - new (expectedMenu.Menus [1].Title, new MenuItem [] { }), - new ( - expectedMenu.Menus [2].Title, - new MenuItem [] - { - new ( - expectedMenu.Menus [2].Children [0].Title, - "", - null - ) - } - ) - ] - }; - - var tf = new TextField { Y = 2, Width = 10 }; - var top = new Toplevel (); - top.Add (menu, tf); - Application.Begin (top); - - Assert.True (tf.HasFocus); - Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (8, 0), Flags = MouseFlags.ReportMousePosition, View = menu } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - top.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (15, 0), Flags = MouseFlags.ReportMousePosition, View = menu } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - top.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (2), output); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (8, 0), Flags = MouseFlags.ReportMousePosition, View = menu } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - top.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (1, 0), Flags = MouseFlags.ReportMousePosition, View = menu } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - top.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - Assert.True (menu.NewMouseEvent (new () { Position = new (8, 0), Flags = MouseFlags.Button1Pressed, View = menu })); - Assert.False (menu.IsMenuOpen); - Assert.True (tf.HasFocus); - top.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - public void RemoveAndThenAddMenuBar_ShouldNotChangeWidth () - { - MenuBar menuBar; - MenuBar menuBar2; - - // TODO: When https: //github.com/gui-cs/Terminal.Gui/issues/3136 is fixed, - // TODO: Change this to Window - var w = new View (); - menuBar2 = new (); - menuBar = new (); - w.Width = Dim.Fill (); - w.Height = Dim.Fill (); - w.X = 0; - w.Y = 0; - - w.Visible = true; - - // TODO: When https: //github.com/gui-cs/Terminal.Gui/issues/3136 is fixed, - // TODO: uncomment this. - //w.Modal = false; - w.Title = ""; - menuBar.Width = Dim.Fill (); - menuBar.Height = 1; - menuBar.X = 0; - menuBar.Y = 0; - menuBar.Visible = true; - w.Add (menuBar); - - menuBar2.Width = Dim.Fill (); - menuBar2.Height = 1; - menuBar2.X = 0; - menuBar2.Y = 4; - menuBar2.Visible = true; - w.Add (menuBar2); - - MenuBar [] menuBars = w.SubViews.OfType ().ToArray (); - Assert.Equal (2, menuBars.Length); - - Assert.Equal (Dim.Fill (), menuBars [0].Width); - Assert.Equal (Dim.Fill (), menuBars [1].Width); - - // Goes wrong here - w.Remove (menuBar); - w.Remove (menuBar2); - - w.Add (menuBar); - w.Add (menuBar2); - - // These assertions fail - Assert.Equal (Dim.Fill (), menuBars [0].Width); - Assert.Equal (Dim.Fill (), menuBars [1].Width); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void Resizing_Close_Menus () - { - var menu = new MenuBar - { - Menus = - [ - new ( - "File", - new MenuItem [] - { - new ( - "Open", - "Open a file", - () => { }, - null, - null, - KeyCode.CtrlMask | KeyCode.O - ) - } - ) - ] - }; - var top = new Toplevel (); - top.Add (menu); - SessionToken rs = Application.Begin (top); - - menu.OpenMenu (); - - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - File -┌────────────────────────────┐ -│ Open Open a file Ctrl+O │ -└────────────────────────────┘", - output - ); - - Application.Driver!.SetScreenSize (20, 15); - - AutoInitShutdownAttribute.RunIteration (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - File", - output - ); - - Application.End (rs); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - public void Separator_Does_Not_Throws_Pressing_Menu_Hotkey () - { - var menu = new MenuBar - { - Menus = - [ - new ( - "File", - new MenuItem [] { new ("_New", "", null), null, new ("_Quit", "", null) } - ) - ] - }; - Assert.False (menu.NewKeyDownEvent (Key.Q.WithAlt)); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - public void SetMenus_With_Same_HotKey_Does_Not_Throws () - { - var mb = new MenuBar (); - - var i1 = new MenuBarItem ("_heey", "fff", () => { }, () => true); - - mb.Menus = new [] { i1 }; - mb.Menus = new [] { i1 }; - - Assert.Equal (Key.H, mb.Menus [0].HotKey); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void ShortCut_Activates () - { - var saveAction = false; - - var menu = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ( - "_Save", - "Saves the file.", - () => { saveAction = true; }, - null, - null, - (KeyCode)Key.S.WithCtrl - ) - } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Application.RaiseKeyDownEvent (Key.S.WithCtrl); - AutoInitShutdownAttribute.RunIteration (); - - Assert.True (saveAction); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - public void Update_ShortcutKey_KeyBindings_Old_ShortcutKey_Is_Removed () - { - var menuBar = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ("New", "Create New", null, null, null, Key.A.WithCtrl) - } - ) - ] - }; - - Assert.True (menuBar.HotKeyBindings.TryGet (Key.A.WithCtrl, out _)); - - menuBar.Menus [0].Children! [0].ShortcutKey = Key.B.WithCtrl; - - Assert.False (menuBar.HotKeyBindings.TryGet (Key.A.WithCtrl, out _)); - Assert.True (menuBar.HotKeyBindings.TryGet (Key.B.WithCtrl, out _)); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - public void UseKeysUpDownAsKeysLeftRight_And_UseSubMenusSingleFrame_Cannot_Be_Both_True () - { - var menu = new MenuBar (); - Assert.False (menu.UseKeysUpDownAsKeysLeftRight); - Assert.False (menu.UseSubMenusSingleFrame); - - menu.UseKeysUpDownAsKeysLeftRight = true; - Assert.True (menu.UseKeysUpDownAsKeysLeftRight); - Assert.False (menu.UseSubMenusSingleFrame); - - menu.UseSubMenusSingleFrame = true; - Assert.False (menu.UseKeysUpDownAsKeysLeftRight); - Assert.True (menu.UseSubMenusSingleFrame); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void UseSubMenusSingleFrame_False_By_Keyboard () - { - var menu = new MenuBar - { - Menus = new MenuBarItem [] - { - new ( - "Numbers", - new MenuItem [] - { - new ("One", "", null), - new MenuBarItem ( - "Two", - new MenuItem [] - { - new ("Sub-Menu 1", "", null), - new ("Sub-Menu 2", "", null) - } - ), - new ("Three", "", null) - } - ) - } - }; - menu.UseKeysUpDownAsKeysLeftRight = true; - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); - Assert.False (menu.UseSubMenusSingleFrame); - - top.Draw (); - - var expected = @" - Numbers -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - Assert.True (menu.NewKeyDownEvent (menu.Key)); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - Assert.True (Application.TopRunnable.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorDown)); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│┌─────────────┐ -│ Three ││ Sub-Menu 1 │ -└────────┘│ Sub-Menu 2 │ - └─────────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - Assert.True (Application.TopRunnable.SubViews.ElementAt (2).NewKeyDownEvent (Key.CursorLeft)); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - Assert.True (Application.TopRunnable.SubViews.ElementAt (1).NewKeyDownEvent (Key.Esc)); - top.Draw (); - - expected = @" - Numbers -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - top.Dispose (); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void UseSubMenusSingleFrame_False_By_Mouse () - { - var menu = new MenuBar - { - Menus = - [ - new ( - "Numbers", - new MenuItem [] - { - new ("One", "", null), - new MenuBarItem ( - "Two", - new MenuItem [] - { - new ( - "Sub-Menu 1", - "", - null - ), - new ( - "Sub-Menu 2", - "", - null - ) - } - ), - new ("Three", "", null) - } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); - Assert.False (menu.UseSubMenusSingleFrame); - - top.Draw (); - - var expected = @" - Numbers -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 1), pos); - - menu.NewMouseEvent ( - new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 10, 6), pos); - - menu.NewMouseEvent ( - new () - { - Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = Application.TopRunnable.SubViews.ElementAt (1) - } - ); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│┌─────────────┐ -│ Three ││ Sub-Menu 1 │ -└────────┘│ Sub-Menu 2 │ - └─────────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 25, 7), pos); - - Assert.False ( - menu.NewMouseEvent ( - new () - { - Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = Application.TopRunnable.SubViews.ElementAt (1) - } - ) - ); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 10, 6), pos); - - menu.NewMouseEvent ( - new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.TopRunnable } - ); - top.Draw (); - - expected = @" - Numbers -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 1), pos); - top.Dispose (); - } - - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void Visible_False_Key_Does_Not_Open_And_Close_All_Opened_Menus () - { - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }) - ] - }; - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.True (menu.Visible); - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - - menu.Visible = false; - Assert.False (menu.IsMenuOpen); - - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - top.Dispose (); - } - - [Fact (Skip = "v2 fake driver broke. Menu still works; disabling tests.")] - [AutoInitShutdown] - public void CanFocus_True_Key_Esc_Exit_Toplevel_If_IsMenuOpen_False () - { - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }) - ], - CanFocus = true - }; - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.True (menu.CanFocus); - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - - Assert.True (menu.NewKeyDownEvent (Key.Esc)); - Assert.False (menu.IsMenuOpen); - - Assert.False (menu.NewKeyDownEvent (Key.Esc)); - Assert.False (menu.IsMenuOpen); - top.Dispose (); - } - - // Defines the expected strings for a Menu. Currently supports - // - MenuBar with any number of MenuItems - // - Each top-level MenuItem can have a SINGLE sub-menu - // - // TODO: Enable multiple sub-menus - // TODO: Enable checked sub-menus - // TODO: Enable sub-menus with sub-menus (perhaps better to put this in a separate class with focused unit tests?) - // - // E.g: - // - // File Edit - // New Copy - public class ExpectedMenuBar : MenuBar - { - - // The expected strings when the menu is closed - public string ClosedMenuText => MenuBarText + "\n"; - - public string ExpectedBottomRow (int i) - { - return $"{Glyphs.LLCorner}{new (Glyphs.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3)}{Glyphs.LRCorner} \n"; - } - - // The 3 spaces at end are a result of Menu.cs line 1062 where `pos` is calculated (` + spacesAfterTitle`) - public string ExpectedMenuItemRow (int i) { return $"{Glyphs.VLine} {Menus [i].Children [0].Title} {Glyphs.VLine} \n"; } - - // The full expected string for an open sub menu - public string ExpectedSubMenuOpen (int i) - { - return ClosedMenuText - + (Menus [i].Children.Length > 0 - ? ExpectedPadding (i) - + ExpectedTopRow (i) - + ExpectedPadding (i) - + ExpectedMenuItemRow (i) - + ExpectedPadding (i) - + ExpectedBottomRow (i) - : ""); - } - - // Define expected menu frame - // "┌──────┐" - // "│ New │" - // "└──────┘" - // - // The width of the Frame is determined in Menu.cs line 144, where `Width` is calculated - // 1 space before the Title and 2 spaces after the Title/Check/Help - public string ExpectedTopRow (int i) - { - return $"{Glyphs.ULCorner}{new (Glyphs.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3)}{Glyphs.URCorner} \n"; - } - - // Each MenuBar title has a 1 space pad on each side - // See `static int leftPadding` and `static int rightPadding` on line 1037 of Menu.cs - public string MenuBarText - { - get - { - var txt = string.Empty; - - foreach (MenuBarItem m in Menus) - { - txt += " " + m.Title + " "; - } - - return txt; - } - } - - // Padding for the X of the sub menu Frame - // Menu.cs - Line 1239 in `internal void OpenMenu` is where the Menu is created - private string ExpectedPadding (int i) - { - var n = 0; - - while (i > 0) - { - n += Menus [i - 1].TitleLength + 2; - i--; - } - - return new (' ', n); - } - } - - private class CustomWindow : Window - { - public CustomWindow () - { - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ( - "Edit", - new MenuItem [] - { - new MenuBarItem ( - "Delete", - new MenuItem [] - { new ("All", "", null), new ("Selected", "", null) } - ) - } - ) - ] - }; - Add (menu); - } - } -} -#pragma warning restore CS0618 // Type or member is obsolete diff --git a/Tests/UnitTests/Views/Menuv1/Menuv1Tests.cs b/Tests/UnitTests/Views/Menuv1/Menuv1Tests.cs deleted file mode 100644 index 86f8bfc32..000000000 --- a/Tests/UnitTests/Views/Menuv1/Menuv1Tests.cs +++ /dev/null @@ -1,111 +0,0 @@ -using Xunit.Abstractions; - -//using static Terminal.Gui.ViewTests.MenuTests; - -namespace UnitTests.ViewsTests; - -#pragma warning disable CS0618 // Type or member is obsolete -public class Menuv1Tests -{ - private readonly ITestOutputHelper _output; - public Menuv1Tests (ITestOutputHelper output) { _output = output; } - - // TODO: Create more low-level unit tests for Menu and MenuItem - - [Fact] - public void Menu_Constructors_Defaults () - { - Assert.Throws (() => new Menu { Host = null, BarItems = new MenuBarItem () }); - Assert.Throws (() => new Menu { Host = new MenuBar (), BarItems = null }); - - var menu = new Menu { Host = new MenuBar (), X = 0, Y = 0, BarItems = new MenuBarItem () }; - Assert.Empty (menu.Title); - Assert.Empty (menu.Text); - } - - [Fact] - public void MenuItem_Constructors_Defaults () - { - var menuItem = new MenuItem (); - Assert.Equal ("", menuItem.Title); - Assert.Equal ("", menuItem.Help); - Assert.Null (menuItem.Action); - Assert.Null (menuItem.CanExecute); - Assert.Null (menuItem.Parent); - Assert.Equal (Key.Empty, menuItem.ShortcutKey); - - menuItem = new MenuItem ("Test", "Help", Run, () => { return true; }, new MenuItem (), KeyCode.F1); - Assert.Equal ("Test", menuItem.Title); - Assert.Equal ("Help", menuItem.Help); - Assert.Equal (Run, menuItem.Action); - Assert.NotNull (menuItem.CanExecute); - Assert.NotNull (menuItem.Parent); - Assert.Equal (KeyCode.F1, menuItem.ShortcutKey); - - void Run () { } - } - - [Fact] - public void MenuBarItem_SubMenu_Can_Return_Null () - { - var menuItem = new MenuItem (); - var menuBarItem = new MenuBarItem (); - Assert.Null (menuBarItem.SubMenu (menuItem)); - } - - [Fact] - public void MenuBarItem_Constructors_Defaults () - { - var menuBarItem = new MenuBarItem (); - Assert.Equal ("", menuBarItem.Title); - Assert.Equal ("", menuBarItem.Help); - Assert.Null (menuBarItem.Action); - Assert.Null (menuBarItem.CanExecute); - Assert.Null (menuBarItem.Parent); - Assert.Equal (Key.Empty, menuBarItem.ShortcutKey); - Assert.Equal ([], menuBarItem.Children); - Assert.False (menuBarItem.IsTopLevel); - - menuBarItem = new MenuBarItem (null!, null!, Run, () => true, new ()); - Assert.Equal ("", menuBarItem.Title); - Assert.Equal ("", menuBarItem.Help); - Assert.Equal (Run, menuBarItem.Action); - Assert.NotNull (menuBarItem.CanExecute); - Assert.NotNull (menuBarItem.Parent); - Assert.Equal (Key.Empty, menuBarItem.ShortcutKey); - Assert.Null (menuBarItem.Children); - Assert.False (menuBarItem.IsTopLevel); - - menuBarItem = new MenuBarItem (null!, Array.Empty (), new ()); - Assert.Equal ("", menuBarItem.Title); - Assert.Equal ("", menuBarItem.Help); - Assert.Null (menuBarItem.Action); - Assert.Null (menuBarItem.CanExecute); - Assert.NotNull (menuBarItem.Parent); - Assert.Equal (Key.Empty, menuBarItem.ShortcutKey); - Assert.Equal ([], menuBarItem.Children); - Assert.False (menuBarItem.IsTopLevel); - - menuBarItem = new MenuBarItem (null!, new List (), new ()); - Assert.Equal ("", menuBarItem.Title); - Assert.Equal ("", menuBarItem.Help); - Assert.Null (menuBarItem.Action); - Assert.Null (menuBarItem.CanExecute); - Assert.NotNull (menuBarItem.Parent); - Assert.Equal (Key.Empty, menuBarItem.ShortcutKey); - Assert.Equal ([], menuBarItem.Children); - Assert.False (menuBarItem.IsTopLevel); - - menuBarItem = new MenuBarItem ([]); - Assert.Equal ("", menuBarItem.Title); - Assert.Equal ("", menuBarItem.Help); - Assert.Null (menuBarItem.Action); - Assert.Null (menuBarItem.CanExecute); - Assert.Null (menuBarItem.Parent); - Assert.Equal (Key.Empty, menuBarItem.ShortcutKey); - Assert.Equal ([], menuBarItem.Children); - Assert.False (menuBarItem.IsTopLevel); - - void Run () { } - } -} diff --git a/Tests/UnitTests/Views/ToplevelTests.cs b/Tests/UnitTests/Views/ToplevelTests.cs index f01ad988e..55a426e3d 100644 --- a/Tests/UnitTests/Views/ToplevelTests.cs +++ b/Tests/UnitTests/Views/ToplevelTests.cs @@ -12,7 +12,6 @@ public class ToplevelTests Assert.Equal ("Fill(Absolute(0))", top.Height.ToString ()); Assert.False (top.Running); Assert.False (top.Modal); - Assert.Null (top.MenuBar); //Assert.Null (top.StatusBar); } @@ -42,32 +41,6 @@ public class ToplevelTests top.OnUnloaded (); Assert.Equal ("Unloaded", eventInvoked); - top.Add (new MenuBar ()); - Assert.NotNull (top.MenuBar); - - //top.Add (new StatusBar ()); - //Assert.NotNull (top.StatusBar); - MenuBar menuBar = top.MenuBar; - top.Remove (top.MenuBar); - Assert.Null (top.MenuBar); - Assert.NotNull (menuBar); - - //var statusBar = top.StatusBar; - //top.Remove (top.StatusBar); - //Assert.Null (top.StatusBar); - //Assert.NotNull (statusBar); -#if DEBUG_IDISPOSABLE - Assert.False (menuBar.WasDisposed); - - //Assert.False (statusBar.WasDisposed); - menuBar.Dispose (); - - //statusBar.Dispose (); - Assert.True (menuBar.WasDisposed); - - //Assert.True (statusBar.WasDisposed); -#endif - Application.Begin (top); Assert.Equal (top, Application.TopRunnable); @@ -76,50 +49,18 @@ public class ToplevelTests Assert.Equal (Application.TopRunnable, supView); Assert.Equal (0, nx); Assert.Equal (0, ny); - - //Assert.Null (sb); - - top.Add (new MenuBar ()); - Assert.NotNull (top.MenuBar); - - // Application.TopRunnable with a menu and without status bar. + // Application.Current with a menu and without status bar. View.GetLocationEnsuringFullVisibility (top, 2, 2, out nx, out ny /*, out sb*/); Assert.Equal (0, nx); - Assert.Equal (1, ny); - - //Assert.Null (sb); - - //top.Add (new StatusBar ()); - //Assert.NotNull (top.StatusBar); - + Assert.Equal (0, ny); // Application.TopRunnable with a menu and status bar. View.GetLocationEnsuringFullVisibility (top, 2, 2, out nx, out ny /*, out sb*/); Assert.Equal (0, nx); - // The available height is lower than the Application.TopRunnable height minus - // the menu bar and status bar, then the top can go beyond the bottom - // Assert.Equal (2, ny); - //Assert.NotNull (sb); - - menuBar = top.MenuBar; - top.Remove (top.MenuBar); - Assert.Null (top.MenuBar); - Assert.NotNull (menuBar); - - // Application.TopRunnable without a menu and with a status bar. + // Application.TopRunnable without a menu and with a status bar. View.GetLocationEnsuringFullVisibility (top, 2, 2, out nx, out ny /*, out sb*/); Assert.Equal (0, nx); - // The available height is lower than the Application.TopRunnable height minus - // the status bar, then the top can go beyond the bottom - // Assert.Equal (2, ny); - //Assert.NotNull (sb); - - //statusBar = top.StatusBar; - //top.Remove (top.StatusBar); - //Assert.Null (top.StatusBar); - //Assert.NotNull (statusBar); - Assert.Null (top.MenuBar); var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () }; top.Add (win); @@ -135,108 +76,10 @@ public class ToplevelTests View.GetLocationEnsuringFullVisibility (win, 0, 0, out nx, out ny /*, out sb*/); Assert.Equal (0, nx); Assert.Equal (0, ny); - - //Assert.Null (sb); - - top.Add (new MenuBar ()); - Assert.NotNull (top.MenuBar); - - // Application.TopRunnable with a menu and without status bar. - View.GetLocationEnsuringFullVisibility (win, 2, 2, out nx, out ny /*, out sb*/); - Assert.Equal (0, nx); - Assert.Equal (1, ny); - - //Assert.Null (sb); - - top.Add (new StatusBar ()); - - //Assert.NotNull (top.StatusBar); - - // Application.TopRunnable with a menu and status bar. - View.GetLocationEnsuringFullVisibility (win, 30, 20, out nx, out ny /*, out sb*/); - Assert.Equal (0, nx); - - // The available height is lower than the Application.TopRunnable height minus - // the menu bar and status bar, then the top can go beyond the bottom - //Assert.Equal (20, ny); - //Assert.NotNull (sb); - - menuBar = top.MenuBar; - - //statusBar = top.StatusBar; - top.Remove (top.MenuBar); - Assert.Null (top.MenuBar); - Assert.NotNull (menuBar); - - //top.Remove (top.StatusBar); - //Assert.Null (top.StatusBar); - //Assert.NotNull (statusBar); - top.Remove (win); win = new () { Width = 60, Height = 15 }; top.Add (win); - - // Application.TopRunnable without menu and status bar. - View.GetLocationEnsuringFullVisibility (win, 0, 0, out nx, out ny /*, out sb*/); - Assert.Equal (0, nx); - Assert.Equal (0, ny); - - //Assert.Null (sb); - - top.Add (new MenuBar ()); - Assert.NotNull (top.MenuBar); - - // Application.TopRunnable with a menu and without status bar. - View.GetLocationEnsuringFullVisibility (win, 2, 2, out nx, out ny /*, out sb*/); - Assert.Equal (2, nx); - Assert.Equal (2, ny); - - //Assert.Null (sb); - - top.Add (new StatusBar ()); - - //Assert.NotNull (top.StatusBar); - - // Application.TopRunnable with a menu and status bar. - View.GetLocationEnsuringFullVisibility (win, 30, 20, out nx, out ny /*, out sb*/); - Assert.Equal (20, nx); // 20+60=80 - - //Assert.Equal (9, ny); // 9+15+1(mb)=25 - //Assert.NotNull (sb); - - //Assert.Null (Toplevel._dragPosition); - win.NewMouseEvent (new () { Position = new (6, 0), Flags = MouseFlags.Button1Pressed }); - - // Assert.Equal (new Point (6, 0), Toplevel._dragPosition); - win.NewMouseEvent (new () { Position = new (6, 0), Flags = MouseFlags.Button1Released }); - - //Assert.Null (Toplevel._dragPosition); - win.CanFocus = false; - win.NewMouseEvent (new () { Position = new (6, 0), Flags = MouseFlags.Button1Pressed }); - - //Assert.Null (Toplevel._dragPosition); -#if DEBUG_IDISPOSABLE - - Assert.False (top.MenuBar.WasDisposed); - - //Assert.False (top.StatusBar.WasDisposed); -#endif - menuBar = top.MenuBar; - - //statusBar = top.StatusBar; - top.Dispose (); - Assert.Null (top.MenuBar); - - //Assert.Null (top.StatusBar); - Assert.NotNull (menuBar); - - //Assert.NotNull (statusBar); -#if DEBUG_IDISPOSABLE - Assert.True (menuBar.WasDisposed); - - //Assert.True (statusBar.WasDisposed); -#endif } [Fact] @@ -714,28 +557,6 @@ public class ToplevelTests window.Dispose (); } - [Fact] - [AutoInitShutdown] - public void Activating_MenuBar_By_Alt_Key_Does_Not_Throw () - { - var menu = new MenuBar - { - Menus = - [ - new ("Child", new MenuItem [] { new ("_Create Child", "", null) }) - ] - }; - var topChild = new Toplevel (); - topChild.Add (menu); - var top = new Toplevel (); - top.Add (topChild); - Application.Begin (top); - - Exception exception = Record.Exception (() => topChild.NewKeyDownEvent (KeyCode.AltMask)); - Assert.Null (exception); - top.Dispose (); - } - [Fact] public void Multi_Thread_Toplevels () { @@ -870,33 +691,4 @@ public class ToplevelTests t.Dispose (); Application.Shutdown (); } - - [Fact] - public void Remove_Do_Not_Dispose_MenuBar_Or_StatusBar () - { - var mb = new MenuBar (); - var sb = new StatusBar (); - var tl = new Toplevel (); - -#if DEBUG - Assert.False (mb.WasDisposed); - Assert.False (sb.WasDisposed); -#endif - tl.Add (mb, sb); - Assert.NotNull (tl.MenuBar); - - //Assert.NotNull (tl.StatusBar); -#if DEBUG - Assert.False (mb.WasDisposed); - Assert.False (sb.WasDisposed); -#endif - tl.RemoveAll (); - Assert.Null (tl.MenuBar); - - //Assert.Null (tl.StatusBar); -#if DEBUG - Assert.False (mb.WasDisposed); - Assert.False (sb.WasDisposed); -#endif - } } diff --git a/Tests/UnitTests/Views/WindowTests.cs b/Tests/UnitTests/Views/WindowTests.cs index 32d524549..9ad07b547 100644 --- a/Tests/UnitTests/Views/WindowTests.cs +++ b/Tests/UnitTests/Views/WindowTests.cs @@ -3,117 +3,8 @@ using Xunit.Abstractions; namespace UnitTests.ViewsTests; -public class WindowTests (ITestOutputHelper output) +public class WindowTests () { - [Fact] - [AutoInitShutdown] - public void Activating_MenuBar_By_Alt_Key_Does_Not_Throw () - { - var menu = new MenuBar - { - Menus = - [ - new MenuBarItem ("Child", new MenuItem [] { new ("_Create Child", "", null) }) - ] - }; - var win = new Window (); - win.Add (menu); - var top = new Toplevel (); - top.Add (win); - Application.Begin (top); - - Exception exception = Record.Exception (() => win.NewKeyDownEvent (KeyCode.AltMask)); - Assert.Null (exception); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void MenuBar_And_StatusBar_Inside_Window () - { - var menu = new MenuBar - { - Menus = - [ - new MenuBarItem ("File", new MenuItem [] { new ("Open", "", null), new ("Quit", "", null) }), - new MenuBarItem ( - "Edit", - new MenuItem [] { new ("Copy", "", null) } - ) - ] - }; - - var sb = new StatusBar (); - - var fv = new FrameView { Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1), Title = "Frame View", BorderStyle = LineStyle.Single }; - var win = new Window (); - win.Add (menu, sb, fv); - Toplevel top = new (); - top.Add (win); - Application.Begin (top); - Application.Driver!.SetScreenSize (20, 10); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────┐ -│ File Edit │ -│┌┤Frame View├────┐│ -││ ││ -││ ││ -││ ││ -││ ││ -│└────────────────┘│ -│ │ -└──────────────────┘", - output - ); - - Application.Driver!.SetScreenSize (40, 20); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌┤Frame View├────────────────────────┐│ -││ ││ -││ ││ -││ ││ -││ ││ -││ ││ -││ ││ -││ ││ -││ ││ -││ ││ -││ ││ -││ ││ -││ ││ -││ ││ -││ ││ -│└────────────────────────────────────┘│ -│ │ -└──────────────────────────────────────┘", - output - ); - - Application.Driver!.SetScreenSize (20, 10); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────┐ -│ File Edit │ -│┌┤Frame View├────┐│ -││ ││ -││ ││ -││ ││ -││ ││ -│└────────────────┘│ -│ │ -└──────────────────┘", - output - ); - top.Dispose (); - } - [Fact] public void New_Initializes () { diff --git a/Tests/UnitTestsParallelizable/Views/BarTests.cs b/Tests/UnitTestsParallelizable/Views/BarTests.cs index 259c97abd..4454c08b0 100644 --- a/Tests/UnitTestsParallelizable/Views/BarTests.cs +++ b/Tests/UnitTestsParallelizable/Views/BarTests.cs @@ -107,7 +107,7 @@ public class BarTests public void GetAttributeForRole_DoesNotDeferToSuperView_WhenSchemeNameIsSet () { // This test would fail before the fix that checks SchemeName in GetAttributeForRole - // StatusBar and MenuBarv2 set SchemeName = "Menu", and should use Menu scheme + // StatusBar and MenuBar set SchemeName = "Menu", and should use Menu scheme // instead of deferring to parent's customized attributes var parentView = new View { SchemeName = "Base" }; diff --git a/Tests/UnitTestsParallelizable/Views/MenuBarItemTests.cs b/Tests/UnitTestsParallelizable/Views/MenuBarItemTests.cs index 75ac7a9a5..f4405628f 100644 --- a/Tests/UnitTestsParallelizable/Views/MenuBarItemTests.cs +++ b/Tests/UnitTestsParallelizable/Views/MenuBarItemTests.cs @@ -9,11 +9,11 @@ public class MenuBarItemTests () [Fact] public void Constructors_Defaults () { - var menuBarItem = new MenuBarItemv2 (); + var menuBarItem = new MenuBarItem (); Assert.Null (menuBarItem.PopoverMenu); Assert.Null (menuBarItem.TargetView); - menuBarItem = new MenuBarItemv2 (targetView: null, command: Command.NotBound, commandText: null, popoverMenu: null); + menuBarItem = new MenuBarItem (targetView: null, command: Command.NotBound, commandText: null, popoverMenu: null); Assert.Null (menuBarItem.PopoverMenu); Assert.Null (menuBarItem.TargetView); diff --git a/Tests/UnitTestsParallelizable/Views/MenuItemTests.cs b/Tests/UnitTestsParallelizable/Views/MenuItemTests.cs deleted file mode 100644 index b48502cd9..000000000 --- a/Tests/UnitTestsParallelizable/Views/MenuItemTests.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Xunit.Abstractions; - -//using static Terminal.Gui.ViewTests.MenuTests; - -namespace UnitTests_Parallelizable.ViewsTests; - -public class MenuItemTests () -{ - [Fact] - public void Constructors_Defaults () - { - - } -} diff --git a/Tests/UnitTestsParallelizable/Views/MenuTests.cs b/Tests/UnitTestsParallelizable/Views/MenuTests.cs index f83dd490e..a20f7d53f 100644 --- a/Tests/UnitTestsParallelizable/Views/MenuTests.cs +++ b/Tests/UnitTestsParallelizable/Views/MenuTests.cs @@ -9,7 +9,7 @@ public class MenuTests () [Fact] public void Constructors_Defaults () { - var menu = new Menuv2 { }; + var menu = new Menu { }; Assert.Empty (menu.Title); Assert.Empty (menu.Text); } diff --git a/docfx/docs/Popovers.md b/docfx/docs/Popovers.md index bfbe549dd..54cc24d10 100644 --- a/docfx/docs/Popovers.md +++ b/docfx/docs/Popovers.md @@ -15,4 +15,4 @@ A `Popover` is any View that meets these characteristics: - Is Transparent (`ViewportSettings = ViewportSettings.Transparent | ViewportSettings.TransparentMouse` - Sets `Visible = false` when it receives `Application.QuitKey` -@Terminal.Gui.Views.PopoverMenu provides a sophisticated implementation that can be used as a context menu and is the basis for @Terminal.Gui.MenuBarv2. \ No newline at end of file +@Terminal.Gui.Views.PopoverMenu provides a sophisticated implementation that can be used as a context menu and is the basis for @Terminal.Gui.MenuBar. \ No newline at end of file diff --git a/docfx/docs/cancellable-work-pattern.md b/docfx/docs/cancellable-work-pattern.md index b39262d69..aa5f7263a 100644 --- a/docfx/docs/cancellable-work-pattern.md +++ b/docfx/docs/cancellable-work-pattern.md @@ -181,7 +181,7 @@ protected bool? RaiseAccepting(ICommandContext? ctx) #### Propagation Challenge -- `Command.Activate` is local, limiting hierarchical coordination (e.g., `MenuBarv2` popovers). A proposed `PropagatedCommands` property addresses this, as detailed in the appendix. +- `Command.Activate` is local, limiting hierarchical coordination (e.g., `MenuBar` popovers). A proposed `PropagatedCommands` property addresses this, as detailed in the appendix. ### 4. Application.Keyboard: Application-Level Keyboard Input diff --git a/docfx/docs/command.md b/docfx/docs/command.md index 9e6779ca6..82f30e4c9 100644 --- a/docfx/docs/command.md +++ b/docfx/docs/command.md @@ -10,7 +10,7 @@ The `Command` system in Terminal.Gui provides a standardized framework for defining and executing actions that views can perform, such as selecting items, accepting input, or navigating content. Implemented primarily through the `View.Command` APIs, this system integrates tightly with input handling (e.g., keyboard and mouse events) and leverages the *Cancellable Work Pattern* to ensure extensibility, cancellation, and decoupling. Central to this system are the `Selecting` and `Accepting` events, which encapsulate common user interactions: `Selecting` for changing a view’s state or preparing it for interaction (e.g., toggling a checkbox, focusing a menu item), and `Accepting` for confirming an action or state (e.g., executing a menu command, submitting a dialog). -This deep dive explores the `Command` and `View.Command` APIs, focusing on the `Selecting` and `Accepting` concepts, their implementation, and their propagation behavior. It critically evaluates the need for additional events (`Selected`/`Accepted`) and the propagation of `Selecting` events, drawing on insights from `Menuv2`, `MenuItemv2`, `MenuBarv2`, `CheckBox`, and `FlagSelector`. These implementations highlight the system’s application in hierarchical (menus) and stateful (checkboxes, flag selectors) contexts. The document reflects the current implementation, including the `Cancel` property in `CommandEventArgs` and local handling of `Command.Select`. An appendix briefly summarizes proposed changes from a filed issue to rename `Command.Select` to `Command.Activate`, replace `Cancel` with `Handled`, and introduce a propagation mechanism, addressing limitations in the current system. +This deep dive explores the `Command` and `View.Command` APIs, focusing on the `Selecting` and `Accepting` concepts, their implementation, and their propagation behavior. It critically evaluates the need for additional events (`Selected`/`Accepted`) and the propagation of `Selecting` events, drawing on insights from `Menu`, `MenuItemv2`, `MenuBar`, `CheckBox`, and `FlagSelector`. These implementations highlight the system’s application in hierarchical (menus) and stateful (checkboxes, flag selectors) contexts. The document reflects the current implementation, including the `Cancel` property in `CommandEventArgs` and local handling of `Command.Select`. An appendix briefly summarizes proposed changes from a filed issue to rename `Command.Select` to `Command.Activate`, replace `Cancel` with `Handled`, and introduce a propagation mechanism, addressing limitations in the current system. ## Overview of the Command System @@ -89,7 +89,7 @@ public bool? InvokeCommand(Command command, ICommandContext? ctx) ### Command Routing Most commands route directly to the target view. `Command.Select` and `Command.Accept` have special routing: -- `Command.Select`: Handled locally, with no propagation to superviews, relying on view-specific events (e.g., `SelectedMenuItemChanged` in `Menuv2`) for hierarchical coordination. +- `Command.Select`: Handled locally, with no propagation to superviews, relying on view-specific events (e.g., `SelectedMenuItemChanged` in `Menu`) for hierarchical coordination. - `Command.Accept`: Propagates to a default button (if `IsDefault = true`), superview, or `SuperMenuItem` (in menus). **Example**: `Command.Accept` in `RaiseAccepting`: @@ -170,7 +170,7 @@ These concepts are opinionated, reflecting Terminal.Gui’s view that most UI in } ``` - **OptionSelector**: Selecting an OpitonSelector option raises `Selecting` to update the selected option. - - **Menuv2** and **MenuBarv2**: Selecting a `MenuItemv2` (e.g., via mouse enter or arrow keys) sets focus, tracked by `SelectedMenuItem` and raising `SelectedMenuItemChanged`: + - **Menu** and **MenuBar**: Selecting a `MenuItemv2` (e.g., via mouse enter or arrow keys) sets focus, tracked by `SelectedMenuItem` and raising `SelectedMenuItemChanged`: ```csharp protected override void OnFocusedChanged(View? previousFocused, View? focused) { @@ -196,7 +196,7 @@ These concepts are opinionated, reflecting Terminal.Gui’s view that most UI in ``` - **Views without State**: For views like `Button`, `Selecting` typically sets focus but does not change state, making it less relevant. -- **Propagation**: `Command.Select` is handled locally by the target view. If the command is unhandled (`null` or `false`), processing stops without propagating to the superview or other views. This is evident in `Menuv2`, where `SelectedMenuItemChanged` is used for hierarchical coordination, and in `CheckBox` and `FlagSelector`, where state changes are internal. +- **Propagation**: `Command.Select` is handled locally by the target view. If the command is unhandled (`null` or `false`), processing stops without propagating to the superview or other views. This is evident in `Menu`, where `SelectedMenuItemChanged` is used for hierarchical coordination, and in `CheckBox` and `FlagSelector`, where state changes are internal. ### Accepting - **Definition**: `Accepting` represents a user action that confirms or finalizes a view’s state or triggers an action, such as submitting a dialog, activating a button, or confirming a selection in a list. It is associated with `Command.Accept`, typically triggered by the Enter key or double-click. @@ -211,7 +211,7 @@ These concepts are opinionated, reflecting Terminal.Gui’s view that most UI in - **Button**: Pressing Enter raises `Accepting` to activate the button (e.g., submit a dialog). - **ListView**: Double-clicking or pressing Enter raises `Accepting` to confirm the selected item(s). - **TextField**: Pressing Enter raises `Accepting` to submit the input. - - **Menuv2** and **MenuBarv2**: Pressing Enter on a `MenuItemv2` raises `Accepting` to execute a command or open a submenu, followed by the `Accepted` event to hide the menu or deactivate the menu bar: + - **Menu** and **MenuBar**: Pressing Enter on a `MenuItemv2` raises `Accepting` to execute a command or open a submenu, followed by the `Accepted` event to hide the menu or deactivate the menu bar: ```csharp protected void RaiseAccepted(ICommandContext? ctx) { @@ -230,7 +230,7 @@ These concepts are opinionated, reflecting Terminal.Gui’s view that most UI in - **Propagation**: `Command.Accept` propagates to: - A default button (if present in the superview with `IsDefault = true`). - The superview, enabling hierarchical handling (e.g., a dialog processes `Accept` if no button handles it). - - In `Menuv2`, propagation extends to the `SuperMenuItem` for submenus in popovers, as seen in `OnAccepting`: + - In `Menu`, propagation extends to the `SuperMenuItem` for submenus in popovers, as seen in `OnAccepting`: ```csharp protected override bool OnAccepting(CommandEventArgs args) { @@ -245,7 +245,7 @@ These concepts are opinionated, reflecting Terminal.Gui’s view that most UI in return false; } ``` - - Similarly, `MenuBarv2` customizes propagation to show popovers: + - Similarly, `MenuBar` customizes propagation to show popovers: ```csharp protected override bool OnAccepting(CommandEventArgs args) { @@ -278,7 +278,7 @@ These concepts are opinionated, reflecting Terminal.Gui’s view that most UI in | **Event** | `Selecting` | `Accepting` | | **Virtual Method** | `OnSelecting` | `OnAccepting` | | **Propagation** | Local to the view | Propagates to default button, superview, or SuperMenuItem (in menus) | -| **Use Cases** | `Menuv2`, `MenuBarv2`, `CheckBox`, `FlagSelector`, `ListView`, `Button` | `Menuv2`, `MenuBarv2`, `CheckBox`, `FlagSelector`, `Button`, `ListView`, `Dialog` | +| **Use Cases** | `Menu`, `MenuBar`, `CheckBox`, `FlagSelector`, `ListView`, `Button` | `Menu`, `MenuBar`, `CheckBox`, `FlagSelector`, `Button`, `ListView`, `Dialog` | | **State Dependency** | Often stateful, but includes focus for stateless views | May be stateless (triggers action) | ### Critical Evaluation: Selecting vs. Accepting @@ -287,9 +287,9 @@ The distinction between `Selecting` and `Accepting` is clear in theory: - `Accepting` is about finalizing an action, such as submitting a selection or activating a button. However, practical challenges arise: -- **Overlapping Triggers**: In `ListView`, pressing Enter might both select an item (`Selecting`) and confirm it (`Accepting`), depending on the interaction model, potentially confusing developers. Similarly, in `Menuv2`, navigation (e.g., arrow keys) triggers `Selecting`, while Enter triggers `Accepting`, but the overlap in user intent can blur the lines. +- **Overlapping Triggers**: In `ListView`, pressing Enter might both select an item (`Selecting`) and confirm it (`Accepting`), depending on the interaction model, potentially confusing developers. Similarly, in `Menu`, navigation (e.g., arrow keys) triggers `Selecting`, while Enter triggers `Accepting`, but the overlap in user intent can blur the lines. - **Stateless Views**: For views like `Button` or `MenuItemv2`, `Selecting` is limited to setting focus, which dilutes its purpose as a state-changing action and may confuse developers expecting a more substantial state change. -- **Propagation Limitations**: The local handling of `Command.Select` restricts hierarchical coordination. For example, `MenuBarv2` relies on `SelectedMenuItemChanged` to manage `PopoverMenu` visibility, which is view-specific and not generalizable. This highlights a need for a propagation mechanism that maintains subview-superview decoupling. +- **Propagation Limitations**: The local handling of `Command.Select` restricts hierarchical coordination. For example, `MenuBar` relies on `SelectedMenuItemChanged` to manage `PopoverMenu` visibility, which is view-specific and not generalizable. This highlights a need for a propagation mechanism that maintains subview-superview decoupling. - **FlagSelector Design Flaw**: In `FlagSelector`, the `CheckBox.Selecting` handler incorrectly triggers both `Selecting` and `Accepting`, conflating state changes (toggling flags) with action confirmation (submitting the flag set). This violates the intended separation and requires a design fix to ensure `Selecting` is limited to subview state changes and `Accepting` is reserved for parent-level confirmation. **Recommendation**: Enhance documentation to clarify the `Selecting`/`Accepting` model: @@ -299,13 +299,13 @@ However, practical challenges arise: ## Evaluating Selected/Accepted Events -The need for `Selected` and `Accepted` events is under consideration, with `Accepted` showing utility in specific views (`Menuv2`, `MenuBarv2`) but not universally required across all views. These events would serve as post-events, notifying that a `Selecting` or `Accepting` action has completed, similar to other *Cancellable Work Pattern* post-events like `ClearedViewport` in `View.Draw` or `OrientationChanged` in `OrientationHelper`. +The need for `Selected` and `Accepted` events is under consideration, with `Accepted` showing utility in specific views (`Menu`, `MenuBar`) but not universally required across all views. These events would serve as post-events, notifying that a `Selecting` or `Accepting` action has completed, similar to other *Cancellable Work Pattern* post-events like `ClearedViewport` in `View.Draw` or `OrientationChanged` in `OrientationHelper`. ### Need for Selected/Accepted Events - **Selected Event**: - **Purpose**: A `Selected` event would notify that a `Selecting` action has completed, indicating that a state change or preparatory action (e.g., a new item highlighted, a checkbox toggled) has taken effect. - **Use Cases**: - - **Menuv2** and **MenuBarv2**: Notify when a new `MenuItemv2` is focused, currently handled by the `SelectedMenuItemChanged` event, which tracks focus changes: + - **Menu** and **MenuBar**: Notify when a new `MenuItemv2` is focused, currently handled by the `SelectedMenuItemChanged` event, which tracks focus changes: ```csharp protected override void OnFocusedChanged(View? previousFocused, View? focused) { @@ -366,7 +366,7 @@ The need for `Selected` and `Accepted` events is under consideration, with `Acce ``` - **ListView**: Notify when a new item is selected, typically handled by `SelectedItemChanged` or similar custom events. - **Button**: Less relevant, as `Selecting` typically only sets focus, and no state change occurs to warrant a `Selected` notification. - - **Current Approach**: Views like `Menuv2`, `CheckBox`, and `FlagSelector` use custom events (`SelectedMenuItemChanged`, `CheckedStateChanged`, `ValueChanged`) to signal state changes, bypassing a generic `Selected` event. These view-specific events provide context (e.g., the selected `MenuItemv2`, the new `CheckedState`, or the updated `Value`) that a generic `Selected` event would struggle to convey without additional complexity. + - **Current Approach**: Views like `Menu`, `CheckBox`, and `FlagSelector` use custom events (`SelectedMenuItemChanged`, `CheckedStateChanged`, `ValueChanged`) to signal state changes, bypassing a generic `Selected` event. These view-specific events provide context (e.g., the selected `MenuItemv2`, the new `CheckedState`, or the updated `Value`) that a generic `Selected` event would struggle to convey without additional complexity. - **Pros**: - A standardized `Selected` event could unify state change notifications across views, reducing the need for custom events in some cases. - Aligns with the *Cancellable Work Pattern*’s post-event phase, providing a consistent way to react to completed `Selecting` actions. @@ -376,13 +376,13 @@ The need for `Selected` and `Accepted` events is under consideration, with `Acce - Less relevant for stateless views like `Button`, where `Selecting` only sets focus, leading to inconsistent usage across view types. - Adds complexity to the base `View` class, potentially bloating the API for a feature not universally needed. - Requires developers to handle generic `Selected` events with less specific information, which could lead to more complex event handling logic compared to targeted view-specific events. - - **Context Insight**: The use of `SelectedMenuItemChanged` in `Menuv2` and `MenuBarv2`, `CheckedStateChanged` in `CheckBox`, and `ValueChanged` in `FlagSelector` suggests that view-specific events are preferred for their specificity and context. These events are tailored to the view’s state (e.g., `MenuItemv2` instance, `CheckState`, or `Value`), making them more intuitive for developers than a generic `Selected` event. The absence of a `Selected` event in the current implementation indicates that it hasn’t been necessary for most use cases, as view-specific events adequately cover state change notifications. - - **Verdict**: A generic `Selected` event could provide a standardized way to notify state changes, but its benefits are outweighed by the effectiveness of view-specific events like `SelectedMenuItemChanged`, `CheckedStateChanged`, and `ValueChanged`. These events offer richer context and are sufficient for current use cases across `Menuv2`, `CheckBox`, `FlagSelector`, and other views. Adding `Selected` to the base `View` class is not justified at this time, as it would add complexity without significant advantages over existing mechanisms. + - **Context Insight**: The use of `SelectedMenuItemChanged` in `Menu` and `MenuBar`, `CheckedStateChanged` in `CheckBox`, and `ValueChanged` in `FlagSelector` suggests that view-specific events are preferred for their specificity and context. These events are tailored to the view’s state (e.g., `MenuItemv2` instance, `CheckState`, or `Value`), making them more intuitive for developers than a generic `Selected` event. The absence of a `Selected` event in the current implementation indicates that it hasn’t been necessary for most use cases, as view-specific events adequately cover state change notifications. + - **Verdict**: A generic `Selected` event could provide a standardized way to notify state changes, but its benefits are outweighed by the effectiveness of view-specific events like `SelectedMenuItemChanged`, `CheckedStateChanged`, and `ValueChanged`. These events offer richer context and are sufficient for current use cases across `Menu`, `CheckBox`, `FlagSelector`, and other views. Adding `Selected` to the base `View` class is not justified at this time, as it would add complexity without significant advantages over existing mechanisms. - **Accepted Event**: - **Purpose**: An `Accepted` event would notify that an `Accepting` action has completed (i.e., was not canceled via `args.Cancel`), indicating that the action has taken effect, aligning with the *Cancellable Work Pattern*’s post-event phase. - **Use Cases**: - - **Menuv2** and **MenuBarv2**: The `Accepted` event is critical for signaling that a menu command has been executed or a submenu action has completed, triggering actions like hiding the menu or deactivating the menu bar. In `Menuv2`, it’s raised by `RaiseAccepted` and used hierarchically: + - **Menu** and **MenuBar**: The `Accepted` event is critical for signaling that a menu command has been executed or a submenu action has completed, triggering actions like hiding the menu or deactivating the menu bar. In `Menu`, it’s raised by `RaiseAccepted` and used hierarchically: ```csharp protected void RaiseAccepted(ICommandContext? ctx) { @@ -391,7 +391,7 @@ The need for `Selected` and `Accepted` events is under consideration, with `Acce Accepted?.Invoke(this, args); } ``` - In `MenuBarv2`, it deactivates the menu bar: + In `MenuBar`, it deactivates the menu bar: ```csharp protected override void OnAccepted(CommandEventArgs args) { @@ -408,16 +408,16 @@ The need for `Selected` and `Accepted` events is under consideration, with `Acce - **Button**: Could notify that the button was activated, typically handled by a custom event like `Clicked`. - **ListView**: Could notify that a selection was confirmed (e.g., Enter pressed), often handled by custom events. - **Dialog**: Could notify that an action was completed (e.g., OK button clicked), useful for hierarchical scenarios. - - **Current Approach**: `Menuv2` and `MenuItemv2` implement `Accepted` to signal action completion, with hierarchical handling via subscriptions (e.g., `MenuItemv2.Accepted` triggers `Menuv2.RaiseAccepted`, which triggers `MenuBarv2.OnAccepted`). Other views like `CheckBox` and `FlagSelector` rely on the completion of the `Accepting` event (i.e., not canceled) or custom events (e.g., `Button.Clicked`) to indicate action completion, without a generic `Accepted` event. + - **Current Approach**: `Menu` and `MenuItemv2` implement `Accepted` to signal action completion, with hierarchical handling via subscriptions (e.g., `MenuItemv2.Accepted` triggers `Menu.RaiseAccepted`, which triggers `MenuBar.OnAccepted`). Other views like `CheckBox` and `FlagSelector` rely on the completion of the `Accepting` event (i.e., not canceled) or custom events (e.g., `Button.Clicked`) to indicate action completion, without a generic `Accepted` event. - **Pros**: - - Provides a standardized way to react to confirmed actions, particularly valuable in composite or hierarchical views like `Menuv2`, `MenuBarv2`, and `Dialog`, where superviews need to respond to action completion (e.g., closing a menu or dialog). + - Provides a standardized way to react to confirmed actions, particularly valuable in composite or hierarchical views like `Menu`, `MenuBar`, and `Dialog`, where superviews need to respond to action completion (e.g., closing a menu or dialog). - Aligns with the *Cancellable Work Pattern*’s post-event phase, offering a consistent mechanism for post-action notifications. - Simplifies hierarchical scenarios by providing a unified event for action completion, reducing reliance on view-specific events in some cases. - **Cons**: - - May duplicate existing view-specific events (e.g., `Button.Clicked`, `Menuv2.Accepted`), leading to redundancy in views where custom events are already established. + - May duplicate existing view-specific events (e.g., `Button.Clicked`, `Menu.Accepted`), leading to redundancy in views where custom events are already established. - Adds complexity to the base `View` class, especially for views like `CheckBox` or `FlagSelector` where `Accepting`’s completion is often sufficient without a post-event. - Requires clear documentation to distinguish `Accepted` from `Accepting` and to clarify when it should be used over view-specific events. - - **Context Insight**: The implementation of `Accepted` in `Menuv2` and `MenuBarv2` demonstrates its utility in hierarchical contexts, where it facilitates actions like menu closure or menu bar deactivation. For example, `MenuItemv2` raises `Accepted` to trigger `Menuv2`’s `RaiseAccepted`, which propagates to `MenuBarv2`: + - **Context Insight**: The implementation of `Accepted` in `Menu` and `MenuBar` demonstrates its utility in hierarchical contexts, where it facilitates actions like menu closure or menu bar deactivation. For example, `MenuItemv2` raises `Accepted` to trigger `Menu`’s `RaiseAccepted`, which propagates to `MenuBar`: ```csharp protected void RaiseAccepted(ICommandContext? ctx) { @@ -427,16 +427,16 @@ The need for `Selected` and `Accepted` events is under consideration, with `Acce } ``` In contrast, `CheckBox` and `FlagSelector` do not use `Accepted`, relying on `Accepting`’s completion or view-specific events like `CheckedStateChanged` or `ValueChanged`. This suggests that `Accepted` is particularly valuable in composite views with hierarchical interactions but not universally needed across all views. The absence of `Accepted` in `CheckBox` and `FlagSelector` indicates that `Accepting` is often sufficient for simple confirmation scenarios, but the hierarchical use in menus and potential dialog applications highlight its potential for broader adoption in specific contexts. - - **Verdict**: The `Accepted` event is highly valuable in composite and hierarchical views like `Menuv2`, `MenuBarv2`, and potentially `Dialog`, where it supports coordinated action completion (e.g., closing menus or dialogs). However, adding it to the base `View` class is premature without broader validation across more view types, as many views (e.g., `CheckBox`, `FlagSelector`) function effectively without it, using `Accepting` or custom events. Implementing `Accepted` in specific views or base classes like `Bar` or `Toplevel` (e.g., for menus and dialogs) and reassessing its necessity for the base `View` class later is a prudent approach. This balances the demonstrated utility in hierarchical scenarios with the need to avoid unnecessary complexity in simpler views. + - **Verdict**: The `Accepted` event is highly valuable in composite and hierarchical views like `Menu`, `MenuBar`, and potentially `Dialog`, where it supports coordinated action completion (e.g., closing menus or dialogs). However, adding it to the base `View` class is premature without broader validation across more view types, as many views (e.g., `CheckBox`, `FlagSelector`) function effectively without it, using `Accepting` or custom events. Implementing `Accepted` in specific views or base classes like `Bar` or `Toplevel` (e.g., for menus and dialogs) and reassessing its necessity for the base `View` class later is a prudent approach. This balances the demonstrated utility in hierarchical scenarios with the need to avoid unnecessary complexity in simpler views. **Recommendation**: Avoid adding `Selected` or `Accepted` events to the base `View` class for now. Instead: -- Continue using view-specific events (e.g., `Menuv2.SelectedMenuItemChanged`, `CheckBox.CheckedStateChanged`, `FlagSelector.ValueChanged`, `ListView.SelectedItemChanged`, `Button.Clicked`) for their contextual specificity and clarity. -- Maintain and potentially formalize the use of `Accepted` in views like `Menuv2`, `MenuBarv2`, and `Dialog`, tracking its utility to determine if broader adoption in a base class like `Bar` or `Toplevel` is warranted. +- Continue using view-specific events (e.g., `Menu.SelectedMenuItemChanged`, `CheckBox.CheckedStateChanged`, `FlagSelector.ValueChanged`, `ListView.SelectedItemChanged`, `Button.Clicked`) for their contextual specificity and clarity. +- Maintain and potentially formalize the use of `Accepted` in views like `Menu`, `MenuBar`, and `Dialog`, tracking its utility to determine if broader adoption in a base class like `Bar` or `Toplevel` is warranted. - If `Selected` or `Accepted` events are added in the future, ensure they fire only when their respective events (`Selecting`, `Accepting`) are not canceled (i.e., `args.Cancel` is `false`), maintaining consistency with the *Cancellable Work Pattern*’s post-event phase. ## Propagation of Selecting -The current implementation of `Command.Select` is local, but `MenuBarv2` requires propagation to manage `PopoverMenu` visibility, highlighting a limitation in the system’s ability to support hierarchical coordination without view-specific mechanisms. +The current implementation of `Command.Select` is local, but `MenuBar` requires propagation to manage `PopoverMenu` visibility, highlighting a limitation in the system’s ability to support hierarchical coordination without view-specific mechanisms. ### Current Behavior - **Selecting**: `Command.Select` is handled locally by the target view, with no propagation to the superview or other views. If the command is unhandled (returns `null` or `false`), processing stops without further routing. @@ -457,7 +457,7 @@ The current implementation of `Command.Select` is local, but `MenuBarv2` require } ``` - **Context Across Views**: - - In `Menuv2`, `Selecting` sets focus and raises `SelectedMenuItemChanged` to track changes, but this is a view-specific mechanism: + - In `Menu`, `Selecting` sets focus and raises `SelectedMenuItemChanged` to track changes, but this is a view-specific mechanism: ```csharp protected override void OnFocusedChanged(View? previousFocused, View? focused) { @@ -466,7 +466,7 @@ The current implementation of `Command.Select` is local, but `MenuBarv2` require RaiseSelectedMenuItemChanged(SelectedMenuItem); } ``` - - In `MenuBarv2`, `SelectedMenuItemChanged` is used to manage `PopoverMenu` visibility, but this relies on custom event handling rather than a generic propagation model: + - In `MenuBar`, `SelectedMenuItemChanged` is used to manage `PopoverMenu` visibility, but this relies on custom event handling rather than a generic propagation model: ```csharp protected override void OnSelectedMenuItemChanged(MenuItemv2? selected) { @@ -481,7 +481,7 @@ The current implementation of `Command.Select` is local, but `MenuBarv2` require - In `Button`, `Selecting` sets focus, which is inherently local. - **Accepting**: `Command.Accept` propagates to a default button (if present), the superview, or a `SuperMenuItem` (in menus), enabling hierarchical handling. - - **Rationale**: `Accepting` often involves actions that affect the broader UI context (e.g., closing a dialog, executing a menu command), requiring coordination with parent views. This is evident in `Menuv2`’s propagation to `SuperMenuItem` and `MenuBarv2`’s handling of `Accepted`: + - **Rationale**: `Accepting` often involves actions that affect the broader UI context (e.g., closing a dialog, executing a menu command), requiring coordination with parent views. This is evident in `Menu`’s propagation to `SuperMenuItem` and `MenuBar`’s handling of `Accepted`: ```csharp protected override void OnAccepting(CommandEventArgs args) { @@ -498,10 +498,10 @@ The current implementation of `Command.Select` is local, but `MenuBarv2` require ``` ### Should Selecting Propagate? -The local handling of `Command.Select` is sufficient for many views, but `MenuBarv2`’s need to manage `PopoverMenu` visibility highlights a gap in the current design, where hierarchical coordination relies on view-specific events like `SelectedMenuItemChanged`. +The local handling of `Command.Select` is sufficient for many views, but `MenuBar`’s need to manage `PopoverMenu` visibility highlights a gap in the current design, where hierarchical coordination relies on view-specific events like `SelectedMenuItemChanged`. - **Arguments For Propagation**: - - **Hierarchical Coordination**: In `MenuBarv2`, propagation would allow the menu bar to react to `MenuItemv2` selections (e.g., focusing a menu item via arrow keys or mouse enter) to show or hide popovers, streamlining the interaction model. Without propagation, `MenuBarv2` depends on `SelectedMenuItemChanged`, which is specific to `Menuv2` and not reusable for other hierarchical components. + - **Hierarchical Coordination**: In `MenuBar`, propagation would allow the menu bar to react to `MenuItemv2` selections (e.g., focusing a menu item via arrow keys or mouse enter) to show or hide popovers, streamlining the interaction model. Without propagation, `MenuBar` depends on `SelectedMenuItemChanged`, which is specific to `Menu` and not reusable for other hierarchical components. - **Consistency with Accepting**: `Command.Accept`’s propagation model supports hierarchical actions (e.g., dialog submission, menu command execution), suggesting that `Command.Select` could benefit from a similar approach to enable broader UI coordination, particularly in complex views like menus or dialogs. - **Future-Proofing**: Propagation could support other hierarchical components, such as `TabView` (coordinating tab selection) or nested dialogs (tracking subview state changes), enhancing the `Command` system’s flexibility for future use cases. @@ -530,7 +530,7 @@ The local handling of `Command.Select` is sufficient for many views, but `MenuBa }; ``` - **Performance and Complexity**: Propagation increases event handling overhead and complicates the API, as superviews must process or ignore `Selecting` events. This could lead to performance issues in deeply nested view hierarchies or views with frequent state changes. - - **Existing Alternatives**: View-specific events like `SelectedMenuItemChanged`, `CheckedStateChanged`, and `ValueChanged` already provide mechanisms for superview coordination, negating the need for generic propagation in many cases. For instance, `MenuBarv2` uses `SelectedMenuItemChanged` to manage popovers, albeit in a view-specific way: + - **Existing Alternatives**: View-specific events like `SelectedMenuItemChanged`, `CheckedStateChanged`, and `ValueChanged` already provide mechanisms for superview coordination, negating the need for generic propagation in many cases. For instance, `MenuBar` uses `SelectedMenuItemChanged` to manage popovers, albeit in a view-specific way: ```csharp protected override void OnSelectedMenuItemChanged(MenuItemv2? selected) { @@ -543,21 +543,21 @@ The local handling of `Command.Select` is sufficient for many views, but `MenuBa Similarly, `CheckBox` and `FlagSelector` use `CheckedStateChanged` and `ValueChanged` to notify superviews or external code of state changes, which is sufficient for most scenarios. - **Semantics of `Cancel`**: Propagation would occur only if `args.Cancel` is `false`, implying an unhandled selection, which is counterintuitive since `Selecting` typically completes its action (e.g., setting focus or toggling a state) within the view. This could confuse developers expecting propagation to occur for all `Selecting` events. -- **Context Insight**: The `MenuBarv2` implementation demonstrates a clear need for propagation to manage `PopoverMenu` visibility, as it must react to `MenuItemv2` selections (e.g., focus changes) across its submenu hierarchy. The reliance on `SelectedMenuItemChanged` works but is specific to `Menuv2`, limiting its applicability to other hierarchical components. In contrast, `CheckBox` and `FlagSelector` show that local handling is adequate for most stateful views, where state changes are self-contained or communicated via view-specific events. `ListView` similarly operates locally, with `SelectedItemChanged` or similar events handling external notifications. `Button`’s focus-based `Selecting` is inherently local, requiring no propagation. This dichotomy suggests that while propagation is critical for certain hierarchical scenarios (e.g., menus), it’s unnecessary for many views, and any propagation mechanism must avoid coupling subviews to superviews to maintain encapsulation. +- **Context Insight**: The `MenuBar` implementation demonstrates a clear need for propagation to manage `PopoverMenu` visibility, as it must react to `MenuItemv2` selections (e.g., focus changes) across its submenu hierarchy. The reliance on `SelectedMenuItemChanged` works but is specific to `Menu`, limiting its applicability to other hierarchical components. In contrast, `CheckBox` and `FlagSelector` show that local handling is adequate for most stateful views, where state changes are self-contained or communicated via view-specific events. `ListView` similarly operates locally, with `SelectedItemChanged` or similar events handling external notifications. `Button`’s focus-based `Selecting` is inherently local, requiring no propagation. This dichotomy suggests that while propagation is critical for certain hierarchical scenarios (e.g., menus), it’s unnecessary for many views, and any propagation mechanism must avoid coupling subviews to superviews to maintain encapsulation. -- **Verdict**: The local handling of `Command.Select` is sufficient for most views, including `CheckBox`, `FlagSelector`, `ListView`, and `Button`, where state changes or preparatory actions are internal or communicated via view-specific events. However, `MenuBarv2`’s requirement for hierarchical coordination to manage `PopoverMenu` visibility highlights a gap in the current design, where view-specific events like `SelectedMenuItemChanged` are used as a workaround. A generic propagation model would enhance flexibility for hierarchical components, but it must ensure that subviews (e.g., `MenuItemv2`) remain decoupled from superviews (e.g., `MenuBarv2`) to avoid implementation-specific dependencies. The current lack of propagation is a limitation, particularly for menus, but adding it requires careful design to avoid overcomplicating the API or impacting performance for views that don’t need it. +- **Verdict**: The local handling of `Command.Select` is sufficient for most views, including `CheckBox`, `FlagSelector`, `ListView`, and `Button`, where state changes or preparatory actions are internal or communicated via view-specific events. However, `MenuBar`’s requirement for hierarchical coordination to manage `PopoverMenu` visibility highlights a gap in the current design, where view-specific events like `SelectedMenuItemChanged` are used as a workaround. A generic propagation model would enhance flexibility for hierarchical components, but it must ensure that subviews (e.g., `MenuItemv2`) remain decoupled from superviews (e.g., `MenuBar`) to avoid implementation-specific dependencies. The current lack of propagation is a limitation, particularly for menus, but adding it requires careful design to avoid overcomplicating the API or impacting performance for views that don’t need it. -**Recommendation**: Maintain the local handling of `Command.Select` for now, as it meets the needs of most views like `CheckBox`, `FlagSelector`, and `ListView`. For `MenuBarv2`, continue using `SelectedMenuItemChanged` as a temporary solution, but prioritize developing a generic propagation mechanism that supports hierarchical coordination without coupling subviews to superviews. This mechanism should allow superviews to opt-in to receiving `Selecting` events from subviews, ensuring encapsulation (see appendix for a proposed solution). +**Recommendation**: Maintain the local handling of `Command.Select` for now, as it meets the needs of most views like `CheckBox`, `FlagSelector`, and `ListView`. For `MenuBar`, continue using `SelectedMenuItemChanged` as a temporary solution, but prioritize developing a generic propagation mechanism that supports hierarchical coordination without coupling subviews to superviews. This mechanism should allow superviews to opt-in to receiving `Selecting` events from subviews, ensuring encapsulation (see appendix for a proposed solution). ## Recommendations for Refining the Design -Based on the analysis of the current `Command` and `View.Command` system, as implemented in `Menuv2`, `MenuBarv2`, `CheckBox`, and `FlagSelector`, the following recommendations aim to refine the system’s clarity, consistency, and flexibility while addressing identified limitations: +Based on the analysis of the current `Command` and `View.Command` system, as implemented in `Menu`, `MenuBar`, `CheckBox`, and `FlagSelector`, the following recommendations aim to refine the system’s clarity, consistency, and flexibility while addressing identified limitations: 1. **Clarify Selecting/Accepting in Documentation**: - Explicitly define `Selecting` as state changes or interaction preparation (e.g., toggling a `CheckBox`, focusing a `MenuItemv2`, selecting a `ListView` item) and `Accepting` as action confirmations (e.g., executing a menu command, submitting a dialog). - Emphasize that `Command.Select` may set focus in stateless views (e.g., `Button`, `MenuItemv2`) but is primarily intended for state changes, to reduce confusion for developers. - - Provide examples for each view type (e.g., `Menuv2`, `CheckBox`, `FlagSelector`, `ListView`, `Button`) to illustrate their distinct roles. For instance: - - `Menuv2`: “`Selecting` focuses a `MenuItemv2` via arrow keys, while `Accepting` executes the selected command.” + - Provide examples for each view type (e.g., `Menu`, `CheckBox`, `FlagSelector`, `ListView`, `Button`) to illustrate their distinct roles. For instance: + - `Menu`: “`Selecting` focuses a `MenuItemv2` via arrow keys, while `Accepting` executes the selected command.” - `CheckBox`: “`Selecting` toggles the `CheckedState`, while `Accepting` confirms the current state.” - `FlagSelector`: “`Selecting` toggles a subview flag, while `Accepting` confirms the entire flag set.” - Document the `Cancel` property’s role in `CommandEventArgs`, noting its current limitation (implying negation rather than completion) and the planned replacement with `Handled` to align with input events like `Key.Handled`. @@ -577,7 +577,7 @@ Based on the analysis of the current `Command` and `View.Command` system, as imp - This ensures `Selecting` only propagates state changes to the parent `FlagSelector` via `RaiseSelecting`, and `Accepting` is triggered separately (e.g., via Enter on the `FlagSelector` itself) to confirm the `Value`. 3. **Enhance ICommandContext with View-Specific State**: - - Enrich `ICommandContext` with a `State` property to include view-specific data (e.g., the selected `MenuItemv2` in `Menuv2`, the new `CheckedState` in `CheckBox`, the updated `Value` in `FlagSelector`). This enables more informed event handlers without requiring view-specific subscriptions. + - Enrich `ICommandContext` with a `State` property to include view-specific data (e.g., the selected `MenuItemv2` in `Menu`, the new `CheckedState` in `CheckBox`, the updated `Value` in `FlagSelector`). This enables more informed event handlers without requiring view-specific subscriptions. - Proposed interface update: ```csharp public interface ICommandContext @@ -588,7 +588,7 @@ Based on the analysis of the current `Command` and `View.Command` system, as imp object? State { get; } // View-specific state (e.g., selected item, CheckState) } ``` - - Example: In `Menuv2`, include the `SelectedMenuItem` in `ICommandContext.State` for `Selecting` handlers: + - Example: In `Menu`, include the `SelectedMenuItem` in `ICommandContext.State` for `Selecting` handlers: ```csharp protected bool? RaiseSelecting(ICommandContext? ctx) { @@ -605,19 +605,19 @@ Based on the analysis of the current `Command` and `View.Command` system, as imp - This enhances the flexibility of event handlers, allowing external code to react to state changes without subscribing to view-specific events like `SelectedMenuItemChanged` or `CheckedStateChanged`. 4. **Monitor Use Cases for Propagation Needs**: - - Track the usage of `Selecting` and `Accepting` in real-world applications, particularly in `Menuv2`, `MenuBarv2`, `CheckBox`, and `FlagSelector`, to identify scenarios where propagation of `Selecting` events could simplify hierarchical coordination. - - Collect feedback on whether the reliance on view-specific events (e.g., `SelectedMenuItemChanged` in `Menuv2`) is sufficient or if a generic propagation model would reduce complexity for hierarchical components like `MenuBarv2`. This will inform the design of a propagation mechanism that maintains subview-superview decoupling (see appendix). + - Track the usage of `Selecting` and `Accepting` in real-world applications, particularly in `Menu`, `MenuBar`, `CheckBox`, and `FlagSelector`, to identify scenarios where propagation of `Selecting` events could simplify hierarchical coordination. + - Collect feedback on whether the reliance on view-specific events (e.g., `SelectedMenuItemChanged` in `Menu`) is sufficient or if a generic propagation model would reduce complexity for hierarchical components like `MenuBar`. This will inform the design of a propagation mechanism that maintains subview-superview decoupling (see appendix). - Example focus areas: - - `MenuBarv2`: Assess whether `SelectedMenuItemChanged` adequately handles `PopoverMenu` visibility or if propagation would streamline the interaction model. + - `MenuBar`: Assess whether `SelectedMenuItemChanged` adequately handles `PopoverMenu` visibility or if propagation would streamline the interaction model. - `Dialog`: Evaluate whether `Selecting` propagation could enhance subview coordination (e.g., tracking checkbox toggles within a dialog). - `TabView`: Consider potential needs for tab selection coordination if implemented in the future. 5. **Improve Propagation for Hierarchical Views**: - - Recognize the limitation in `Command.Select`’s local handling for hierarchical components like `MenuBarv2`, where superviews need to react to subview selections (e.g., focusing a `MenuItemv2` to manage popovers). The current reliance on `SelectedMenuItemChanged` is effective but view-specific, limiting reusability. + - Recognize the limitation in `Command.Select`’s local handling for hierarchical components like `MenuBar`, where superviews need to react to subview selections (e.g., focusing a `MenuItemv2` to manage popovers). The current reliance on `SelectedMenuItemChanged` is effective but view-specific, limiting reusability. - Develop a propagation mechanism that allows superviews to opt-in to receiving `Selecting` events from subviews without requiring subviews to know superview details, ensuring encapsulation. This could involve a new event or property in `View` to enable propagation while maintaining decoupling (see appendix for a proposed solution). - - Example: For `MenuBarv2`, a propagation mechanism could allow it to handle `Selecting` events from `MenuItemv2` subviews to show or hide popovers, replacing the need for `SelectedMenuItemChanged`: + - Example: For `MenuBar`, a propagation mechanism could allow it to handle `Selecting` events from `MenuItemv2` subviews to show or hide popovers, replacing the need for `SelectedMenuItemChanged`: ```csharp - // Current workaround in MenuBarv2 + // Current workaround in MenuBar protected override void OnSelectedMenuItemChanged(MenuItemv2? selected) { if (IsOpen() && selected is MenuBarItemv2 { PopoverMenuOpen: false } selectedMenuBarItem) @@ -628,7 +628,7 @@ Based on the analysis of the current `Command` and `View.Command` system, as imp ``` 6. **Standardize Hierarchical Handling for Accepting**: - - Refine the propagation model for `Command.Accept` to reduce reliance on view-specific logic, such as `Menuv2`’s use of `SuperMenuItem` for submenu propagation. The current approach, while functional, introduces coupling: + - Refine the propagation model for `Command.Accept` to reduce reliance on view-specific logic, such as `Menu`’s use of `SuperMenuItem` for submenu propagation. The current approach, while functional, introduces coupling: ```csharp if (SuperView is null && SuperMenuItem is {}) { @@ -636,9 +636,9 @@ Based on the analysis of the current `Command` and `View.Command` system, as imp } ``` - Explore a more generic mechanism, such as allowing superviews to subscribe to `Accepting` events from subviews, to streamline propagation and improve encapsulation. This could be addressed in conjunction with `Selecting` propagation (see appendix). - - Example: In `Menuv2`, a subscription-based model could replace `SuperMenuItem` logic: + - Example: In `Menu`, a subscription-based model could replace `SuperMenuItem` logic: ```csharp - // Hypothetical subscription in Menuv2 + // Hypothetical subscription in Menu SubViewAdded += (sender, args) => { if (args.View is MenuItemv2 menuItem) @@ -650,15 +650,15 @@ Based on the analysis of the current `Command` and `View.Command` system, as imp ## Conclusion -The `Command` and `View.Command` system in Terminal.Gui provides a robust framework for handling view actions, with `Selecting` and `Accepting` serving as opinionated mechanisms for state changes/preparation and action confirmations. The system is effectively implemented across `Menuv2`, `MenuBarv2`, `CheckBox`, and `FlagSelector`, supporting a range of stateful and stateless interactions. However, limitations in terminology (`Select`’s ambiguity), cancellation semantics (`Cancel`’s misleading implication), and propagation (local `Selecting` handling) highlight areas for improvement. +The `Command` and `View.Command` system in Terminal.Gui provides a robust framework for handling view actions, with `Selecting` and `Accepting` serving as opinionated mechanisms for state changes/preparation and action confirmations. The system is effectively implemented across `Menu`, `MenuBar`, `CheckBox`, and `FlagSelector`, supporting a range of stateful and stateless interactions. However, limitations in terminology (`Select`’s ambiguity), cancellation semantics (`Cancel`’s misleading implication), and propagation (local `Selecting` handling) highlight areas for improvement. -The `Selecting`/`Accepting` distinction is clear in principle but requires careful documentation to avoid confusion, particularly for stateless views where `Selecting` is focus-driven and for views like `FlagSelector` where implementation flaws conflate the two concepts. View-specific events like `SelectedMenuItemChanged`, `CheckedStateChanged`, and `ValueChanged` are sufficient for post-selection notifications, negating the need for a generic `Selected` event. The `Accepted` event is valuable in hierarchical views like `Menuv2` and `MenuBarv2` but not universally required, suggesting inclusion in `Bar` or `Toplevel` rather than `View`. +The `Selecting`/`Accepting` distinction is clear in principle but requires careful documentation to avoid confusion, particularly for stateless views where `Selecting` is focus-driven and for views like `FlagSelector` where implementation flaws conflate the two concepts. View-specific events like `SelectedMenuItemChanged`, `CheckedStateChanged`, and `ValueChanged` are sufficient for post-selection notifications, negating the need for a generic `Selected` event. The `Accepted` event is valuable in hierarchical views like `Menu` and `MenuBar` but not universally required, suggesting inclusion in `Bar` or `Toplevel` rather than `View`. -By clarifying terminology, fixing implementation flaws (e.g., `FlagSelector`), enhancing `ICommandContext`, and developing a decoupled propagation model, Terminal.Gui can enhance the `Command` system’s clarity and flexibility, particularly for hierarchical components like `MenuBarv2`. The appendix summarizes proposed changes to address these limitations, aligning with a filed issue to guide future improvements. +By clarifying terminology, fixing implementation flaws (e.g., `FlagSelector`), enhancing `ICommandContext`, and developing a decoupled propagation model, Terminal.Gui can enhance the `Command` system’s clarity and flexibility, particularly for hierarchical components like `MenuBar`. The appendix summarizes proposed changes to address these limitations, aligning with a filed issue to guide future improvements. ## Appendix: Summary of Proposed Changes to Command System -A filed issue proposes enhancements to the `Command` system to address limitations in terminology, cancellation semantics, and propagation, informed by `Menuv2`, `MenuBarv2`, `CheckBox`, and `FlagSelector`. These changes are not yet implemented but aim to improve clarity, consistency, and flexibility. +A filed issue proposes enhancements to the `Command` system to address limitations in terminology, cancellation semantics, and propagation, informed by `Menu`, `MenuBar`, `CheckBox`, and `FlagSelector`. These changes are not yet implemented but aim to improve clarity, consistency, and flexibility. ### Proposed Changes 1. **Rename `Command.Select` to `Command.Activate`**: @@ -672,9 +672,9 @@ A filed issue proposes enhancements to the `Command` system to address limitatio - Impact: Clarifies semantics, requires updating event handlers. 3. **Introduce `PropagateActivating` Event**: - - Add `event EventHandler? PropagateActivating` to `View`, allowing superviews (e.g., `MenuBarv2`) to subscribe to subview propagation requests. - - Rationale: Enables hierarchical coordination (e.g., `MenuBarv2` managing `PopoverMenu` visibility) without coupling subviews to superviews, addressing the current reliance on view-specific events like `SelectedMenuItemChanged`. - - Impact: Enhances flexibility for hierarchical views, requires subscription management in superviews like `MenuBarv2`. + - Add `event EventHandler? PropagateActivating` to `View`, allowing superviews (e.g., `MenuBar`) to subscribe to subview propagation requests. + - Rationale: Enables hierarchical coordination (e.g., `MenuBar` managing `PopoverMenu` visibility) without coupling subviews to superviews, addressing the current reliance on view-specific events like `SelectedMenuItemChanged`. + - Impact: Enhances flexibility for hierarchical views, requires subscription management in superviews like `MenuBar`. ### Benefits - **Clarity**: `Activate` improves terminology for all views. @@ -685,7 +685,7 @@ A filed issue proposes enhancements to the `Command` system to address limitatio ### Implementation Notes - Update `Command` enum, `View`, and derived classes for the rename. - Modify `CommandEventArgs` for `Handled`. -- Implement `PropagateActivating` and test in `MenuBarv2`. +- Implement `PropagateActivating` and test in `MenuBar`. - Revise documentation to reflect changes. For details, refer to the filed issue in the Terminal.Gui repository. \ No newline at end of file diff --git a/docfx/docs/events.md b/docfx/docs/events.md index 52f6363d7..88e7a457c 100644 --- a/docfx/docs/events.md +++ b/docfx/docs/events.md @@ -350,7 +350,7 @@ TG follows the *naming* advice provided in [.NET Naming Guidelines - Names of Ev ### Proposed Enhancement: Command Propagation -The *Cancellable Work Pattern* in `View.Command` currently supports local `Command.Activate` and propagating `Command.Accept`. To address hierarchical coordination needs (e.g., `MenuBarv2` popovers, `Dialog` closing), a `PropagatedCommands` property is proposed (Issue #4050): +The *Cancellable Work Pattern* in `View.Command` currently supports local `Command.Activate` and propagating `Command.Accept`. To address hierarchical coordination needs (e.g., `MenuBar` popovers, `Dialog` closing), a `PropagatedCommands` property is proposed (Issue #4050): - **Change**: Add `IReadOnlyList PropagatedCommands` to `View`, defaulting to `[Command.Accept]`. `Raise*` methods propagate if the command is in `SuperView?.PropagatedCommands` and `args.Handled` is `false`. - **Example**: @@ -373,7 +373,7 @@ The *Cancellable Work Pattern* in `View.Command` currently supports local `Comma } ``` -- **Impact**: Enables `Command.Activate` propagation for `MenuBarv2` while preserving `Command.Accept` propagation, maintaining decoupling and avoiding noise from irrelevant commands. +- **Impact**: Enables `Command.Activate` propagation for `MenuBar` while preserving `Command.Accept` propagation, maintaining decoupling and avoiding noise from irrelevant commands. ### **Conflation in FlagSelector**: - **Issue**: `CheckBox.Activating` triggers `Accepting`, conflating state change and confirmation. @@ -389,7 +389,7 @@ The *Cancellable Work Pattern* in `View.Command` currently supports local `Comma ``` ### **Propagation Limitations**: - - **Issue**: Local `Command.Activate` restricts `MenuBarv2` coordination; `Command.Accept` uses hacks (#3925). + - **Issue**: Local `Command.Activate` restricts `MenuBar` coordination; `Command.Accept` uses hacks (#3925). - **Recommendation**: Adopt `PropagatedCommands` to enable targeted propagation, as proposed. ### **Complexity in Multi-Phase Workflows**: diff --git a/docfx/docs/newinv2.md b/docfx/docs/newinv2.md index 43dec8e87..570507950 100644 --- a/docfx/docs/newinv2.md +++ b/docfx/docs/newinv2.md @@ -173,14 +173,14 @@ See the [Views Overview](views.md) for a complete catalog of all built-in views. v2 introduces many new View subclasses that were not present in v1: -- **Bar**: A foundational view for horizontal or vertical layouts of `Shortcut` or other items, used in `StatusBar`, `MenuBarv2`, and `PopoverMenu`. +- **Bar**: A foundational view for horizontal or vertical layouts of `Shortcut` or other items, used in `StatusBar`, `MenuBar`, and `PopoverMenu`. - **CharMap**: A scrollable, searchable Unicode character map with support for the Unicode Character Database (UCD) API, enabling users to browse and select from all Unicode codepoints with detailed character information. See [Character Map Deep Dive](CharacterMap.md). - **ColorPicker**: Leverages TrueColor for a comprehensive color selection experience, supporting multiple color models (HSV, RGB, HSL, Grayscale) with interactive color bars. - **DatePicker**: Provides a calendar-based date selection UI with month/year navigation, leveraging v2's improved drawing and navigation systems. - **FlagSelector**: Enables selection of non-mutually-exclusive flags with checkbox-based UI, supporting both dictionary-based and enum-based flag definitions. - **GraphView**: Displays graphs (bar charts, scatter plots, line graphs) with flexible axes, labels, scaling, scrolling, and annotations - bringing data visualization to the terminal. - **Line**: Draws single horizontal or vertical lines using the `LineCanvas` system with automatic intersection handling and multiple line styles (Single, Double, Heavy, Rounded, Dashed, Dotted). -- **Menuv2 System** (MenuBarv2, PopoverMenu): A completely redesigned menu system built on the `Bar` infrastructure, providing a more flexible and visually appealing menu experience. +- **Menu System** (MenuBar, PopoverMenu): A completely redesigned menu system built on the `Bar` infrastructure, providing a more flexible and visually appealing menu experience. - **NumericUpDown**: Type-safe numeric input with increment/decrement buttons, supporting `int`, `long`, `float`, `double`, and `decimal` types. - **OptionSelector**: Displays a list of mutually-exclusive options with checkbox-style UI (radio button equivalent), supporting both horizontal and vertical orientations. - **Shortcut**: An opinionated view for displaying commands with key bindings, simplifying status bar and toolbar creation with consistent visual presentation. diff --git a/docfx/schemas/tui-config-schema.json b/docfx/schemas/tui-config-schema.json index 52b7d7f38..8eb937141 100644 --- a/docfx/schemas/tui-config-schema.json +++ b/docfx/schemas/tui-config-schema.json @@ -572,7 +572,7 @@ ], "description": "Default shadow style for Button controls, often used for 3D effect." }, - "Menuv2.DefaultBorderStyle": { + "Menu.DefaultBorderStyle": { "type": "string", "enum": [ "None", @@ -581,9 +581,9 @@ "Heavy", "Rounded" ], - "description": "Default border style for the newer Menuv2 control and its sub-menus." + "description": "Default border style for the newer Menu control and its sub-menus." }, - "MenuBarv2.DefaultBorderStyle": { + "MenuBar.DefaultBorderStyle": { "type": "string", "enum": [ "None", @@ -592,7 +592,7 @@ "Heavy", "Rounded" ], - "description": "Default border style for the MenuBarv2 control." + "description": "Default border style for the MenuBar control." }, "StatusBar.DefaultSeparatorLineStyle": { "type": "string", From e199063a3190a361317028e990e957fc1a145d70 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:01:16 -0700 Subject: [PATCH 10/16] Introduce IRunnable interface architecture with Fluent API (Phase 1) (#4405) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * Add IRunnable interface, Runnable base class, and RunnableSessionToken Co-authored-by: tig <585482+tig@users.noreply.github.com> * Add comprehensive parallelizable unit tests for IRunnable Co-authored-by: tig <585482+tig@users.noreply.github.com> * Add 41 more unit tests for comprehensive IRunnable coverage - Added ApplicationRunnableIntegrationTests with 29 integration tests covering Begin/End/Run lifecycle - Added RunnableEdgeCasesTests with 24 edge case and error condition tests - Tests cover event propagation, cancellation scenarios, nested runnables, result handling - Fixed App property not being set in Begin() method - Total test count increased from 23 to 64 tests for IRunnable functionality Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fix parallel test failures in CI/CD - Fixed IsModal property to check RunnableSessionStack instead of just TopRunnable - Added "fake" driver parameter to Application.Init() in integration tests - Fixed Begin() to capture old IsModal value before pushing to stack - Moved App property assignment before stack operations to ensure proper state - Skipped 2 tests that use Run() with main loop (not suitable for parallel tests) - All 11,654 parallelizable tests now pass (4 skipped) Co-authored-by: tig <585482+tig@users.noreply.github.com> * Refactor Application with IRunnable and session management Modernized `Application` and `ApplicationImpl` by introducing `IRunnable` and `RunnableSessionToken` for improved session management. Deprecated legacy methods and added `Obsolete` attributes to indicate their removal. Simplified method bodies using expression-bodied members and null-coalescing assignments. Enhanced lifecycle management in `ApplicationImpl` by removing redundant code and improving `SessionStack` iteration. Introduced `IToplevelTransitionManager` to handle top-level state changes. Updated `Runnable` to implement `IRunnable` with lifecycle event handling for `IsRunning` and `IsModal` states. Improved result management during lifecycle transitions. Removed legacy classes like `SessionToken` and consolidated their functionality into the new constructs. Updated and expanded the test suite to cover `IRunnable` lifecycle events, `RunnableSessionToken` behavior, and integration with `Application`. Performed code cleanup, improved readability, and updated documentation with detailed remarks and examples. Added new unit tests for edge cases and lifecycle behavior. * Implement fluent API for Init/Run/Shutdown with automatic disposal - Changed Init() to return IApplication for fluent chaining - Changed Run() to return IApplication (breaking change from TRunnable) - Changed Shutdown() to return object? (extracts and returns result from last Run()) - Added FrameworkOwnedRunnable property to track runnable created by Run() - Shutdown() automatically disposes framework-owned runnables - Created FluentExample demonstrating: Application.Create().Init().Run().Shutdown() - Disposal semantics: framework creates → framework disposes; caller creates → caller disposes Co-authored-by: tig <585482+tig@users.noreply.github.com> * New Example: Demonstrates new Fluent API using ColorPicker Conditional compilation (`#if POST_4148`) to support both a new Fluent API and a traditional approach for running `ColorPickerView`. The Fluent API simplifies the application lifecycle with method chaining and automatic disposal, while the traditional approach retains explicit lifecycle management. Refactor `ColorPickerView` to support both approaches: - Add an `instructions` label for user guidance. - Replace `_okButton` and `_cancelButton` with local `Button` instances. - Use a new `ColorPicker` with enhanced styling options. Add a warning log for WIP issue (#4148) in `ApplicationImpl.Run.cs` to highlight limitations with non-`Toplevel` views as runnables. Update `Terminal.sln` to include the new `FluentExample` project with appropriate build configurations. Improve code readability with verbatim string literals and better alignment/indentation. * Introduce `RunnableWrapper` for making any View runnable Added the `RunnableWrapper` pattern to enable any `View` to be run as a blocking session with typed results, without requiring inheritance from `Runnable` or implementation of `IRunnable`. - Added `RunnableWrapperExample` project to demonstrate usage. - Introduced `ApplicationRunnableExtensions` and `ViewRunnableExtensions` for clean, type-safe APIs to run views with or without result extraction. - Updated `CodeSharingStrategy.md` to document reduced duplication using `#if POST_4148` directives. - Added `RunnableWrapper.md` with detailed documentation and examples. - Created runnable examples in `Program.cs` showcasing various use cases. - Improved maintainability by reducing code duplication by 86% and increasing shared code by 264%. - Gated all new functionality behind the `POST_4148` feature flag for backward compatibility. * Simplified `#if POST_4148` usage to reduce duplication and improve clarity. Refactored `RunnableWrapper` to use a parameterless constructor with `required` properties, ensuring type safety and better lifecycle management. Updated `AllViewsView` with new commands, improved generic handling, and enhanced logging. Refactored `ApplicationRunnableExtensions` and `ViewRunnableExtensions` for cleaner initialization and event handling. Enhanced `TestsAllViews` to handle required properties and constraints dynamically. Updated documentation to reflect new designs and provide clearer examples. Improved overall code readability, consistency, and maintainability while leveraging modern C# features. * Update docfx documentation for IRunnable architecture - Updated View.md with comprehensive IRunnable section - Interface-based architecture explanation - Fluent API patterns and examples - Disposal semantics ("whoever creates it, owns it") - Result extraction patterns - Lifecycle properties and CWP-compliant events - Marked legacy Modal Views section for clarity - Updated application.md with IRunnable deep dive - Key features and benefits - Fluent API patterns with method chaining - Disposal semantics table - Creating runnable views with examples - Lifecycle properties and events - RunnableSessionStack management - Updated IApplication interface documentation - Updated runnable-architecture-proposal.md - Marked Phase 1 as COMPLETE ✅ - Updated status to "Phase 1 Complete - Phase 2 In Progress" - Documented all implemented features - Added bonus features (fluent API, automatic disposal) - Included migration examples All documentation is now clear, concise, and complete relative to Phase 1 implementation. Co-authored-by: tig <585482+tig@users.noreply.github.com> --------- Co-authored-by: Tig Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Examples/FluentExample/FluentExample.csproj | 11 + Examples/FluentExample/Program.cs | 143 +++++ Examples/RunnableWrapperExample/Program.cs | 165 ++++++ .../RunnableWrapperExample.csproj | 15 + .../EditorsAndHelpers/AllViewsView.cs | 56 +- Terminal.Gui/App/Application.Run.cs | 4 +- Terminal.Gui/App/ApplicationImpl.Lifecycle.cs | 35 +- Terminal.Gui/App/ApplicationImpl.Run.cs | 308 +++++++++- Terminal.Gui/App/ApplicationImpl.cs | 32 +- .../App/ApplicationRunnableExtensions.cs | 158 +++++ Terminal.Gui/App/IApplication.cs | 204 ++++++- Terminal.Gui/App/Runnable/IRunnable.cs | 233 ++++++++ .../IToplevelTransitionManager.cs | 0 .../App/Runnable/RunnableSessionToken.cs | 87 +++ .../App/{ => Runnable}/SessionToken.cs | 0 .../{ => Runnable}/SessionTokenEventArgs.cs | 0 .../ToplevelTransitionManager.cs | 0 Terminal.Gui/ViewBase/Runnable.cs | 223 +++++++ Terminal.Gui/ViewBase/RunnableWrapper.cs | 90 +++ .../ViewBase/ViewRunnableExtensions.cs | 126 ++++ Terminal.sln | 12 + Terminal.sln.DotSettings | 2 + .../ApplicationImplBeginEndTests.cs | 42 +- .../Application/ApplicationPopoverTests.cs | 6 +- Tests/UnitTests/TestsAllViews.cs | 28 +- .../Runnable/RunnableEdgeCasesTests.cs | 327 +++++++++++ .../Runnable/RunnableIntegrationTests.cs | 543 ++++++++++++++++++ .../Runnable/RunnableLifecycleTests.cs | 156 +++++ .../Runnable/RunnableSessionTokenTests.cs | 62 ++ .../Application/Runnable/RunnableTests.cs | 222 +++++++ docfx/docs/View.md | 134 ++++- docfx/docs/application.md | 202 ++++++- docfx/docs/runnable-architecture-proposal.md | 67 ++- 33 files changed, 3573 insertions(+), 120 deletions(-) create mode 100644 Examples/FluentExample/FluentExample.csproj create mode 100644 Examples/FluentExample/Program.cs create mode 100644 Examples/RunnableWrapperExample/Program.cs create mode 100644 Examples/RunnableWrapperExample/RunnableWrapperExample.csproj create mode 100644 Terminal.Gui/App/ApplicationRunnableExtensions.cs create mode 100644 Terminal.Gui/App/Runnable/IRunnable.cs rename Terminal.Gui/App/{Toplevel => Runnable}/IToplevelTransitionManager.cs (100%) create mode 100644 Terminal.Gui/App/Runnable/RunnableSessionToken.cs rename Terminal.Gui/App/{ => Runnable}/SessionToken.cs (100%) rename Terminal.Gui/App/{ => Runnable}/SessionTokenEventArgs.cs (100%) rename Terminal.Gui/App/{Toplevel => Runnable}/ToplevelTransitionManager.cs (100%) create mode 100644 Terminal.Gui/ViewBase/Runnable.cs create mode 100644 Terminal.Gui/ViewBase/RunnableWrapper.cs create mode 100644 Terminal.Gui/ViewBase/ViewRunnableExtensions.cs create mode 100644 Tests/UnitTestsParallelizable/Application/Runnable/RunnableEdgeCasesTests.cs create mode 100644 Tests/UnitTestsParallelizable/Application/Runnable/RunnableIntegrationTests.cs create mode 100644 Tests/UnitTestsParallelizable/Application/Runnable/RunnableLifecycleTests.cs create mode 100644 Tests/UnitTestsParallelizable/Application/Runnable/RunnableSessionTokenTests.cs create mode 100644 Tests/UnitTestsParallelizable/Application/Runnable/RunnableTests.cs diff --git a/Examples/FluentExample/FluentExample.csproj b/Examples/FluentExample/FluentExample.csproj new file mode 100644 index 000000000..2086af6ed --- /dev/null +++ b/Examples/FluentExample/FluentExample.csproj @@ -0,0 +1,11 @@ + + + Exe + net8.0 + preview + enable + + + + + diff --git a/Examples/FluentExample/Program.cs b/Examples/FluentExample/Program.cs new file mode 100644 index 000000000..e27caf26e --- /dev/null +++ b/Examples/FluentExample/Program.cs @@ -0,0 +1,143 @@ +// Fluent API example demonstrating IRunnable with automatic disposal and result extraction + +using Terminal.Gui.App; +using Terminal.Gui.Drawing; +using Terminal.Gui.ViewBase; +using Terminal.Gui.Views; + +#if POST_4148 +// Run the application with fluent API - automatically creates, runs, and disposes the runnable + +// Display the result +if (Application.Create () + .Init () + .Run () + .Shutdown () is Color { } result) +{ + Console.WriteLine (@$"Selected Color: {(Color?)result}"); +} +else +{ + Console.WriteLine (@"No color selected"); +} +#else + +// Run using traditional approach +IApplication app = Application.Create (); +app.Init (); +var colorPicker = new ColorPickerView (); +app.Run (colorPicker); + +Color? resultColor = colorPicker.Result; + +colorPicker.Dispose (); +app.Shutdown (); + +if (resultColor is { } result) +{ + Console.WriteLine (@$"Selected Color: {(Color?)result}"); +} +else +{ + Console.WriteLine (@"No color selected"); +} + +#endif + +#if POST_4148 +/// +/// A runnable view that allows the user to select a color. +/// Demonstrates IRunnable pattern with automatic disposal. +/// +public class ColorPickerView : Runnable +{ + +#else +/// +/// A runnable view that allows the user to select a color. +/// Uses the traditional approach without automatic disposal/Fluent API. +/// +public class ColorPickerView : Toplevel +{ + public Color? Result { get; set; } + +#endif + public ColorPickerView () + { + Title = "Select a Color (Esc to quit)"; + BorderStyle = LineStyle.Single; + Height = Dim.Auto (); + Width = Dim.Auto (); + + // Add instructions + var instructions = new Label + { + Text = "Use arrow keys to select a color, Enter to accept", + X = Pos.Center (), + Y = 0 + }; + + // Create color picker + ColorPicker colorPicker = new () + { + X = Pos.Center (), + Y = Pos.Bottom (instructions), + Style = new ColorPickerStyle () + { + ShowColorName = true, + ShowTextFields = true + } + }; + colorPicker.ApplyStyleChanges (); + + // Create OK button + Button okButton = new () + { + Title = "_OK", + X = Pos.Align (Alignment.Center), + Y = Pos.AnchorEnd (), + IsDefault = true + }; + + okButton.Accepting += (s, e) => + { + // Extract result before stopping + Result = colorPicker.SelectedColor; + RequestStop (); + e.Handled = true; + }; + + // Create Cancel button + Button cancelButton = new () + { + Title = "_Cancel", + X = Pos.Align (Alignment.Center), + Y = Pos.AnchorEnd () + }; + + cancelButton.Accepting += (s, e) => + { + // Don't set result - leave as null + RequestStop (); + e.Handled = true; + }; + + // Add views + Add (instructions, colorPicker, okButton, cancelButton); + } + +#if POST_4148 + protected override bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning) + { + // Alternative place to extract result before stopping + // This is called before the view is removed from the stack + if (!newIsRunning && Result is null) + { + // User pressed Esc - could extract current selection here + // Result = _colorPicker.SelectedColor; + } + + return base.OnIsRunningChanging (oldIsRunning, newIsRunning); + } +#endif +} diff --git a/Examples/RunnableWrapperExample/Program.cs b/Examples/RunnableWrapperExample/Program.cs new file mode 100644 index 000000000..41a2df32b --- /dev/null +++ b/Examples/RunnableWrapperExample/Program.cs @@ -0,0 +1,165 @@ +// Example demonstrating how to make ANY View runnable without implementing IRunnable + +using Terminal.Gui.App; +using Terminal.Gui.Drawing; +using Terminal.Gui.ViewBase; +using Terminal.Gui.Views; + +IApplication app = Application.Create (); +app.Init (); + +// Example 1: Use extension method with result extraction +var textField = new TextField { Width = 40, Text = "Default text" }; +textField.Title = "Enter your name"; +textField.BorderStyle = LineStyle.Single; + +var textRunnable = textField.AsRunnable (tf => tf.Text); +app.Run (textRunnable); + +if (textRunnable.Result is { } name) +{ + MessageBox.Query ("Result", $"You entered: {name}", "OK"); +} +else +{ + MessageBox.Query ("Result", "Canceled", "OK"); +} +textRunnable.Dispose (); + +// Example 2: Use IApplication.RunView() for one-liner +var selectedColor = app.RunView ( + new ColorPicker + { + Title = "Pick a Color", + BorderStyle = LineStyle.Single + }, + cp => cp.SelectedColor); + +MessageBox.Query ("Result", $"Selected color: {selectedColor}", "OK"); + +// Example 3: FlagSelector with typed enum result +var flagSelector = new FlagSelector +{ + Title = "Choose Styles", + BorderStyle = LineStyle.Single +}; + +var flagsRunnable = flagSelector.AsRunnable (fs => fs.Value); +app.Run (flagsRunnable); + +MessageBox.Query ("Result", $"Selected styles: {flagsRunnable.Result}", "OK"); +flagsRunnable.Dispose (); + +// Example 4: Any View without result extraction +var label = new Label +{ + Text = "Press Esc to continue...", + X = Pos.Center (), + Y = Pos.Center () +}; + +var labelRunnable = label.AsRunnable (); +app.Run (labelRunnable); + +// Can still access the wrapped view +MessageBox.Query ("Result", $"Label text was: {labelRunnable.WrappedView.Text}", "OK"); +labelRunnable.Dispose (); + +// Example 5: Complex custom View made runnable +var formView = CreateCustomForm (); +var formRunnable = formView.AsRunnable (ExtractFormData); + +app.Run (formRunnable); + +if (formRunnable.Result is { } formData) +{ + MessageBox.Query ( + "Form Results", + $"Name: {formData.Name}\nAge: {formData.Age}\nAgreed: {formData.Agreed}", + "OK"); +} +formRunnable.Dispose (); + +app.Shutdown (); + +// Helper method to create a custom form +View CreateCustomForm () +{ + var form = new View + { + Title = "User Information", + BorderStyle = LineStyle.Single, + Width = 50, + Height = 10 + }; + + var nameField = new TextField + { + Id = "nameField", + X = 10, + Y = 1, + Width = 30 + }; + + var ageField = new TextField + { + Id = "ageField", + X = 10, + Y = 3, + Width = 10 + }; + + var agreeCheckbox = new CheckBox + { + Id = "agreeCheckbox", + Title = "I agree to terms", + X = 10, + Y = 5 + }; + + var okButton = new Button + { + Title = "OK", + X = Pos.Center (), + Y = 7, + IsDefault = true + }; + + okButton.Accepting += (s, e) => + { + form.App?.RequestStop (); + e.Handled = true; + }; + + form.Add (new Label { Text = "Name:", X = 2, Y = 1 }); + form.Add (nameField); + form.Add (new Label { Text = "Age:", X = 2, Y = 3 }); + form.Add (ageField); + form.Add (agreeCheckbox); + form.Add (okButton); + + return form; +} + +// Helper method to extract data from the custom form +FormData ExtractFormData (View form) +{ + var nameField = form.SubViews.FirstOrDefault (v => v.Id == "nameField") as TextField; + var ageField = form.SubViews.FirstOrDefault (v => v.Id == "ageField") as TextField; + var agreeCheckbox = form.SubViews.FirstOrDefault (v => v.Id == "agreeCheckbox") as CheckBox; + + return new FormData + { + Name = nameField?.Text ?? string.Empty, + Age = int.TryParse (ageField?.Text, out int age) ? age : 0, + Agreed = agreeCheckbox?.CheckedState == CheckState.Checked + }; +} + +// Result type for custom form +record FormData +{ + public string Name { get; init; } = string.Empty; + public int Age { get; init; } + public bool Agreed { get; init; } +} diff --git a/Examples/RunnableWrapperExample/RunnableWrapperExample.csproj b/Examples/RunnableWrapperExample/RunnableWrapperExample.csproj new file mode 100644 index 000000000..7e34acedb --- /dev/null +++ b/Examples/RunnableWrapperExample/RunnableWrapperExample.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + enable + latest + + + + + + + diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/AllViewsView.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/AllViewsView.cs index 862cc2083..f8f78ac6f 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/AllViewsView.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/AllViewsView.cs @@ -4,6 +4,7 @@ namespace UICatalog.Scenarios; public class AllViewsView : View { private const int MAX_VIEW_FRAME_HEIGHT = 25; + public AllViewsView () { CanFocus = true; @@ -24,6 +25,7 @@ public class AllViewsView : View AddCommand (Command.Down, () => ScrollVertical (1)); AddCommand (Command.PageUp, () => ScrollVertical (-SubViews.OfType ().First ().Frame.Height)); AddCommand (Command.PageDown, () => ScrollVertical (SubViews.OfType ().First ().Frame.Height)); + AddCommand ( Command.Start, () => @@ -32,6 +34,7 @@ public class AllViewsView : View return true; }); + AddCommand ( Command.End, () => @@ -65,12 +68,12 @@ public class AllViewsView : View MouseBindings.Add (MouseFlags.WheeledRight, Command.ScrollRight); } - /// + /// public override void EndInit () { base.EndInit (); - var allClasses = GetAllViewClassesCollection (); + List allClasses = GetAllViewClassesCollection (); View? previousView = null; @@ -95,19 +98,6 @@ public class AllViewsView : View } } - private static List GetAllViewClassesCollection () - { - List types = typeof (View).Assembly.GetTypes () - .Where ( - myType => myType is { IsClass: true, IsAbstract: false, IsPublic: true } - && myType.IsSubclassOf (typeof (View))) - .ToList (); - - types.Add (typeof (View)); - - return types; - } - private View? CreateView (Type type) { // If we are to create a generic Type @@ -125,12 +115,32 @@ public class AllViewsView : View } else { - typeArguments.Add (typeof (object)); + // Check if the generic parameter has constraints + Type [] constraints = arg.GetGenericParameterConstraints (); + + if (constraints.Length > 0) + { + // Use the first constraint type to satisfy the constraint + typeArguments.Add (constraints [0]); + } + else + { + typeArguments.Add (typeof (object)); + } } } // And change what type we are instantiating from MyClass to MyClass or MyClass - type = type.MakeGenericType (typeArguments.ToArray ()); + try + { + type = type.MakeGenericType (typeArguments.ToArray ()); + } + catch (ArgumentException ex) + { + Logging.Warning ($"Cannot create generic type {type} with arguments [{string.Join (", ", typeArguments.Select (t => t.Name))}]: {ex.Message}"); + + return null; + } } // Ensure the type does not contain any generic parameters @@ -164,6 +174,18 @@ public class AllViewsView : View return view; } + private static List GetAllViewClassesCollection () + { + List types = typeof (View).Assembly.GetTypes () + .Where (myType => myType is { IsClass: true, IsAbstract: false, IsPublic: true } + && myType.IsSubclassOf (typeof (View))) + .ToList (); + + types.Add (typeof (View)); + + return types; + } + private void OnViewInitialized (object? sender, EventArgs e) { if (sender is not View view) diff --git a/Terminal.Gui/App/Application.Run.cs b/Terminal.Gui/App/Application.Run.cs index d218c370d..9e6b2e064 100644 --- a/Terminal.Gui/App/Application.Run.cs +++ b/Terminal.Gui/App/Application.Run.cs @@ -20,7 +20,7 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E set => ApplicationImpl.Instance.Keyboard.ArrangeKey = value; } - /// + /// [Obsolete ("The legacy static Application object is going away.")] public static SessionToken Begin (Toplevel toplevel) => ApplicationImpl.Instance.Begin (toplevel); @@ -82,7 +82,7 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E [Obsolete ("The legacy static Application object is going away.")] public static void RequestStop (Toplevel? top = null) => ApplicationImpl.Instance.RequestStop (top); - /// + /// [Obsolete ("The legacy static Application object is going away.")] public static void End (SessionToken sessionToken) => ApplicationImpl.Instance.End (sessionToken); diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs index 3e08a3f72..ed1e98741 100644 --- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs +++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs @@ -14,7 +14,7 @@ public partial class ApplicationImpl /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public void Init (string? driverName = null) + public IApplication Init (string? driverName = null) { if (Initialized) { @@ -71,11 +71,24 @@ public partial class ApplicationImpl SynchronizationContext.SetSynchronizationContext (new ()); MainThreadId = Thread.CurrentThread.ManagedThreadId; + + return this; } /// Shutdown an application initialized with . - public void Shutdown () + public object? Shutdown () { + // Extract result from framework-owned runnable before disposal + object? result = null; + IRunnable? runnableToDispose = FrameworkOwnedRunnable; + + if (runnableToDispose is { }) + { + // Extract the result using reflection to get the Result property value + var resultProperty = runnableToDispose.GetType().GetProperty("Result"); + result = resultProperty?.GetValue(runnableToDispose); + } + // Stop the coordinator if running Coordinator?.Stop (); @@ -97,6 +110,16 @@ public partial class ApplicationImpl } #endif + // Dispose the framework-owned runnable if it exists + if (runnableToDispose is { }) + { + if (runnableToDispose is IDisposable disposable) + { + disposable.Dispose(); + } + FrameworkOwnedRunnable = null; + } + // Clean up all application state (including sync context) // ResetState handles the case where Initialized is false ResetState (); @@ -113,6 +136,8 @@ public partial class ApplicationImpl // Clear the event to prevent memory leaks InitializedChanged = null; + + return result; } #if DEBUG @@ -156,9 +181,9 @@ public partial class ApplicationImpl TimedEvents?.StopAll (); // === 1. Stop all running toplevels === - foreach (Toplevel? t in SessionStack) + foreach (Toplevel t in SessionStack) { - t!.Running = false; + t.Running = false; } // === 2. Close and dispose popover === @@ -175,6 +200,7 @@ public partial class ApplicationImpl // === 3. Clean up toplevels === SessionStack.Clear (); + RunnableSessionStack?.Clear (); #if DEBUG_IDISPOSABLE @@ -222,6 +248,7 @@ public partial class ApplicationImpl // === 7. Clear navigation and screen state === ScreenChanged = null; + //Navigation = null; // === 8. Reset initialization state === diff --git a/Terminal.Gui/App/ApplicationImpl.Run.cs b/Terminal.Gui/App/ApplicationImpl.Run.cs index 3d03f7f60..944c64e09 100644 --- a/Terminal.Gui/App/ApplicationImpl.Run.cs +++ b/Terminal.Gui/App/ApplicationImpl.Run.cs @@ -165,7 +165,7 @@ public partial class ApplicationImpl /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public Toplevel Run (Func? errorHandler = null, string? driverName = null) { return Run (errorHandler, driverName); } + public Toplevel Run (Func? errorHandler = null, string? driverName = null) => Run (errorHandler, driverName); /// [RequiresUnreferencedCode ("AOT")] @@ -185,7 +185,6 @@ public partial class ApplicationImpl return top; } - /// public void Run (Toplevel view, Func? errorHandler = null) { @@ -222,7 +221,7 @@ public partial class ApplicationImpl if (StopAfterFirstIteration && firstIteration) { Logging.Information ("Run - Stopping after first iteration as requested"); - view.RequestStop (); + RequestStop ((Toplevel?)view); } firstIteration = false; @@ -291,7 +290,7 @@ public partial class ApplicationImpl } /// - public void RequestStop () { RequestStop (null); } + public void RequestStop () { RequestStop ((Toplevel?)null); } /// public void RequestStop (Toplevel? top) @@ -326,10 +325,10 @@ public partial class ApplicationImpl public ITimedEvents? TimedEvents => _timedEvents; /// - public object AddTimeout (TimeSpan time, Func callback) { return _timedEvents.Add (time, callback); } + public object AddTimeout (TimeSpan time, Func callback) => _timedEvents.Add (time, callback); /// - public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); } + public bool RemoveTimeout (object token) => _timedEvents.Remove (token); /// public void Invoke (Action? action) @@ -353,7 +352,6 @@ public partial class ApplicationImpl ); } - /// public void Invoke (Action action) { @@ -377,4 +375,300 @@ public partial class ApplicationImpl } #endregion Timeouts and Invoke + + #region IRunnable Support + + /// + public RunnableSessionToken Begin (IRunnable runnable) + { + ArgumentNullException.ThrowIfNull (runnable); + + // Ensure the mouse is ungrabbed + if (Mouse.MouseGrabView is { }) + { + Mouse.UngrabMouse (); + } + + // Create session token + RunnableSessionToken token = new (runnable); + + // Set the App property if the runnable is a View (needed for IsRunning/IsModal checks) + if (runnable is View runnableView) + { + runnableView.App = this; + } + + // Get old IsRunning and IsModal values BEFORE any stack changes + bool oldIsRunning = runnable.IsRunning; + bool oldIsModalValue = runnable.IsModal; + + // Raise IsRunningChanging (false -> true) - can be canceled + if (runnable.RaiseIsRunningChanging (oldIsRunning, true)) + { + // Starting was canceled + return token; + } + + // Push token onto RunnableSessionStack (IsRunning becomes true) + RunnableSessionStack?.Push (token); + + // Update TopRunnable to the new top of stack + IRunnable? previousTop = null; + + // In Phase 1, Toplevel doesn't implement IRunnable yet + // In Phase 2, it will, and this will work properly + if (TopRunnable is IRunnable r) + { + previousTop = r; + } + + // Set TopRunnable (handles both Toplevel and IRunnable) + if (runnable is Toplevel tl) + { + TopRunnable = tl; + } + else if (runnable is View v) + { + // For now, we can't set a non-Toplevel View as TopRunnable + // This is a limitation of the current architecture + // In Phase 2, we'll make TopRunnable an IRunnable property + Logging.Warning ($"WIP on Issue #4148 - Runnable '{runnable}' is a View but not a Toplevel; cannot set as TopRunnable"); + } + + // Raise IsRunningChanged (now true) + runnable.RaiseIsRunningChangedEvent (true); + + // If there was a previous top, it's no longer modal + if (previousTop != null) + { + // Get old IsModal value (should be true before becoming non-modal) + bool oldIsModal = previousTop.IsModal; + + // Raise IsModalChanging (true -> false) + previousTop.RaiseIsModalChanging (oldIsModal, false); + + // IsModal is now false (derived property) + previousTop.RaiseIsModalChangedEvent (false); + } + + // New runnable becomes modal + // Raise IsModalChanging (false -> true) using the old value we captured earlier + runnable.RaiseIsModalChanging (oldIsModalValue, true); + + // IsModal is now true (derived property) + runnable.RaiseIsModalChangedEvent (true); + + // Initialize if needed + if (runnable is View view && !view.IsInitialized) + { + view.BeginInit (); + view.EndInit (); + + // Initialized event is raised by View.EndInit() + } + + // Initial Layout and draw + LayoutAndDraw (true); + + // Set focus + if (runnable is View viewToFocus && !viewToFocus.HasFocus) + { + viewToFocus.SetFocus (); + } + + if (PositionCursor ()) + { + Driver?.UpdateCursor (); + } + + return token; + } + + /// + public void Run (IRunnable runnable, Func? errorHandler = null) + { + ArgumentNullException.ThrowIfNull (runnable); + + if (!Initialized) + { + throw new NotInitializedException (nameof (Run)); + } + + // Begin the session (adds to stack, raises IsRunningChanging/IsRunningChanged) + RunnableSessionToken token = Begin (runnable); + + try + { + // All runnables block until RequestStop() is called + RunLoop (runnable, errorHandler); + } + finally + { + // End the session (raises IsRunningChanging/IsRunningChanged, pops from stack) + End (token); + } + } + + /// + public IApplication Run (Func? errorHandler = null) where TRunnable : IRunnable, new () + { + if (!Initialized) + { + throw new NotInitializedException (nameof (Run)); + } + + TRunnable runnable = new (); + + // Store the runnable for automatic disposal by Shutdown + FrameworkOwnedRunnable = runnable; + + Run (runnable, errorHandler); + + return this; + } + + private void RunLoop (IRunnable runnable, Func? errorHandler) + { + // Main loop - blocks until RequestStop() is called + // Note: IsRunning is a derived property (stack.Contains), so we check it each iteration + var firstIteration = true; + + while (runnable.IsRunning) + { + if (Coordinator is null) + { + throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run"); + } + + try + { + // Process one iteration of the event loop + Coordinator.RunIteration (); + } + catch (Exception ex) + { + if (errorHandler is null || !errorHandler (ex)) + { + throw; + } + } + + if (StopAfterFirstIteration && firstIteration) + { + Logging.Information ("Run - Stopping after first iteration as requested"); + RequestStop (runnable); + } + + firstIteration = false; + } + } + + /// + public void End (RunnableSessionToken token) + { + ArgumentNullException.ThrowIfNull (token); + + if (token.Runnable is null) + { + return; // Already ended + } + + IRunnable runnable = token.Runnable; + + // Get old IsRunning value (should be true before stopping) + bool oldIsRunning = runnable.IsRunning; + + // Raise IsRunningChanging (true -> false) - can be canceled + // This is where Result should be extracted! + if (runnable.RaiseIsRunningChanging (oldIsRunning, false)) + { + // Stopping was canceled + return; + } + + // Current runnable is no longer modal + // Get old IsModal value (should be true before becoming non-modal) + bool oldIsModal = runnable.IsModal; + + // Raise IsModalChanging (true -> false) + runnable.RaiseIsModalChanging (oldIsModal, false); + + // IsModal is now false (will be false after pop) + runnable.RaiseIsModalChangedEvent (false); + + // Pop token from RunnableSessionStack (IsRunning becomes false) + if (RunnableSessionStack?.TryPop (out RunnableSessionToken? popped) == true && popped == token) + { + // Restore previous top runnable + if (RunnableSessionStack?.TryPeek (out RunnableSessionToken? previousToken) == true && previousToken?.Runnable is { }) + { + IRunnable? previousRunnable = previousToken.Runnable; + + // Update TopRunnable if it's a Toplevel + if (previousRunnable is Toplevel tl) + { + TopRunnable = tl; + } + + // Previous runnable becomes modal again + // Get old IsModal value (should be false before becoming modal again) + bool oldIsModalValue = previousRunnable.IsModal; + + // Raise IsModalChanging (false -> true) + previousRunnable.RaiseIsModalChanging (oldIsModalValue, true); + + // IsModal is now true (derived property) + previousRunnable.RaiseIsModalChangedEvent (true); + } + else + { + // No more runnables, clear TopRunnable + if (TopRunnable is IRunnable) + { + TopRunnable = null; + } + } + } + + // Raise IsRunningChanged (now false) + runnable.RaiseIsRunningChangedEvent (false); + + // Set focus to new TopRunnable if exists + if (TopRunnable is View viewToFocus && !viewToFocus.HasFocus) + { + viewToFocus.SetFocus (); + } + + // Clear the token + token.Runnable = null; + } + + /// + public void RequestStop (IRunnable? runnable) + { + // Get the runnable to stop + if (runnable is null) + { + // Try to get from TopRunnable + if (TopRunnable is IRunnable r) + { + runnable = r; + } + else + { + return; + } + } + + // For Toplevel, use the existing mechanism + if (runnable is Toplevel toplevel) + { + RequestStop (toplevel); + } + + // Note: The End() method will be called from the finally block in Run() + // and that's where IsRunningChanging/IsRunningChanged will be raised + } + + #endregion IRunnable Support } diff --git a/Terminal.Gui/App/ApplicationImpl.cs b/Terminal.Gui/App/ApplicationImpl.cs index 1aca088dd..2dc54cbda 100644 --- a/Terminal.Gui/App/ApplicationImpl.cs +++ b/Terminal.Gui/App/ApplicationImpl.cs @@ -25,10 +25,7 @@ public partial class ApplicationImpl : IApplication /// Configures the singleton instance of to use the specified backend implementation. /// /// - public static void SetInstance (IApplication? app) - { - _instance = app; - } + public static void SetInstance (IApplication? app) { _instance = app; } // Private static readonly Lazy instance of Application private static IApplication? _instance; @@ -42,7 +39,6 @@ public partial class ApplicationImpl : IApplication private string? _driverName; - #region Input private IMouse? _mouse; @@ -54,10 +50,7 @@ public partial class ApplicationImpl : IApplication { get { - if (_mouse is null) - { - _mouse = new MouseImpl { App = this }; - } + _mouse ??= new MouseImpl { App = this }; return _mouse; } @@ -73,10 +66,7 @@ public partial class ApplicationImpl : IApplication { get { - if (_keyboard is null) - { - _keyboard = new KeyboardImpl { App = this }; - } + _keyboard ??= new KeyboardImpl { App = this }; return _keyboard; } @@ -94,10 +84,7 @@ public partial class ApplicationImpl : IApplication { get { - if (_popover is null) - { - _popover = new () { App = this }; - } + _popover ??= new () { App = this }; return _popover; } @@ -111,10 +98,7 @@ public partial class ApplicationImpl : IApplication { get { - if (_navigation is null) - { - _navigation = new () { App = this }; - } + _navigation ??= new () { App = this }; return _navigation; } @@ -146,6 +130,12 @@ public partial class ApplicationImpl : IApplication /// public Toplevel? CachedSessionTokenToplevel { get; set; } + /// + public ConcurrentStack? RunnableSessionStack { get; } = new (); + + /// + public IRunnable? FrameworkOwnedRunnable { get; set; } + #endregion View Management /// diff --git a/Terminal.Gui/App/ApplicationRunnableExtensions.cs b/Terminal.Gui/App/ApplicationRunnableExtensions.cs new file mode 100644 index 000000000..7e706e9d5 --- /dev/null +++ b/Terminal.Gui/App/ApplicationRunnableExtensions.cs @@ -0,0 +1,158 @@ +namespace Terminal.Gui.App; + +/// +/// Extension methods for that enable running any as a runnable session. +/// +/// +/// These extensions provide convenience methods for wrapping views in +/// and running them in a single call, similar to how works. +/// +public static class ApplicationRunnableExtensions +{ + /// + /// Runs any View as a runnable session, extracting a typed result via a function. + /// + /// The type of view to run. + /// The type of result data to extract. + /// The application instance. Cannot be null. + /// The view to run as a blocking session. Cannot be null. + /// + /// Function that extracts the result from the view when stopping. + /// Called automatically when the runnable session ends. + /// + /// Optional handler for unhandled exceptions during the session. + /// The extracted result, or null if the session was canceled. + /// + /// Thrown if , , or is null. + /// + /// + /// + /// This method wraps the view in a , runs it as a blocking + /// session, and returns the extracted result. The wrapper is NOT disposed automatically; + /// the caller is responsible for disposal. + /// + /// + /// The result is extracted before the view is disposed, ensuring all data is still accessible. + /// + /// + /// + /// + /// var app = Application.Create(); + /// app.Init(); + /// + /// // Run a TextField and get the entered text + /// var text = app.RunView( + /// new TextField { Width = 40 }, + /// tf => tf.Text); + /// Console.WriteLine($"You entered: {text}"); + /// + /// // Run a ColorPicker and get the selected color + /// var color = app.RunView( + /// new ColorPicker(), + /// cp => cp.SelectedColor); + /// Console.WriteLine($"Selected color: {color}"); + /// + /// // Run a FlagSelector and get the selected flags + /// var flags = app.RunView( + /// new FlagSelector<SelectorStyles>(), + /// fs => fs.Value); + /// Console.WriteLine($"Selected styles: {flags}"); + /// + /// app.Shutdown(); + /// + /// + public static TResult? RunView ( + this IApplication app, + TView view, + Func resultExtractor, + Func? errorHandler = null) + where TView : View + { + if (app is null) + { + throw new ArgumentNullException (nameof (app)); + } + + if (view is null) + { + throw new ArgumentNullException (nameof (view)); + } + + if (resultExtractor is null) + { + throw new ArgumentNullException (nameof (resultExtractor)); + } + + var wrapper = new RunnableWrapper { WrappedView = view }; + + // Subscribe to IsRunningChanging to extract result when stopping + wrapper.IsRunningChanging += (s, e) => + { + if (!e.NewValue) // Stopping + { + wrapper.Result = resultExtractor (view); + } + }; + + app.Run (wrapper, errorHandler); + + return wrapper.Result; + } + + /// + /// Runs any View as a runnable session without result extraction. + /// + /// The type of view to run. + /// The application instance. Cannot be null. + /// The view to run as a blocking session. Cannot be null. + /// Optional handler for unhandled exceptions during the session. + /// The view that was run, allowing access to its state after the session ends. + /// Thrown if or is null. + /// + /// + /// This method wraps the view in a and runs it as a blocking + /// session. The wrapper is NOT disposed automatically; the caller is responsible for disposal. + /// + /// + /// Use this overload when you don't need automatic result extraction, but still want the view + /// to run as a blocking session. Access the view's properties directly after running. + /// + /// + /// + /// + /// var app = Application.Create(); + /// app.Init(); + /// + /// // Run a ColorPicker without automatic result extraction + /// var colorPicker = new ColorPicker(); + /// app.RunView(colorPicker); + /// + /// // Access the view's state directly + /// Console.WriteLine($"Selected: {colorPicker.SelectedColor}"); + /// + /// app.Shutdown(); + /// + /// + public static TView RunView ( + this IApplication app, + TView view, + Func? errorHandler = null) + where TView : View + { + if (app is null) + { + throw new ArgumentNullException (nameof (app)); + } + + if (view is null) + { + throw new ArgumentNullException (nameof (view)); + } + + var wrapper = new RunnableWrapper { WrappedView = view }; + + app.Run (wrapper, errorHandler); + + return view; + } +} diff --git a/Terminal.Gui/App/IApplication.cs b/Terminal.Gui/App/IApplication.cs index 88e1d2d1f..f663a351f 100644 --- a/Terminal.Gui/App/IApplication.cs +++ b/Terminal.Gui/App/IApplication.cs @@ -44,6 +44,7 @@ public interface IApplication /// The short name (e.g. "dotnet", "windows", "unix", or "fake") of the /// to use. If not specified the default driver for the platform will be used. /// + /// This instance for fluent API chaining. /// /// Call this method once per instance (or after has been called). /// @@ -52,17 +53,20 @@ public interface IApplication /// /// /// must be called when the application is closing (typically after - /// has returned) to ensure resources are cleaned up and terminal settings restored. + /// has returned) to ensure resources are cleaned up and terminal settings restored. /// /// - /// The function combines and + /// The function combines and /// into a single call. An application can use - /// without explicitly calling . + /// without explicitly calling . + /// + /// + /// Supports fluent API: Application.Create().Init().Run<MyView>().Shutdown() /// /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public void Init (string? driverName = null); + public IApplication Init (string? driverName = null); /// /// This event is raised after the and methods have been called. @@ -76,12 +80,25 @@ public interface IApplication bool Initialized { get; set; } /// Shutdown an application initialized with . + /// + /// The result from the last call, or if none. + /// Automatically disposes any runnable created by . + /// /// - /// Shutdown must be called for every call to or - /// to ensure all resources are cleaned - /// up (Disposed) and terminal settings are restored. + /// + /// Shutdown must be called for every call to or + /// to ensure all resources are cleaned + /// up (Disposed) and terminal settings are restored. + /// + /// + /// When used in a fluent chain with , this method automatically + /// disposes the runnable instance and extracts its result for return. + /// + /// + /// Supports fluent API: var result = Application.Create().Init().Run<MyView>().Shutdown() as MyResultType + /// /// - public void Shutdown (); + public object? Shutdown (); /// /// Resets the state of this instance. @@ -177,7 +194,7 @@ public interface IApplication /// . /// /// - /// When using or , + /// When using or , /// will be called automatically. /// /// @@ -225,7 +242,7 @@ public interface IApplication /// . /// /// - /// When using or , + /// When using or , /// will be called automatically. /// /// @@ -301,14 +318,16 @@ public interface IApplication /// /// This will cause to return. /// - /// This is equivalent to calling with as the parameter. + /// This is equivalent to calling with as the + /// parameter. /// /// void RequestStop (); /// Requests that the currently running Session stop. The Session will stop after the current iteration completes. /// - /// The to stop. If , stops the currently running . + /// The to stop. If , stops the currently running + /// . /// /// /// This will cause to return. @@ -324,7 +343,7 @@ public interface IApplication /// /// /// - /// Used primarily for unit testing. When , will be called + /// Used primarily for unit testing. When , will be called /// automatically after the first main loop iteration. /// /// @@ -386,6 +405,165 @@ public interface IApplication #endregion Toplevel Management + #region IRunnable Management + + /// + /// Gets the stack of all active runnable session tokens. + /// Sessions execute serially - the top of stack is the currently modal session. + /// + /// + /// + /// Session tokens are pushed onto the stack when is called and + /// popped when + /// completes. The stack grows during nested modal calls and + /// shrinks as they complete. + /// + /// + /// Only the top session () has exclusive keyboard/mouse input ( + /// = true). + /// All other sessions on the stack continue to be laid out, drawn, and receive iteration events ( + /// = true), + /// but they don't receive user input. + /// + /// + /// Stack during nested modals: + /// + /// RunnableSessionStack (top to bottom): + /// - MessageBox (TopRunnable, IsModal=true, IsRunning=true, has input) + /// - FileDialog (IsModal=false, IsRunning=true, continues to update/draw) + /// - MainWindow (IsModal=false, IsRunning=true, continues to update/draw) + /// + /// + /// + ConcurrentStack? RunnableSessionStack { get; } + + /// + /// Gets or sets the runnable that was created by for automatic disposal. + /// + /// + /// + /// When creates a runnable instance, it stores it here so + /// can automatically dispose it and extract its result. + /// + /// + /// This property is if was used + /// with an externally-created runnable. + /// + /// + IRunnable? FrameworkOwnedRunnable { get; set; } + + /// + /// Building block API: Creates a and prepares the provided + /// for + /// execution. Not usually called directly by applications. Use + /// instead. + /// + /// The to prepare execution for. + /// + /// The that needs to be passed to the + /// method upon + /// completion. + /// + /// + /// + /// This method prepares the provided for running. It adds this to the + /// , lays out the SubViews, focuses the first element, and draws the + /// runnable on the screen. This is usually followed by starting the main loop, and then the + /// method upon termination which will undo these changes. + /// + /// + /// Raises the , , + /// , and events. + /// + /// + RunnableSessionToken Begin (IRunnable runnable); + + /// + /// Runs a new Session with the provided runnable view. + /// + /// The runnable to execute. + /// Optional handler for unhandled exceptions (resumes when returns true, rethrows when null). + /// + /// + /// This method is used to start processing events for the main application, but it is also used to run other + /// modal views such as dialogs. + /// + /// + /// To make stop execution, call + /// or . + /// + /// + /// Calling is equivalent to calling + /// , followed by starting the main loop, and then calling + /// . + /// + /// + /// In RELEASE builds: When is any exceptions will be + /// rethrown. Otherwise, will be called. If + /// returns the main loop will resume; otherwise this method will exit. + /// + /// + void Run (IRunnable runnable, Func? errorHandler = null); + + /// + /// Creates and runs a new session with a of the specified type. + /// + /// The type of runnable to create and run. Must have a parameterless constructor. + /// Optional handler for unhandled exceptions (resumes when returns true, rethrows when null). + /// This instance for fluent API chaining. The created runnable is stored internally for disposal. + /// + /// + /// This is a convenience method that creates an instance of and runs it. + /// The framework owns the created instance and will automatically dispose it when is called. + /// + /// + /// To access the result, use which returns the result from . + /// + /// + /// Supports fluent API: var result = Application.Create().Init().Run<MyView>().Shutdown() as MyResultType + /// + /// + IApplication Run (Func? errorHandler = null) where TRunnable : IRunnable, new (); + + /// + /// Requests that the specified runnable session stop. + /// + /// The runnable to stop. If , stops the current . + /// + /// + /// This will cause to return. + /// + /// + /// Raises , , + /// , and events. + /// + /// + void RequestStop (IRunnable? runnable); + + /// + /// Building block API: Ends the session associated with the token and completes the execution of an + /// . + /// Not usually called directly by applications. + /// will automatically call this method when the session is stopped. + /// + /// + /// The returned by the + /// method. + /// + /// + /// + /// This method removes the from the , + /// raises the lifecycle events, and disposes the . + /// + /// + /// Raises , , + /// , and events. + /// + /// + void End (RunnableSessionToken sessionToken); + + #endregion IRunnable Management + #region Screen and Driver /// Gets or sets the console driver being used. diff --git a/Terminal.Gui/App/Runnable/IRunnable.cs b/Terminal.Gui/App/Runnable/IRunnable.cs new file mode 100644 index 000000000..2e6711d0c --- /dev/null +++ b/Terminal.Gui/App/Runnable/IRunnable.cs @@ -0,0 +1,233 @@ +namespace Terminal.Gui.App; + +/// +/// Non-generic base interface for runnable views. Provides common members without type parameter. +/// +/// +/// +/// This interface enables storing heterogeneous runnables in collections (e.g., +/// ) +/// while preserving type safety at usage sites via . +/// +/// +/// Most code should use directly. This base interface is primarily +/// for framework infrastructure (session management, stacking, etc.). +/// +/// +/// A runnable view executes as a self-contained blocking session with its own lifecycle, +/// event loop iteration, and focus management./> +/// blocks until +/// is called. +/// +/// +/// This interface follows the Terminal.Gui Cancellable Work Pattern (CWP) for all lifecycle events. +/// +/// +/// +/// +public interface IRunnable +{ + #region Running or not (added to/removed from RunnableSessionStack) + + /// + /// Gets whether this runnable session is currently running (i.e., on the + /// ). + /// + /// + /// + /// Read-only property derived from stack state. Returns if this runnable + /// is currently on the , otherwise. + /// + /// + /// Runnables are added to the stack during and removed in + /// . + /// + /// + bool IsRunning { get; } + + /// + /// Called by the framework to raise the event. + /// + /// The current value of . + /// The new value of (true = starting, false = stopping). + /// if the change was canceled; otherwise . + /// + /// + /// This method implements the Cancellable Work Pattern. It calls the protected virtual method first, + /// then raises the event if not canceled. + /// + /// + /// When is (stopping), this is the ideal place + /// for implementations to extract Result from views before the runnable is removed from the stack. + /// + /// + bool RaiseIsRunningChanging (bool oldIsRunning, bool newIsRunning); + + /// + /// Raised when is changing (e.g., when or + /// is called). + /// Can be canceled by setting to . + /// + /// + /// + /// Subscribe to this event to participate in the runnable lifecycle before state changes occur. + /// When is (stopping), + /// this is the ideal place to extract Result before views are disposed and to optionally + /// cancel the stop operation (e.g., prompt to save changes). + /// + /// + /// This event follows the Terminal.Gui Cancellable Work Pattern (CWP). + /// + /// + event EventHandler>? IsRunningChanging; + + /// + /// Called by the framework to raise the event. + /// + /// The new value of (true = started, false = stopped). + /// + /// This method is called after the state change has occurred and cannot be canceled. + /// + void RaiseIsRunningChangedEvent (bool newIsRunning); + + /// + /// Raised after has changed (after the runnable has been added to or removed from the + /// ). + /// + /// + /// + /// Subscribe to this event to perform post-state-change logic. When is + /// , + /// the runnable has started and is on the stack. When , the runnable has stopped and been + /// removed from the stack. + /// + /// + /// This event follows the Terminal.Gui Cancellable Work Pattern (CWP). + /// + /// + event EventHandler>? IsRunningChanged; + + #endregion Running or not (added to/removed from RunnableSessionStack) + + #region Modal or not (top of RunnableSessionStack or not) + + /// + /// Gets whether this runnable session is at the top of the and thus + /// exclusively receiving mouse and keyboard input. + /// + /// + /// + /// Read-only property derived from stack state. Returns if this runnable + /// is at the top of the stack (i.e., this == app.TopRunnable), otherwise. + /// + /// + /// The runnable at the top of the stack gets all mouse/keyboard input and thus is running "modally". + /// + /// + bool IsModal { get; } + + /// + /// Called by the framework to raise the event. + /// + /// The current value of . + /// The new value of (true = becoming modal/top, false = no longer modal). + /// if the change was canceled; otherwise . + /// + /// This method implements the Cancellable Work Pattern. It calls the protected virtual method first, + /// then raises the event if not canceled. + /// + bool RaiseIsModalChanging (bool oldIsModal, bool newIsModal); + + /// + /// Raised when this runnable is about to become modal (top of stack) or cease being modal. + /// Can be canceled by setting to . + /// + /// + /// + /// Subscribe to this event to participate in modal state transitions before they occur. + /// When is , the runnable is becoming modal (top + /// of stack). + /// When , another runnable is becoming modal and this one will no longer receive input. + /// + /// + /// This event follows the Terminal.Gui Cancellable Work Pattern (CWP). + /// + /// + event EventHandler>? IsModalChanging; + + /// + /// Called by the framework to raise the event. + /// + /// The new value of (true = became modal/top, false = no longer modal). + /// + /// This method is called after the modal state change has occurred and cannot be canceled. + /// + void RaiseIsModalChangedEvent (bool newIsModal); + + /// + /// Raised after this runnable has become modal (top of stack) or ceased being modal. + /// + /// + /// + /// Subscribe to this event to perform post-activation logic (e.g., setting focus, updating UI state). + /// When is , the runnable became modal (top of + /// stack). + /// When , the runnable is no longer modal (another runnable is on top). + /// + /// + /// This event follows the Terminal.Gui Cancellable Work Pattern (CWP). + /// + /// + event EventHandler>? IsModalChanged; + + #endregion Modal or not (top of RunnableSessionStack or not) +} + +/// +/// Defines a view that can be run as an independent blocking session with , +/// returning a typed result. +/// +/// +/// The type of result data returned when the session completes. +/// Common types: for button indices, for file paths, +/// custom types for complex form data. +/// +/// +/// +/// A runnable view executes as a self-contained blocking session with its own lifecycle, +/// event loop iteration, and focus management. blocks until +/// is called. +/// +/// +/// When is , the session was stopped without being accepted +/// (e.g., ESC key pressed, window closed). When non-, it contains the result data +/// extracted in (when stopping) before views are disposed. +/// +/// +/// Implementing does not require deriving from any specific +/// base class or using . These are orthogonal concerns. +/// +/// +/// This interface follows the Terminal.Gui Cancellable Work Pattern (CWP) for all lifecycle events. +/// +/// +/// +/// +public interface IRunnable : IRunnable +{ + /// + /// Gets or sets the result data extracted when the session was accepted, or if not accepted. + /// + /// + /// + /// Implementations should set this in the method + /// (when stopping, i.e., newIsRunning == false) by extracting data from + /// views before they are disposed. + /// + /// + /// indicates the session was stopped without accepting (ESC key, close without action). + /// Non- contains the type-safe result data. + /// + /// + TResult? Result { get; set; } +} diff --git a/Terminal.Gui/App/Toplevel/IToplevelTransitionManager.cs b/Terminal.Gui/App/Runnable/IToplevelTransitionManager.cs similarity index 100% rename from Terminal.Gui/App/Toplevel/IToplevelTransitionManager.cs rename to Terminal.Gui/App/Runnable/IToplevelTransitionManager.cs diff --git a/Terminal.Gui/App/Runnable/RunnableSessionToken.cs b/Terminal.Gui/App/Runnable/RunnableSessionToken.cs new file mode 100644 index 000000000..0f386b635 --- /dev/null +++ b/Terminal.Gui/App/Runnable/RunnableSessionToken.cs @@ -0,0 +1,87 @@ +using System.Collections.Concurrent; + +namespace Terminal.Gui.App; + +/// +/// Represents a running session created by . +/// Wraps an instance and is stored in . +/// +public class RunnableSessionToken : IDisposable +{ + internal RunnableSessionToken (IRunnable runnable) { Runnable = runnable; } + + /// + /// Gets or sets the runnable associated with this session. + /// Set to by when the session completes. + /// + public IRunnable? Runnable { get; internal set; } + + /// + /// Releases all resource used by the object. + /// + /// + /// + /// Call when you are finished using the . + /// + /// + /// method leaves the in an unusable state. After + /// calling + /// , you must release all references to the so the + /// garbage collector can + /// reclaim the memory that the was occupying. + /// + /// + public void Dispose () + { + Dispose (true); + GC.SuppressFinalize (this); + +#if DEBUG_IDISPOSABLE + WasDisposed = true; +#endif + } + + /// + /// Releases all resource used by the object. + /// + /// If set to we are disposing and should dispose held objects. + protected virtual void Dispose (bool disposing) + { + if (Runnable is { } && disposing) + { + // Runnable must be null before disposing + throw new InvalidOperationException ( + "Runnable must be null before calling RunnableSessionToken.Dispose" + ); + } + } + +#if DEBUG_IDISPOSABLE +#pragma warning disable CS0419 // Ambiguous reference in cref attribute + /// + /// Gets whether was called on this RunnableSessionToken or not. + /// For debug purposes to verify objects are being disposed properly. + /// Only valid when DEBUG_IDISPOSABLE is defined. + /// + public bool WasDisposed { get; private set; } + + /// + /// Gets the number of times was called on this object. + /// For debug purposes to verify objects are being disposed properly. + /// Only valid when DEBUG_IDISPOSABLE is defined. + /// + public int DisposedCount { get; private set; } + + /// + /// Gets the list of RunnableSessionToken objects that have been created and not yet disposed. + /// Note, this is a static property and will affect all RunnableSessionToken objects. + /// For debug purposes to verify objects are being disposed properly. + /// Only valid when DEBUG_IDISPOSABLE is defined. + /// + public static ConcurrentBag Instances { get; } = []; + + /// Creates a new RunnableSessionToken object. + public RunnableSessionToken () { Instances.Add (this); } +#pragma warning restore CS0419 // Ambiguous reference in cref attribute +#endif +} diff --git a/Terminal.Gui/App/SessionToken.cs b/Terminal.Gui/App/Runnable/SessionToken.cs similarity index 100% rename from Terminal.Gui/App/SessionToken.cs rename to Terminal.Gui/App/Runnable/SessionToken.cs diff --git a/Terminal.Gui/App/SessionTokenEventArgs.cs b/Terminal.Gui/App/Runnable/SessionTokenEventArgs.cs similarity index 100% rename from Terminal.Gui/App/SessionTokenEventArgs.cs rename to Terminal.Gui/App/Runnable/SessionTokenEventArgs.cs diff --git a/Terminal.Gui/App/Toplevel/ToplevelTransitionManager.cs b/Terminal.Gui/App/Runnable/ToplevelTransitionManager.cs similarity index 100% rename from Terminal.Gui/App/Toplevel/ToplevelTransitionManager.cs rename to Terminal.Gui/App/Runnable/ToplevelTransitionManager.cs diff --git a/Terminal.Gui/ViewBase/Runnable.cs b/Terminal.Gui/ViewBase/Runnable.cs new file mode 100644 index 000000000..eedbc09f1 --- /dev/null +++ b/Terminal.Gui/ViewBase/Runnable.cs @@ -0,0 +1,223 @@ +namespace Terminal.Gui.ViewBase; + +/// +/// Base implementation of for views that can be run as blocking sessions. +/// +/// The type of result data returned when the session completes. +/// +/// +/// Views can derive from this class or implement directly. +/// +/// +/// This class provides default implementations of the interface +/// following the Terminal.Gui Cancellable Work Pattern (CWP). +/// +/// +public class Runnable : View, IRunnable +{ + /// + public TResult? Result { get; set; } + + #region IRunnable Implementation - IsRunning (from base interface) + + /// + public bool IsRunning => App?.RunnableSessionStack?.Any (token => token.Runnable == this) ?? false; + + /// + public bool RaiseIsRunningChanging (bool oldIsRunning, bool newIsRunning) + { + // Clear previous result when starting + if (newIsRunning) + { + Result = default (TResult); + } + + // CWP Phase 1: Virtual method (pre-notification) + if (OnIsRunningChanging (oldIsRunning, newIsRunning)) + { + return true; // Canceled + } + + // CWP Phase 2: Event notification + bool newValue = newIsRunning; + CancelEventArgs args = new (in oldIsRunning, ref newValue); + IsRunningChanging?.Invoke (this, args); + + return args.Cancel; + } + + /// + public event EventHandler>? IsRunningChanging; + + /// + public void RaiseIsRunningChangedEvent (bool newIsRunning) + { + // CWP Phase 3: Post-notification (work already done by Application.Begin/End) + OnIsRunningChanged (newIsRunning); + + EventArgs args = new (newIsRunning); + IsRunningChanged?.Invoke (this, args); + } + + /// + public event EventHandler>? IsRunningChanged; + + /// + /// Called before event. Override to cancel state change or extract + /// . + /// + /// The current value of . + /// The new value of (true = starting, false = stopping). + /// to cancel; to proceed. + /// + /// + /// Default implementation returns (allow change). + /// + /// + /// IMPORTANT: When is (stopping), this is the ideal + /// place + /// to extract from views before the runnable is removed from the stack. + /// At this point, all views are still alive and accessible, and subscribers can inspect the result + /// and optionally cancel the stop. + /// + /// + /// + /// protected override bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning) + /// { + /// if (!newIsRunning) // Stopping + /// { + /// // Extract result before removal from stack + /// Result = _textField.Text; + /// + /// // Or check if user wants to save first + /// if (HasUnsavedChanges ()) + /// { + /// int result = MessageBox.Query ("Save?", "Save changes?", "Yes", "No", "Cancel"); + /// if (result == 2) return true; // Cancel stopping + /// if (result == 0) Save (); + /// } + /// } + /// + /// return base.OnIsRunningChanging (oldIsRunning, newIsRunning); + /// } + /// + /// + /// + protected virtual bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning) => false; + + /// + /// Called after has changed. Override for post-state-change logic. + /// + /// The new value of (true = started, false = stopped). + /// + /// Default implementation does nothing. Overrides should call base to ensure extensibility. + /// + protected virtual void OnIsRunningChanged (bool newIsRunning) + { + // Default: no-op + } + + #endregion + + #region IRunnable Implementation - IsModal (from base interface) + + /// + public bool IsModal + { + get + { + if (App is null) + { + return false; + } + + // Check if this runnable is at the top of the RunnableSessionStack + // The top of the stack is the modal runnable + if (App.RunnableSessionStack is { } && App.RunnableSessionStack.TryPeek (out RunnableSessionToken? topToken)) + { + return topToken?.Runnable == this; + } + + // Fallback: Check if this is the TopRunnable (for Toplevel compatibility) + // In Phase 1, TopRunnable is still Toplevel?, so we need to check both cases + if (this is Toplevel tl && App.TopRunnable == tl) + { + return true; + } + + return false; + } + } + + /// + public bool RaiseIsModalChanging (bool oldIsModal, bool newIsModal) + { + // CWP Phase 1: Virtual method (pre-notification) + if (OnIsModalChanging (oldIsModal, newIsModal)) + { + return true; // Canceled + } + + // CWP Phase 2: Event notification + bool newValue = newIsModal; + CancelEventArgs args = new (in oldIsModal, ref newValue); + IsModalChanging?.Invoke (this, args); + + return args.Cancel; + } + + /// + public event EventHandler>? IsModalChanging; + + /// + public void RaiseIsModalChangedEvent (bool newIsModal) + { + // CWP Phase 3: Post-notification (work already done by Application) + OnIsModalChanged (newIsModal); + + EventArgs args = new (newIsModal); + IsModalChanged?.Invoke (this, args); + } + + /// + public event EventHandler>? IsModalChanged; + + /// + /// Called before event. Override to cancel activation/deactivation. + /// + /// The current value of . + /// The new value of (true = becoming modal/top, false = no longer modal). + /// to cancel; to proceed. + /// + /// Default implementation returns (allow change). + /// + protected virtual bool OnIsModalChanging (bool oldIsModal, bool newIsModal) => false; + + /// + /// Called after has changed. Override for post-activation logic. + /// + /// The new value of (true = became modal, false = no longer modal). + /// + /// + /// Default implementation does nothing. Overrides should call base to ensure extensibility. + /// + /// + /// Common uses: setting focus when becoming modal, updating UI state. + /// + /// + protected virtual void OnIsModalChanged (bool newIsModal) + { + // Default: no-op + } + + #endregion + + /// + /// Requests that this runnable session stop. + /// + public virtual void RequestStop () + { + // Use the IRunnable-specific RequestStop if the App supports it + App?.RequestStop (this); + } +} diff --git a/Terminal.Gui/ViewBase/RunnableWrapper.cs b/Terminal.Gui/ViewBase/RunnableWrapper.cs new file mode 100644 index 000000000..5f1d46136 --- /dev/null +++ b/Terminal.Gui/ViewBase/RunnableWrapper.cs @@ -0,0 +1,90 @@ +namespace Terminal.Gui.ViewBase; + +/// +/// Wraps any to make it runnable with a typed result, similar to how +/// wraps . +/// +/// The type of view being wrapped. +/// The type of result data returned when the session completes. +/// +/// +/// This class enables any View to be run as a blocking session with +/// without requiring the View to implement or derive from +/// . +/// +/// +/// Use for a fluent API approach, +/// or to run directly. +/// +/// +/// +/// // Wrap a TextField to make it runnable with string result +/// var textField = new TextField { Width = 40 }; +/// var runnable = new RunnableWrapper<TextField, string> { WrappedView = textField }; +/// +/// // Extract result when stopping +/// runnable.IsRunningChanging += (s, e) => +/// { +/// if (!e.NewValue) // Stopping +/// { +/// runnable.Result = runnable.WrappedView.Text; +/// } +/// }; +/// +/// app.Run(runnable); +/// Console.WriteLine($"User entered: {runnable.Result}"); +/// runnable.Dispose(); +/// +/// +/// +public class RunnableWrapper : Runnable where TView : View +{ + /// + /// Initializes a new instance of . + /// + public RunnableWrapper () + { + // Make the wrapper automatically size to fit the wrapped view + Width = Dim.Fill (); + Height = Dim.Fill (); + } + + private TView? _wrappedView; + + /// + /// Gets or sets the wrapped view that is being made runnable. + /// + /// + /// + /// This property must be set before the wrapper is initialized. + /// Access this property to interact with the original view, extract its state, + /// or configure result extraction logic. + /// + /// + /// Thrown if the property is set after initialization. + public required TView WrappedView + { + get => _wrappedView ?? throw new InvalidOperationException ("WrappedView must be set before use."); + init + { + if (IsInitialized) + { + throw new InvalidOperationException ("WrappedView cannot be changed after initialization."); + } + + _wrappedView = value; + } + } + + /// + public override void EndInit () + { + base.EndInit (); + + // Add the wrapped view as a subview after initialization + if (_wrappedView is { }) + { + Add (_wrappedView); + } + } +} diff --git a/Terminal.Gui/ViewBase/ViewRunnableExtensions.cs b/Terminal.Gui/ViewBase/ViewRunnableExtensions.cs new file mode 100644 index 000000000..7b12bb055 --- /dev/null +++ b/Terminal.Gui/ViewBase/ViewRunnableExtensions.cs @@ -0,0 +1,126 @@ +namespace Terminal.Gui.ViewBase; + +/// +/// Extension methods for making any runnable with typed results. +/// +/// +/// These extensions provide a fluent API for wrapping views in , +/// enabling any View to be run as a blocking session without implementing . +/// +public static class ViewRunnableExtensions +{ + /// + /// Converts any View into a runnable with typed result extraction. + /// + /// The type of view to make runnable. + /// The type of result data to extract. + /// The view to wrap. Cannot be null. + /// + /// Function that extracts the result from the view when stopping. + /// Called automatically when the runnable session ends. + /// + /// A that wraps the view. + /// Thrown if or is null. + /// + /// + /// This method wraps the view in a and automatically + /// subscribes to to extract the result when the session stops. + /// + /// + /// The result is extracted before the view is disposed, ensuring all data is still accessible. + /// + /// + /// + /// + /// // Make a TextField runnable with string result + /// var runnable = new TextField { Width = 40 } + /// .AsRunnable(tf => tf.Text); + /// + /// app.Run(runnable); + /// Console.WriteLine($"User entered: {runnable.Result}"); + /// runnable.Dispose(); + /// + /// // Make a ColorPicker runnable with Color? result + /// var colorRunnable = new ColorPicker() + /// .AsRunnable(cp => cp.SelectedColor); + /// + /// app.Run(colorRunnable); + /// Console.WriteLine($"Selected: {colorRunnable.Result}"); + /// colorRunnable.Dispose(); + /// + /// // Make a FlagSelector runnable with enum result + /// var flagsRunnable = new FlagSelector<SelectorStyles>() + /// .AsRunnable(fs => fs.Value); + /// + /// app.Run(flagsRunnable); + /// Console.WriteLine($"Selected styles: {flagsRunnable.Result}"); + /// flagsRunnable.Dispose(); + /// + /// + public static RunnableWrapper AsRunnable ( + this TView view, + Func resultExtractor) + where TView : View + { + if (view is null) + { + throw new ArgumentNullException (nameof (view)); + } + + if (resultExtractor is null) + { + throw new ArgumentNullException (nameof (resultExtractor)); + } + + var wrapper = new RunnableWrapper { WrappedView = view }; + + // Subscribe to IsRunningChanging to extract result when stopping + wrapper.IsRunningChanging += (s, e) => + { + if (!e.NewValue) // Stopping + { + wrapper.Result = resultExtractor (view); + } + }; + + return wrapper; + } + + /// + /// Converts any View into a runnable without result extraction. + /// + /// The type of view to make runnable. + /// The view to wrap. Cannot be null. + /// A that wraps the view. + /// Thrown if is null. + /// + /// + /// Use this overload when you don't need to extract a typed result, but still want to + /// run the view as a blocking session. The wrapped view can still be accessed via + /// after running. + /// + /// + /// + /// + /// // Make a view runnable without result extraction + /// var colorPicker = new ColorPicker(); + /// var runnable = colorPicker.AsRunnable(); + /// + /// app.Run(runnable); + /// + /// // Access the wrapped view directly to get the result + /// Console.WriteLine($"Selected: {runnable.WrappedView.SelectedColor}"); + /// runnable.Dispose(); + /// + /// + public static RunnableWrapper AsRunnable (this TView view) + where TView : View + { + if (view is null) + { + throw new ArgumentNullException (nameof (view)); + } + + return new RunnableWrapper { WrappedView = view }; + } +} diff --git a/Terminal.sln b/Terminal.sln index 050122e35..b2fb285cd 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -122,6 +122,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{1A3C Tests\UnitTests\runsettings.xml = Tests\UnitTests\runsettings.xml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentExample", "Examples\FluentExample\FluentExample.csproj", "{8C05292F-86C9-C29A-635B-A4DFC5955D1C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RunnableWrapperExample", "Examples\RunnableWrapperExample\RunnableWrapperExample.csproj", "{26FDEE3C-9D1F-79A6-F48F-D0944C7F09F8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -196,6 +200,14 @@ Global {8C643A64-2A77-4432-987A-2E72BD9708E3}.Debug|Any CPU.Build.0 = Debug|Any CPU {8C643A64-2A77-4432-987A-2E72BD9708E3}.Release|Any CPU.ActiveCfg = Release|Any CPU {8C643A64-2A77-4432-987A-2E72BD9708E3}.Release|Any CPU.Build.0 = Release|Any CPU + {8C05292F-86C9-C29A-635B-A4DFC5955D1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C05292F-86C9-C29A-635B-A4DFC5955D1C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C05292F-86C9-C29A-635B-A4DFC5955D1C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C05292F-86C9-C29A-635B-A4DFC5955D1C}.Release|Any CPU.Build.0 = Release|Any CPU + {26FDEE3C-9D1F-79A6-F48F-D0944C7F09F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26FDEE3C-9D1F-79A6-F48F-D0944C7F09F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26FDEE3C-9D1F-79A6-F48F-D0944C7F09F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26FDEE3C-9D1F-79A6-F48F-D0944C7F09F8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings index ef25662a4..e6cac2cdd 100644 --- a/Terminal.sln.DotSettings +++ b/Terminal.sln.DotSettings @@ -417,12 +417,14 @@ True True True + True True True True True True True + True True True True diff --git a/Tests/UnitTests/Application/ApplicationImplBeginEndTests.cs b/Tests/UnitTests/Application/ApplicationImplBeginEndTests.cs index ad48cf802..956964738 100644 --- a/Tests/UnitTests/Application/ApplicationImplBeginEndTests.cs +++ b/Tests/UnitTests/Application/ApplicationImplBeginEndTests.cs @@ -8,11 +8,9 @@ namespace UnitTests.ApplicationTests; /// These tests ensure the fragile state management logic is robust and catches regressions. /// Tests work directly with ApplicationImpl instances to avoid global Application state issues. /// -public class ApplicationImplBeginEndTests +public class ApplicationImplBeginEndTests (ITestOutputHelper output) { - private readonly ITestOutputHelper _output; - - public ApplicationImplBeginEndTests (ITestOutputHelper output) { _output = output; } + private readonly ITestOutputHelper _output = output; private IApplication NewApplicationImpl () { @@ -28,7 +26,7 @@ public class ApplicationImplBeginEndTests try { - Assert.Throws (() => app.Begin (null!)); + Assert.Throws (() => app.Begin ((Toplevel)null!)); } finally { @@ -69,8 +67,8 @@ public class ApplicationImplBeginEndTests try { - toplevel1 = new() { Id = "1" }; - toplevel2 = new() { Id = "2" }; + toplevel1 = new () { Id = "1" }; + toplevel2 = new () { Id = "2" }; app.Begin (toplevel1); Assert.Single (app.SessionStack); @@ -135,7 +133,7 @@ public class ApplicationImplBeginEndTests try { - Assert.Throws (() => app.End (null!)); + Assert.Throws (() => app.End ((SessionToken)null!)); } finally { @@ -152,8 +150,8 @@ public class ApplicationImplBeginEndTests try { - toplevel1 = new() { Id = "1" }; - toplevel2 = new() { Id = "2" }; + toplevel1 = new () { Id = "1" }; + toplevel2 = new () { Id = "2" }; SessionToken token1 = app.Begin (toplevel1); SessionToken token2 = app.Begin (toplevel2); @@ -186,8 +184,8 @@ public class ApplicationImplBeginEndTests try { - toplevel1 = new() { Id = "1" }; - toplevel2 = new() { Id = "2" }; + toplevel1 = new () { Id = "1" }; + toplevel2 = new () { Id = "2" }; SessionToken token1 = app.Begin (toplevel1); SessionToken token2 = app.Begin (toplevel2); @@ -220,9 +218,9 @@ public class ApplicationImplBeginEndTests try { - toplevel1 = new() { Id = "1" }; - toplevel2 = new() { Id = "2" }; - toplevel3 = new() { Id = "3" }; + toplevel1 = new () { Id = "1" }; + toplevel2 = new () { Id = "2" }; + toplevel3 = new () { Id = "3" }; SessionToken token1 = app.Begin (toplevel1); SessionToken token2 = app.Begin (toplevel2); @@ -351,8 +349,8 @@ public class ApplicationImplBeginEndTests try { - toplevel1 = new() { Id = "1" }; - toplevel2 = new() { Id = "2" }; + toplevel1 = new () { Id = "1" }; + toplevel2 = new () { Id = "2" }; app.Begin (toplevel1); app.Begin (toplevel2); @@ -385,8 +383,8 @@ public class ApplicationImplBeginEndTests try { - toplevel1 = new() { Id = "1", Running = true }; - toplevel2 = new() { Id = "2", Running = true }; + toplevel1 = new () { Id = "1", Running = true }; + toplevel2 = new () { Id = "2", Running = true }; app.Begin (toplevel1); app.Begin (toplevel2); @@ -418,8 +416,8 @@ public class ApplicationImplBeginEndTests try { - toplevel1 = new() { Id = "1" }; - toplevel2 = new() { Id = "2" }; + toplevel1 = new () { Id = "1" }; + toplevel2 = new () { Id = "2" }; var toplevel1Deactivated = false; var toplevel2Activated = false; @@ -450,7 +448,7 @@ public class ApplicationImplBeginEndTests try { - toplevel = new() { Id = "test-id" }; + toplevel = new () { Id = "test-id" }; app.Begin (toplevel); Assert.Single (app.SessionStack); diff --git a/Tests/UnitTests/Application/ApplicationPopoverTests.cs b/Tests/UnitTests/Application/ApplicationPopoverTests.cs index 778e681a9..4d3db123a 100644 --- a/Tests/UnitTests/Application/ApplicationPopoverTests.cs +++ b/Tests/UnitTests/Application/ApplicationPopoverTests.cs @@ -219,8 +219,8 @@ public class ApplicationPopoverTests { // Arrange Application.Init ("fake"); - Application.TopRunnable = new() { Id = "initialTop" }; - PopoverTestClass? popover = new () { }; + Application.TopRunnable = new () { Id = "initialTop" }; + PopoverTestClass? popover = new (); var keyDownEvents = 0; popover.KeyDown += (s, e) => @@ -234,7 +234,7 @@ public class ApplicationPopoverTests // Act Application.RaiseKeyDownEvent (Key.A); // Goes to initialTop - Application.TopRunnable = new() { Id = "secondaryTop" }; + Application.TopRunnable = new () { Id = "secondaryTop" }; Application.RaiseKeyDownEvent (Key.A); // Goes to secondaryTop // Test diff --git a/Tests/UnitTests/TestsAllViews.cs b/Tests/UnitTests/TestsAllViews.cs index 7428e26c0..583f94457 100644 --- a/Tests/UnitTests/TestsAllViews.cs +++ b/Tests/UnitTests/TestsAllViews.cs @@ -64,7 +64,15 @@ public class TestsAllViews : FakeDriverBase // use or the original type if applicable foreach (Type arg in type.GetGenericArguments ()) { - if (arg.IsValueType && Nullable.GetUnderlyingType (arg) == null) + // Check if this type parameter has constraints that object can't satisfy + Type [] constraints = arg.GetGenericParameterConstraints (); + + // If there's a View constraint, use View instead of object + if (constraints.Any (c => c == typeof (View) || c.IsSubclassOf (typeof (View)))) + { + typeArguments.Add (typeof (View)); + } + else if (arg.IsValueType && Nullable.GetUnderlyingType (arg) == null) { typeArguments.Add (arg); } @@ -85,6 +93,14 @@ public class TestsAllViews : FakeDriverBase return null; } + // Check if the type has required properties that can't be satisfied by Activator.CreateInstance + // This handles cases like RunnableWrapper which has a required WrappedView property + if (HasRequiredProperties (type)) + { + Logging.Warning ($"Cannot create an instance of {type} because it has required properties that must be set."); + return null; + } + Assert.IsType (type, (View)Activator.CreateInstance (type)!); } else @@ -139,6 +155,16 @@ public class TestsAllViews : FakeDriverBase return viewType; } + /// + /// Checks if a type has required properties (C# 11 feature). + /// + private static bool HasRequiredProperties (Type type) + { + // Check all public instance properties for the RequiredMemberAttribute + return type.GetProperties (BindingFlags.Public | BindingFlags.Instance) + .Any (p => p.GetCustomAttributes (typeof (System.Runtime.CompilerServices.RequiredMemberAttribute), true).Any ()); + } + private static void AddArguments (Type paramType, List pTypes) { if (paramType == typeof (Rectangle)) diff --git a/Tests/UnitTestsParallelizable/Application/Runnable/RunnableEdgeCasesTests.cs b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableEdgeCasesTests.cs new file mode 100644 index 000000000..67e568984 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableEdgeCasesTests.cs @@ -0,0 +1,327 @@ +using Xunit.Abstractions; + +namespace UnitTests_Parallelizable.ApplicationTests.RunnableTests; + +/// +/// Tests for edge cases and error conditions in IRunnable implementation. +/// +public class RunnableEdgeCasesTests (ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + + [Fact] + public void RunnableSessionToken_CannotDisposeWithRunnableSet () + { + // Arrange + Runnable runnable = new (); + RunnableSessionToken token = new (runnable); + + // Act & Assert + var ex = Assert.Throws (() => token.Dispose ()); + Assert.Contains ("Runnable must be null", ex.Message); + } + + [Fact] + public void RunnableSessionToken_CanDisposeAfterClearingRunnable () + { + // Arrange + Runnable runnable = new (); + RunnableSessionToken token = new (runnable); + token.Runnable = null; + + // Act & Assert - Should not throw + token.Dispose (); + } + + [Fact] + public void Runnable_MultipleEventSubscribers_AllInvoked () + { + // Arrange + Runnable runnable = new (); + var subscriber1Called = false; + var subscriber2Called = false; + var subscriber3Called = false; + + runnable.IsRunningChanging += (s, e) => subscriber1Called = true; + runnable.IsRunningChanging += (s, e) => subscriber2Called = true; + runnable.IsRunningChanging += (s, e) => subscriber3Called = true; + + // Act + runnable.RaiseIsRunningChanging (false, true); + + // Assert + Assert.True (subscriber1Called); + Assert.True (subscriber2Called); + Assert.True (subscriber3Called); + } + + [Fact] + public void Runnable_EventSubscriber_CanCancelAfterOthers () + { + // Arrange + Runnable runnable = new (); + var subscriber1Called = false; + var subscriber2Called = false; + + runnable.IsRunningChanging += (s, e) => subscriber1Called = true; + + runnable.IsRunningChanging += (s, e) => + { + subscriber2Called = true; + e.Cancel = true; // Second subscriber cancels + }; + + // Act + bool canceled = runnable.RaiseIsRunningChanging (false, true); + + // Assert + Assert.True (subscriber1Called); + Assert.True (subscriber2Called); + Assert.True (canceled); + } + + [Fact] + public void Runnable_Result_CanBeSetMultipleTimes () + { + // Arrange + Runnable runnable = new (); + + // Act + runnable.Result = 1; + runnable.Result = 2; + runnable.Result = 3; + + // Assert + Assert.Equal (3, runnable.Result); + } + + [Fact] + public void Runnable_Result_ClearedOnMultipleStarts () + { + // Arrange + Runnable runnable = new () { Result = 42 }; + + // Act & Assert - First start + runnable.RaiseIsRunningChanging (false, true); + Assert.Equal (0, runnable.Result); + + // Set result again + runnable.Result = 99; + Assert.Equal (99, runnable.Result); + + // Second start should clear again + runnable.RaiseIsRunningChanging (false, true); + Assert.Equal (0, runnable.Result); + } + + [Fact] + public void Runnable_NullableResult_DefaultsToNull () + { + // Arrange & Act + Runnable runnable = new (); + + // Assert + Assert.Null (runnable.Result); + } + + [Fact] + public void Runnable_NullableResult_CanBeExplicitlyNull () + { + // Arrange + Runnable runnable = new () { Result = "test" }; + + // Act + runnable.Result = null; + + // Assert + Assert.Null (runnable.Result); + } + + [Fact] + public void Runnable_ComplexType_Result () + { + // Arrange + Runnable runnable = new (); + ComplexResult result = new () { Value = 42, Text = "test" }; + + // Act + runnable.Result = result; + + // Assert + Assert.NotNull (runnable.Result); + Assert.Equal (42, runnable.Result.Value); + Assert.Equal ("test", runnable.Result.Text); + } + + [Fact] + public void Runnable_IsRunning_WithNoApp () + { + // Arrange + Runnable runnable = new (); + + // Don't set App property + + // Act & Assert + Assert.False (runnable.IsRunning); + } + + [Fact] + public void Runnable_IsModal_WithNoApp () + { + // Arrange + Runnable runnable = new (); + + // Don't set App property + + // Act & Assert + Assert.False (runnable.IsModal); + } + + [Fact] + public void Runnable_VirtualMethods_CanBeOverridden () + { + // Arrange + OverriddenRunnable runnable = new (); + + // Act + bool canceledRunning = runnable.RaiseIsRunningChanging (false, true); + runnable.RaiseIsRunningChangedEvent (true); + bool canceledModal = runnable.RaiseIsModalChanging (false, true); + runnable.RaiseIsModalChangedEvent (true); + + // Assert + Assert.True (runnable.OnIsRunningChangingCalled); + Assert.True (runnable.OnIsRunningChangedCalled); + Assert.True (runnable.OnIsModalChangingCalled); + Assert.True (runnable.OnIsModalChangedCalled); + } + + [Fact] + public void Runnable_RequestStop_WithNoApp () + { + // Arrange + Runnable runnable = new (); + + // Don't set App property + + // Act & Assert - Should not throw + runnable.RequestStop (); + } + + [Fact] + public void RunnableSessionToken_Constructor_RequiresRunnable () + { + // This is implicitly tested by the constructor signature, + // but let's verify it creates with non-null runnable + + // Arrange + Runnable runnable = new (); + + // Act + RunnableSessionToken token = new (runnable); + + // Assert + Assert.NotNull (token.Runnable); + } + + [Fact] + public void Runnable_EventArgs_PreservesValues () + { + // Arrange + Runnable runnable = new (); + bool? capturedOldValue = null; + bool? capturedNewValue = null; + + runnable.IsRunningChanging += (s, e) => + { + capturedOldValue = e.CurrentValue; + capturedNewValue = e.NewValue; + }; + + // Act + runnable.RaiseIsRunningChanging (false, true); + + // Assert + Assert.NotNull (capturedOldValue); + Assert.NotNull (capturedNewValue); + Assert.False (capturedOldValue.Value); + Assert.True (capturedNewValue.Value); + } + + [Fact] + public void Runnable_IsModalChanged_EventArgs_PreservesValue () + { + // Arrange + Runnable runnable = new (); + bool? capturedValue = null; + + runnable.IsModalChanged += (s, e) => { capturedValue = e.Value; }; + + // Act + runnable.RaiseIsModalChangedEvent (true); + + // Assert + Assert.NotNull (capturedValue); + Assert.True (capturedValue.Value); + } + + [Fact] + public void Runnable_DifferentGenericTypes_Independent () + { + // Arrange & Act + Runnable intRunnable = new () { Result = 42 }; + Runnable stringRunnable = new () { Result = "test" }; + Runnable boolRunnable = new () { Result = true }; + + // Assert + Assert.Equal (42, intRunnable.Result); + Assert.Equal ("test", stringRunnable.Result); + Assert.True (boolRunnable.Result); + } + + /// + /// Complex result type for testing. + /// + private class ComplexResult + { + public int Value { get; set; } + public string? Text { get; set; } + } + + /// + /// Runnable that tracks virtual method calls. + /// + private class OverriddenRunnable : Runnable + { + public bool OnIsRunningChangingCalled { get; private set; } + public bool OnIsRunningChangedCalled { get; private set; } + public bool OnIsModalChangingCalled { get; private set; } + public bool OnIsModalChangedCalled { get; private set; } + + protected override bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning) + { + OnIsRunningChangingCalled = true; + + return base.OnIsRunningChanging (oldIsRunning, newIsRunning); + } + + protected override void OnIsRunningChanged (bool newIsRunning) + { + OnIsRunningChangedCalled = true; + base.OnIsRunningChanged (newIsRunning); + } + + protected override bool OnIsModalChanging (bool oldIsModal, bool newIsModal) + { + OnIsModalChangingCalled = true; + + return base.OnIsModalChanging (oldIsModal, newIsModal); + } + + protected override void OnIsModalChanged (bool newIsModal) + { + OnIsModalChangedCalled = true; + base.OnIsModalChanged (newIsModal); + } + } +} diff --git a/Tests/UnitTestsParallelizable/Application/Runnable/RunnableIntegrationTests.cs b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableIntegrationTests.cs new file mode 100644 index 000000000..d9ef7b6a5 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableIntegrationTests.cs @@ -0,0 +1,543 @@ +using Xunit.Abstractions; + +namespace UnitTests_Parallelizable.ApplicationTests.RunnableTests; + +/// +/// Integration tests for IApplication's IRunnable support. +/// Tests the full lifecycle of IRunnable instances through Application methods. +/// +public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : IDisposable +{ + private readonly ITestOutputHelper _output = output; + private IApplication? _app; + + private IApplication GetApp () + { + if (_app is null) + { + _app = Application.Create (); + _app.Init ("fake"); + } + + return _app; + } + + public void Dispose () + { + _app?.Shutdown (); + _app = null; + } + + [Fact] + public void Begin_AddsRunnableToStack () + { + // Arrange + IApplication app = GetApp (); + Runnable runnable = new (); + int stackCountBefore = app.RunnableSessionStack?.Count ?? 0; + + // Act + RunnableSessionToken token = app.Begin (runnable); + + // Assert + Assert.NotNull (token); + Assert.NotNull (token.Runnable); + Assert.Same (runnable, token.Runnable); + Assert.Equal (stackCountBefore + 1, app.RunnableSessionStack?.Count ?? 0); + + // Cleanup + app.End (token); + } + + [Fact] + public void Begin_ThrowsOnNullRunnable () + { + // Arrange + IApplication app = GetApp (); + + // Act & Assert + Assert.Throws (() => app.Begin ((IRunnable)null!)); + } + + [Fact] + public void Begin_RaisesIsRunningChangingEvent () + { + // Arrange + IApplication app = GetApp (); + Runnable runnable = new (); + var isRunningChangingRaised = false; + bool? oldValue = null; + bool? newValue = null; + + runnable.IsRunningChanging += (s, e) => + { + isRunningChangingRaised = true; + oldValue = e.CurrentValue; + newValue = e.NewValue; + }; + + // Act + RunnableSessionToken token = app.Begin (runnable); + + // Assert + Assert.True (isRunningChangingRaised); + Assert.False (oldValue); + Assert.True (newValue); + + // Cleanup + app.End (token); + } + + [Fact] + public void Begin_RaisesIsRunningChangedEvent () + { + // Arrange + IApplication app = GetApp (); + Runnable runnable = new (); + var isRunningChangedRaised = false; + bool? receivedValue = null; + + runnable.IsRunningChanged += (s, e) => + { + isRunningChangedRaised = true; + receivedValue = e.Value; + }; + + // Act + RunnableSessionToken token = app.Begin (runnable); + + // Assert + Assert.True (isRunningChangedRaised); + Assert.True (receivedValue); + + // Cleanup + app.End (token); + } + + [Fact] + public void Begin_RaisesIsModalChangingEvent () + { + // Arrange + IApplication app = GetApp (); + Runnable runnable = new (); + var isModalChangingRaised = false; + bool? oldValue = null; + bool? newValue = null; + + runnable.IsModalChanging += (s, e) => + { + isModalChangingRaised = true; + oldValue = e.CurrentValue; + newValue = e.NewValue; + }; + + // Act + RunnableSessionToken token = app.Begin (runnable); + + // Assert + Assert.True (isModalChangingRaised); + Assert.False (oldValue); + Assert.True (newValue); + + // Cleanup + app.End (token); + } + + [Fact] + public void Begin_RaisesIsModalChangedEvent () + { + // Arrange + IApplication app = GetApp (); + Runnable runnable = new (); + var isModalChangedRaised = false; + bool? receivedValue = null; + + runnable.IsModalChanged += (s, e) => + { + isModalChangedRaised = true; + receivedValue = e.Value; + }; + + // Act + RunnableSessionToken token = app.Begin (runnable); + + // Assert + Assert.True (isModalChangedRaised); + Assert.True (receivedValue); + + // Cleanup + app.End (token); + } + + [Fact] + public void Begin_SetsIsRunningToTrue () + { + // Arrange + IApplication app = GetApp (); + Runnable runnable = new (); + + // Act + RunnableSessionToken token = app.Begin (runnable); + + // Assert + Assert.True (runnable.IsRunning); + + // Cleanup + app.End (token); + } + + [Fact] + public void Begin_SetsIsModalToTrue () + { + // Arrange + IApplication app = GetApp (); + Runnable runnable = new (); + + // Act + RunnableSessionToken token = app.Begin (runnable); + + // Assert + Assert.True (runnable.IsModal); + + // Cleanup + app.End (token); + } + + [Fact] + public void End_RemovesRunnableFromStack () + { + // Arrange + IApplication app = GetApp (); + Runnable runnable = new (); + RunnableSessionToken token = app.Begin (runnable); + int stackCountBefore = app.RunnableSessionStack?.Count ?? 0; + + // Act + app.End (token); + + // Assert + Assert.Equal (stackCountBefore - 1, app.RunnableSessionStack?.Count ?? 0); + } + + [Fact] + public void End_ThrowsOnNullToken () + { + // Arrange + IApplication app = GetApp (); + + // Act & Assert + Assert.Throws (() => app.End ((RunnableSessionToken)null!)); + } + + [Fact] + public void End_RaisesIsRunningChangingEvent () + { + // Arrange + IApplication app = GetApp (); + Runnable runnable = new (); + RunnableSessionToken token = app.Begin (runnable); + var isRunningChangingRaised = false; + bool? oldValue = null; + bool? newValue = null; + + runnable.IsRunningChanging += (s, e) => + { + isRunningChangingRaised = true; + oldValue = e.CurrentValue; + newValue = e.NewValue; + }; + + // Act + app.End (token); + + // Assert + Assert.True (isRunningChangingRaised); + Assert.True (oldValue); + Assert.False (newValue); + } + + [Fact] + public void End_RaisesIsRunningChangedEvent () + { + // Arrange + IApplication app = GetApp (); + Runnable runnable = new (); + RunnableSessionToken token = app.Begin (runnable); + var isRunningChangedRaised = false; + bool? receivedValue = null; + + runnable.IsRunningChanged += (s, e) => + { + isRunningChangedRaised = true; + receivedValue = e.Value; + }; + + // Act + app.End (token); + + // Assert + Assert.True (isRunningChangedRaised); + Assert.False (receivedValue); + } + + [Fact] + public void End_SetsIsRunningToFalse () + { + // Arrange + IApplication app = GetApp (); + Runnable runnable = new (); + RunnableSessionToken token = app.Begin (runnable); + + // Act + app.End (token); + + // Assert + Assert.False (runnable.IsRunning); + } + + [Fact] + public void End_SetsIsModalToFalse () + { + // Arrange + IApplication app = GetApp (); + Runnable runnable = new (); + RunnableSessionToken token = app.Begin (runnable); + + // Act + app.End (token); + + // Assert + Assert.False (runnable.IsModal); + } + + [Fact] + public void End_ClearsTokenRunnable () + { + // Arrange + IApplication app = GetApp (); + Runnable runnable = new (); + RunnableSessionToken token = app.Begin (runnable); + + // Act + app.End (token); + + // Assert + Assert.Null (token.Runnable); + } + + [Fact] + public void NestedBegin_MaintainsStackOrder () + { + // Arrange + IApplication app = GetApp (); + Runnable runnable1 = new () { Id = "1" }; + Runnable runnable2 = new () { Id = "2" }; + + // Act + RunnableSessionToken token1 = app.Begin (runnable1); + RunnableSessionToken token2 = app.Begin (runnable2); + + // Assert - runnable2 should be on top + Assert.True (runnable2.IsModal); + Assert.False (runnable1.IsModal); + Assert.True (runnable1.IsRunning); // Still running, just not modal + Assert.True (runnable2.IsRunning); + + // Cleanup + app.End (token2); + app.End (token1); + } + + [Fact] + public void NestedEnd_RestoresPreviousModal () + { + // Arrange + IApplication app = GetApp (); + Runnable runnable1 = new () { Id = "1" }; + Runnable runnable2 = new () { Id = "2" }; + RunnableSessionToken token1 = app.Begin (runnable1); + RunnableSessionToken token2 = app.Begin (runnable2); + + // Act - End the top runnable + app.End (token2); + + // Assert - runnable1 should become modal again + Assert.True (runnable1.IsModal); + Assert.False (runnable2.IsModal); + Assert.True (runnable1.IsRunning); + Assert.False (runnable2.IsRunning); + + // Cleanup + app.End (token1); + } + + [Fact] + public void RequestStop_WithIRunnable_WorksCorrectly () + { + // Arrange + IApplication app = GetApp (); + StoppableRunnable runnable = new (); + RunnableSessionToken token = app.Begin (runnable); + + // Act + app.RequestStop (runnable); + + // Assert - RequestStop should trigger End eventually + // For now, just verify it doesn't throw + Assert.NotNull (runnable); + + // Cleanup + app.End (token); + } + + [Fact] + public void RequestStop_WithNull_UsesTopRunnable () + { + // Arrange + IApplication app = GetApp (); + StoppableRunnable runnable = new (); + RunnableSessionToken token = app.Begin (runnable); + + // Act + app.RequestStop ((IRunnable?)null); + + // Assert - Should not throw + Assert.NotNull (runnable); + + // Cleanup + app.End (token); + } + + [Fact (Skip = "Run methods with main loop are not suitable for parallel tests - use non-parallel UnitTests instead")] + public void RunGeneric_CreatesAndReturnsRunnable () + { + // Arrange + IApplication app = GetApp (); + app.StopAfterFirstIteration = true; + + // Act - With fluent API, Run() returns IApplication for chaining + IApplication result = app.Run (); + + // Assert + Assert.NotNull (result); + Assert.Same (app, result); // Fluent API returns this + + // Note: Run blocks until stopped, but StopAfterFirstIteration makes it return immediately + // The runnable is automatically disposed by Shutdown() + } + + [Fact (Skip = "Run methods with main loop are not suitable for parallel tests - use non-parallel UnitTests instead")] + public void RunGeneric_ThrowsIfNotInitialized () + { + // Arrange + IApplication app = Application.Create (); + + // Don't call Init + + // Act & Assert + Assert.Throws (() => app.Run ()); + + // Cleanup + app.Shutdown (); + } + + [Fact] + public void Begin_CanBeCanceled_ByIsRunningChanging () + { + // Arrange + IApplication app = GetApp (); + CancelableRunnable runnable = new () { CancelStart = true }; + + // Act + RunnableSessionToken token = app.Begin (runnable); + + // Assert - Should not be added to stack if canceled + Assert.False (runnable.IsRunning); + + // Token is still created but runnable not added to stack + Assert.NotNull (token); + } + + [Fact] + public void End_CanBeCanceled_ByIsRunningChanging () + { + // Arrange + IApplication app = GetApp (); + CancelableRunnable runnable = new () { CancelStop = true }; + RunnableSessionToken token = app.Begin (runnable); + runnable.CancelStop = true; // Enable cancellation + + // Act + app.End (token); + + // Assert - Should still be running if canceled + Assert.True (runnable.IsRunning); + + // Force end by disabling cancellation + runnable.CancelStop = false; + app.End (token); + } + + [Fact] + public void MultipleRunnables_IndependentResults () + { + // Arrange + IApplication app = GetApp (); + Runnable runnable1 = new (); + Runnable runnable2 = new (); + + // Act + runnable1.Result = 42; + runnable2.Result = "test"; + + // Assert + Assert.Equal (42, runnable1.Result); + Assert.Equal ("test", runnable2.Result); + } + + /// + /// Test runnable that can be stopped. + /// + private class StoppableRunnable : Runnable + { + public bool WasStopRequested { get; private set; } + + public override void RequestStop () + { + WasStopRequested = true; + base.RequestStop (); + } + } + + /// + /// Test runnable for generic Run tests. + /// + private class TestRunnable : Runnable + { + public TestRunnable () { Id = "TestRunnable"; } + } + + /// + /// Test runnable that can cancel lifecycle changes. + /// + private class CancelableRunnable : Runnable + { + public bool CancelStart { get; set; } + public bool CancelStop { get; set; } + + protected override bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning) + { + if (newIsRunning && CancelStart) + { + return true; // Cancel starting + } + + if (!newIsRunning && CancelStop) + { + return true; // Cancel stopping + } + + return base.OnIsRunningChanging (oldIsRunning, newIsRunning); + } + } +} diff --git a/Tests/UnitTestsParallelizable/Application/Runnable/RunnableLifecycleTests.cs b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableLifecycleTests.cs new file mode 100644 index 000000000..f84bb85d1 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableLifecycleTests.cs @@ -0,0 +1,156 @@ +using Xunit.Abstractions; + +namespace UnitTests_Parallelizable.ApplicationTests.RunnableTests; + +/// +/// Tests for IRunnable lifecycle behavior. +/// +public class RunnableLifecycleTests (ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + + [Fact] + public void Runnable_OnIsRunningChanging_CanExtractResult () + { + // Arrange + ResultExtractingRunnable runnable = new (); + runnable.TestValue = "extracted"; + + // Act + bool canceled = runnable.RaiseIsRunningChanging (true, false); // Stopping + + // Assert + Assert.False (canceled); + Assert.Equal ("extracted", runnable.Result); + } + + [Fact] + public void Runnable_OnIsRunningChanging_ClearsResultWhenStarting () + { + // Arrange + ResultExtractingRunnable runnable = new () { Result = "previous" }; + + // Act + bool canceled = runnable.RaiseIsRunningChanging (false, true); // Starting + + // Assert + Assert.False (canceled); + Assert.Null (runnable.Result); // Result should be cleared + } + + [Fact] + public void Runnable_CanCancelStoppingWithUnsavedChanges () + { + // Arrange + UnsavedChangesRunnable runnable = new () { HasUnsavedChanges = true }; + + // Act + bool canceled = runnable.RaiseIsRunningChanging (true, false); // Stopping + + // Assert + Assert.True (canceled); // Should be canceled + } + + [Fact] + public void Runnable_AllowsStoppingWithoutUnsavedChanges () + { + // Arrange + UnsavedChangesRunnable runnable = new () { HasUnsavedChanges = false }; + + // Act + bool canceled = runnable.RaiseIsRunningChanging (true, false); // Stopping + + // Assert + Assert.False (canceled); // Should not be canceled + } + + [Fact] + public void Runnable_OnIsRunningChanged_CalledAfterStateChange () + { + // Arrange + TrackedRunnable runnable = new (); + + // Act + runnable.RaiseIsRunningChangedEvent (true); + + // Assert + Assert.True (runnable.OnIsRunningChangedCalled); + Assert.True (runnable.LastIsRunningValue); + } + + [Fact] + public void Runnable_OnIsModalChanged_CalledAfterStateChange () + { + // Arrange + TrackedRunnable runnable = new (); + + // Act + runnable.RaiseIsModalChangedEvent (true); + + // Assert + Assert.True (runnable.OnIsModalChangedCalled); + Assert.True (runnable.LastIsModalValue); + } + + /// + /// Test runnable that extracts result in OnIsRunningChanging. + /// + private class ResultExtractingRunnable : Runnable + { + public string? TestValue { get; set; } + + protected override bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning) + { + if (!newIsRunning) // Stopping + { + // Extract result before removal from stack + Result = TestValue; + } + + return base.OnIsRunningChanging (oldIsRunning, newIsRunning); + } + } + + /// + /// Test runnable that can prevent stopping with unsaved changes. + /// + private class UnsavedChangesRunnable : Runnable + { + public bool HasUnsavedChanges { get; set; } + + protected override bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning) + { + if (!newIsRunning && HasUnsavedChanges) // Stopping with unsaved changes + { + return true; // Cancel stopping + } + + return base.OnIsRunningChanging (oldIsRunning, newIsRunning); + } + } + + /// + /// Test runnable that tracks lifecycle method calls. + /// + private class TrackedRunnable : Runnable + { + public bool OnIsRunningChangedCalled { get; private set; } + public bool LastIsRunningValue { get; private set; } + public bool OnIsModalChangedCalled { get; private set; } + public bool LastIsModalValue { get; private set; } + + protected override void OnIsRunningChanged (bool newIsRunning) + { + OnIsRunningChangedCalled = true; + LastIsRunningValue = newIsRunning; + base.OnIsRunningChanged (newIsRunning); + } + + protected override void OnIsModalChanged (bool newIsModal) + { + OnIsModalChangedCalled = true; + LastIsModalValue = newIsModal; + base.OnIsModalChanged (newIsModal); + } + } +} diff --git a/Tests/UnitTestsParallelizable/Application/Runnable/RunnableSessionTokenTests.cs b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableSessionTokenTests.cs new file mode 100644 index 000000000..82439cd5d --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableSessionTokenTests.cs @@ -0,0 +1,62 @@ +using Xunit.Abstractions; + +namespace UnitTests_Parallelizable.ApplicationTests.RunnableTests; + +/// +/// Tests for RunnableSessionToken class. +/// +public class RunnableSessionTokenTests (ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + + [Fact] + public void RunnableSessionToken_Constructor_SetsRunnable () + { + // Arrange + Runnable runnable = new (); + + // Act + RunnableSessionToken token = new (runnable); + + // Assert + Assert.NotNull (token.Runnable); + Assert.Same (runnable, token.Runnable); + } + + [Fact] + public void RunnableSessionToken_Runnable_CanBeSetToNull () + { + // Arrange + Runnable runnable = new (); + RunnableSessionToken token = new (runnable); + + // Act + token.Runnable = null; + + // Assert + Assert.Null (token.Runnable); + } + + [Fact] + public void RunnableSessionToken_Dispose_ThrowsIfRunnableNotNull () + { + // Arrange + Runnable runnable = new (); + RunnableSessionToken token = new (runnable); + + // Act & Assert + Assert.Throws (() => token.Dispose ()); + } + + [Fact] + public void RunnableSessionToken_Dispose_SucceedsIfRunnableIsNull () + { + // Arrange + Runnable runnable = new (); + RunnableSessionToken token = new (runnable); + token.Runnable = null; + + // Act & Assert - should not throw + token.Dispose (); + } +} diff --git a/Tests/UnitTestsParallelizable/Application/Runnable/RunnableTests.cs b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableTests.cs new file mode 100644 index 000000000..1d9b71ca8 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableTests.cs @@ -0,0 +1,222 @@ +using Xunit.Abstractions; + +namespace UnitTests_Parallelizable.ApplicationTests.RunnableTests; + +/// +/// Tests for IRunnable interface and Runnable base class. +/// +public class RunnableTests (ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + + [Fact] + public void Runnable_Implements_IRunnable () + { + // Arrange & Act + Runnable runnable = new (); + + // Assert + Assert.IsAssignableFrom (runnable); + Assert.IsAssignableFrom> (runnable); + } + + [Fact] + public void Runnable_Result_DefaultsToDefault () + { + // Arrange & Act + Runnable runnable = new (); + + // Assert + Assert.Equal (0, runnable.Result); + } + + [Fact] + public void Runnable_Result_CanBeSet () + { + // Arrange + Runnable runnable = new (); + + // Act + runnable.Result = 42; + + // Assert + Assert.Equal (42, runnable.Result); + } + + [Fact] + public void Runnable_Result_CanBeSetToNull () + { + // Arrange + Runnable runnable = new (); + + // Act + runnable.Result = null; + + // Assert + Assert.Null (runnable.Result); + } + + [Fact] + public void Runnable_IsRunning_ReturnsFalse_WhenNotRunning () + { + // Arrange + IApplication app = Application.Create (); + app.Init (); + Runnable runnable = new (); + + // Act & Assert + Assert.False (runnable.IsRunning); + + // Cleanup + app.Shutdown (); + } + + [Fact] + public void Runnable_IsModal_ReturnsFalse_WhenNotRunning () + { + // Arrange + Runnable runnable = new (); + + // Act & Assert + // IsModal should be false when the runnable has no app or is not TopRunnable + Assert.False (runnable.IsModal); + } + + [Fact] + public void RaiseIsRunningChanging_ClearsResult_WhenStarting () + { + // Arrange + Runnable runnable = new () { Result = 42 }; + + // Act + bool canceled = runnable.RaiseIsRunningChanging (false, true); + + // Assert + Assert.False (canceled); + Assert.Equal (0, runnable.Result); // Result should be cleared + } + + [Fact] + public void RaiseIsRunningChanging_CanBeCanceled_ByVirtualMethod () + { + // Arrange + CancelableRunnable runnable = new (); + + // Act + bool canceled = runnable.RaiseIsRunningChanging (false, true); + + // Assert + Assert.True (canceled); + } + + [Fact] + public void RaiseIsRunningChanging_CanBeCanceled_ByEvent () + { + // Arrange + Runnable runnable = new (); + var eventRaised = false; + + runnable.IsRunningChanging += (s, e) => + { + eventRaised = true; + e.Cancel = true; + }; + + // Act + bool canceled = runnable.RaiseIsRunningChanging (false, true); + + // Assert + Assert.True (eventRaised); + Assert.True (canceled); + } + + [Fact] + public void RaiseIsRunningChanged_RaisesEvent () + { + // Arrange + Runnable runnable = new (); + var eventRaised = false; + bool? receivedValue = null; + + runnable.IsRunningChanged += (s, e) => + { + eventRaised = true; + receivedValue = e.Value; + }; + + // Act + runnable.RaiseIsRunningChangedEvent (true); + + // Assert + Assert.True (eventRaised); + Assert.True (receivedValue); + } + + [Fact] + public void RaiseIsModalChanging_CanBeCanceled_ByVirtualMethod () + { + // Arrange + CancelableRunnable runnable = new () { CancelModalChange = true }; + + // Act + bool canceled = runnable.RaiseIsModalChanging (false, true); + + // Assert + Assert.True (canceled); + } + + [Fact] + public void RaiseIsModalChanging_CanBeCanceled_ByEvent () + { + // Arrange + Runnable runnable = new (); + var eventRaised = false; + + runnable.IsModalChanging += (s, e) => + { + eventRaised = true; + e.Cancel = true; + }; + + // Act + bool canceled = runnable.RaiseIsModalChanging (false, true); + + // Assert + Assert.True (eventRaised); + Assert.True (canceled); + } + + [Fact] + public void RaiseIsModalChanged_RaisesEvent () + { + // Arrange + Runnable runnable = new (); + var eventRaised = false; + bool? receivedValue = null; + + runnable.IsModalChanged += (s, e) => + { + eventRaised = true; + receivedValue = e.Value; + }; + + // Act + runnable.RaiseIsModalChangedEvent (true); + + // Assert + Assert.True (eventRaised); + Assert.True (receivedValue); + } + + /// + /// Test runnable that can cancel lifecycle changes. + /// + private class CancelableRunnable : Runnable + { + public bool CancelModalChange { get; set; } + + protected override bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning) => true; // Always cancel + + protected override bool OnIsModalChanging (bool oldIsModal, bool newIsModal) => CancelModalChange; + } +} diff --git a/docfx/docs/View.md b/docfx/docs/View.md index 33ac685fd..635ceb824 100644 --- a/docfx/docs/View.md +++ b/docfx/docs/View.md @@ -558,11 +558,134 @@ view.AddCommand(Command.ScrollDown, () => { view.ScrollVertical(1); return true; --- -## Modal Views +## Runnable Views (IRunnable) -Views can run modally (exclusively capturing all input until closed). See [Toplevel](~/api/Terminal.Gui.Views.Toplevel.yml) for details. +Views can implement [IRunnable](~/api/Terminal.Gui.App.IRunnable.yml) to run as independent, blocking sessions with typed results. This decouples runnability from inheritance, allowing any View to participate in session management. -### Running a View Modally +### IRunnable Architecture + +The **IRunnable** pattern provides: + +- **Interface-Based**: Implement `IRunnable` instead of inheriting from `Toplevel` +- **Type-Safe Results**: Generic `TResult` parameter for compile-time type safety +- **Fluent API**: Chain `Init()`, `Run()`, and `Shutdown()` for concise code +- **Automatic Disposal**: Framework manages lifecycle of created runnables +- **CWP Lifecycle Events**: `IsRunningChanging/Changed`, `IsModalChanging/Changed` + +### Creating a Runnable View + +Derive from [Runnable](~/api/Terminal.Gui.ViewBase.Runnable-1.yml) or implement [IRunnable](~/api/Terminal.Gui.App.IRunnable-1.yml): + +```csharp +public class ColorPickerDialog : Runnable +{ + private ColorPicker16 _colorPicker; + + public ColorPickerDialog() + { + Title = "Select a Color"; + + _colorPicker = new ColorPicker16 { X = Pos.Center(), Y = 2 }; + + var okButton = new Button { Text = "OK", IsDefault = true }; + okButton.Accepting += (s, e) => { + Result = _colorPicker.SelectedColor; + Application.RequestStop(); + }; + + Add(_colorPicker, okButton); + } +} +``` + +### Running with Fluent API + +The fluent API enables elegant, concise code with automatic disposal: + +```csharp +// Framework creates, runs, and disposes the runnable automatically +Color? result = Application.Create() + .Init() + .Run() + .Shutdown() as Color?; + +if (result is { }) +{ + Console.WriteLine($"Selected: {result}"); +} +``` + +### Running with Explicit Control + +For more control over the lifecycle: + +```csharp +var app = Application.Create(); +app.Init(); + +var dialog = new ColorPickerDialog(); +app.Run(dialog); + +// Extract result after Run returns +Color? result = dialog.Result; + +// Caller is responsible for disposal +dialog.Dispose(); + +app.Shutdown(); +``` + +### Disposal Semantics + +**"Whoever creates it, owns it":** + +- `Run()`: Framework creates → Framework disposes (in `Shutdown()`) +- `Run(IRunnable)`: Caller creates → Caller disposes + +### Result Extraction + +Extract the result in `OnIsRunningChanging` when stopping: + +```csharp +protected override bool OnIsRunningChanging(bool oldIsRunning, bool newIsRunning) +{ + if (!newIsRunning) // Stopping - extract result before disposal + { + Result = _colorPicker.SelectedColor; + + // Optionally cancel stop (e.g., prompt to save) + if (HasUnsavedChanges()) + { + return true; // Cancel stop + } + } + + return base.OnIsRunningChanging(oldIsRunning, newIsRunning); +} +``` + +### Lifecycle Properties + +- **`IsRunning`** - True when on the `RunnableSessionStack` +- **`IsModal`** - True when at the top of the stack (receiving all input) +- **`Result`** - The typed result value (set before stopping) + +### Lifecycle Events (CWP-Compliant) + +- **`IsRunningChanging`** - Cancellable event before added/removed from stack +- **`IsRunningChanged`** - Non-cancellable event after stack change +- **`IsModalChanging`** - Cancellable event before becoming/leaving top of stack +- **`IsModalChanged`** - Non-cancellable event after modal state change + +--- + +## Modal Views (Legacy) + +Views can run modally (exclusively capturing all input until closed). See [Toplevel](~/api/Terminal.Gui.Views.Toplevel.yml) for the legacy pattern. + +**Note:** New code should use `IRunnable` pattern (see above) for better type safety and lifecycle management. + +### Running a View Modally (Legacy) ```csharp var dialog = new Dialog @@ -580,16 +703,17 @@ dialog.Add(label); Application.Run(dialog); // Dialog has been closed +dialog.Dispose(); ``` -### Modal View Types +### Modal View Types (Legacy) - **[Toplevel](~/api/Terminal.Gui.Views.Toplevel.yml)** - Base class for modal views, can fill entire screen - **[Window](~/api/Terminal.Gui.Views.Window.yml)** - Overlapped container with border and title - **[Dialog](~/api/Terminal.Gui.Views.Dialog.yml)** - Modal Window, centered with button support - **[Wizard](~/api/Terminal.Gui.Views.Wizard.yml)** - Multi-step modal dialog -### Dialog Example +### Dialog Example (Legacy) [Dialogs](~/api/Terminal.Gui.Views.Dialog.yml) are Modal [Windows](~/api/Terminal.Gui.Views.Window.yml) centered on screen: diff --git a/docfx/docs/application.md b/docfx/docs/application.md index 530f53dc5..d624441b3 100644 --- a/docfx/docs/application.md +++ b/docfx/docs/application.md @@ -1,6 +1,15 @@ # Application Architecture -Terminal.Gui v2 uses an instance-based application architecture that decouples views from the global application state, improving testability and enabling multiple application contexts. +Terminal.Gui v2 uses an instance-based application architecture with the **IRunnable** interface pattern that decouples views from the global application state, improving testability, enabling multiple application contexts, and providing type-safe result handling. + +## Key Features + +- **Instance-Based**: Use `Application.Create()` to get an `IApplication` instance instead of static methods +- **IRunnable Interface**: Views implement `IRunnable` to participate in session management without inheriting from `Toplevel` +- **Fluent API**: Chain `Init()`, `Run()`, and `Shutdown()` for elegant, concise code +- **Automatic Disposal**: Framework-created runnables are automatically disposed +- **Type-Safe Results**: Generic `TResult` parameter provides compile-time type safety +- **CWP Compliance**: All lifecycle events follow the Cancellable Work Pattern ## View Hierarchy and Run Stack @@ -87,6 +96,12 @@ top.Add(myView); app.Run(top); top.Dispose(); app.Shutdown(); + +// NEWEST (v2 with IRunnable and Fluent API): +Color? result = Application.Create() + .Init() + .Run() + .Shutdown() as Color?; ``` **Note:** The static `Application` class delegates to `ApplicationImpl.Instance` (a singleton). `Application.Create()` creates a **new** `ApplicationImpl` instance, enabling multiple application contexts and better testability. @@ -158,32 +173,199 @@ public class MyView : View } ``` -## IApplication Interface +## IRunnable Architecture -The `IApplication` interface defines the application contract: +Terminal.Gui v2 introduces the **IRunnable** interface pattern that decouples runnable behavior from the `Toplevel` class hierarchy. Views can implement `IRunnable` to participate in session management without inheritance constraints. + +### Key Benefits + +- **Interface-Based**: No forced inheritance from `Toplevel` +- **Type-Safe Results**: Generic `TResult` parameter provides compile-time type safety +- **Fluent API**: Method chaining for elegant, concise code +- **Automatic Disposal**: Framework manages lifecycle of created runnables +- **CWP Compliance**: All lifecycle events follow the Cancellable Work Pattern + +### Fluent API Pattern + +The fluent API enables elegant method chaining with automatic resource management: + +```csharp +// All-in-one: Create, initialize, run, shutdown, and extract result +Color? result = Application.Create() + .Init() + .Run() + .Shutdown() as Color?; + +if (result is { }) +{ + ApplyColor(result); +} +``` + +**Key Methods:** + +- `Init()` - Returns `IApplication` for chaining +- `Run()` - Creates and runs runnable, returns `IApplication` +- `Shutdown()` - Disposes framework-owned runnables, returns `object?` result + +### Disposal Semantics + +**"Whoever creates it, owns it":** + +| Method | Creator | Owner | Disposal | +|--------|---------|-------|----------| +| `Run()` | Framework | Framework | Automatic in `Shutdown()` | +| `Run(IRunnable)` | Caller | Caller | Manual by caller | + +```csharp +// Framework ownership - automatic disposal +var result = app.Run().Shutdown(); + +// Caller ownership - manual disposal +var dialog = new MyDialog(); +app.Run(dialog); +var result = dialog.Result; +dialog.Dispose(); // Caller must dispose +``` + +### Creating Runnable Views + +Derive from `Runnable` or implement `IRunnable`: + +```csharp +public class FileDialog : Runnable +{ + private TextField _pathField; + + public FileDialog() + { + Title = "Select File"; + + _pathField = new TextField { X = 1, Y = 1, Width = Dim.Fill(1) }; + + var okButton = new Button { Text = "OK", IsDefault = true }; + okButton.Accepting += (s, e) => { + Result = _pathField.Text; + Application.RequestStop(); + }; + + Add(_pathField, okButton); + } + + protected override bool OnIsRunningChanging(bool oldValue, bool newValue) + { + if (!newValue) // Stopping - extract result before disposal + { + Result = _pathField?.Text; + } + return base.OnIsRunningChanging(oldValue, newValue); + } +} +``` + +### Lifecycle Properties + +- **`IsRunning`** - True when runnable is on `RunnableSessionStack` +- **`IsModal`** - True when runnable is at top of stack (capturing all input) +- **`Result`** - Typed result value set before stopping + +### Lifecycle Events (CWP-Compliant) + +All events follow Terminal.Gui's Cancellable Work Pattern: + +| Event | Cancellable | When | Use Case | +|-------|-------------|------|----------| +| `IsRunningChanging` | ✓ | Before add/remove from stack | Extract result, prevent close | +| `IsRunningChanged` | ✗ | After stack change | Post-start/stop cleanup | +| `IsModalChanging` | ✓ | Before becoming/leaving top | Prevent activation | +| `IsModalChanged` | ✗ | After modal state change | Update UI after focus change | + +**Example - Result Extraction:** + +```csharp +protected override bool OnIsRunningChanging(bool oldValue, bool newValue) +{ + if (!newValue) // Stopping + { + // Extract result before views are disposed + Result = _colorPicker.SelectedColor; + + // Optionally cancel stop (e.g., unsaved changes) + if (HasUnsavedChanges()) + { + int response = MessageBox.Query("Save?", "Save changes?", "Yes", "No", "Cancel"); + if (response == 2) return true; // Cancel stop + if (response == 0) Save(); + } + } + + return base.OnIsRunningChanging(oldValue, newValue); +} +``` + +### RunnableSessionStack + +The `RunnableSessionStack` manages all running `IRunnable` sessions: ```csharp public interface IApplication { /// - /// Gets the currently running Toplevel (the "current session"). - /// Renamed from "Top" for clarity. + /// Stack of running IRunnable sessions. + /// Each entry is a RunnableSessionToken wrapping an IRunnable. /// - Toplevel? Current { get; } + ConcurrentStack? RunnableSessionStack { get; } /// - /// Gets the stack of running sessions. - /// Renamed from "TopLevels" to align with SessionToken terminology. + /// The IRunnable at the top of RunnableSessionStack (currently modal). /// + IRunnable? TopRunnable { get; } +} +``` + +**Stack Behavior:** + +- Push: `Begin(IRunnable)` adds to top of stack +- Pop: `End(RunnableSessionToken)` removes from stack +- Peek: `TopRunnable` returns current modal runnable +- All: `RunnableSessionStack` enumerates all running sessions + +## IApplication Interface + +The `IApplication` interface defines the application contract with support for both legacy `Toplevel` and modern `IRunnable` patterns: + +```csharp +public interface IApplication +{ + // Legacy Toplevel support + Toplevel? Current { get; } ConcurrentStack SessionStack { get; } + // IRunnable support + IRunnable? TopRunnable { get; } + ConcurrentStack? RunnableSessionStack { get; } + IRunnable? FrameworkOwnedRunnable { get; set; } + + // Driver and lifecycle IDriver? Driver { get; } IMainLoopCoordinator? MainLoop { get; } - void Init(string? driverName = null); - void Shutdown(); + // Fluent API methods + IApplication Init(string? driverName = null); + object? Shutdown(); + + // Runnable methods + RunnableSessionToken Begin(IRunnable runnable); + void Run(IRunnable runnable, Func? errorHandler = null); + IApplication Run(Func? errorHandler = null) where TRunnable : IRunnable, new(); + void RequestStop(IRunnable? runnable); + void End(RunnableSessionToken sessionToken); + + // Legacy Toplevel methods SessionToken? Begin(Toplevel toplevel); + void Run(Toplevel view, Func? errorHandler = null); void End(SessionToken sessionToken); + // ... other members } ``` diff --git a/docfx/docs/runnable-architecture-proposal.md b/docfx/docs/runnable-architecture-proposal.md index 854095521..8903ede79 100644 --- a/docfx/docs/runnable-architecture-proposal.md +++ b/docfx/docs/runnable-architecture-proposal.md @@ -1,10 +1,12 @@ # IRunnable Architecture Proposal -**Status**: Proposal +**Status**: Phase 1 Complete ✅ - Phase 2 In Progress -**Version**: 1.7 - Approved - Implementing +**Version**: 1.8 - Phase 1 Implemented -**Date**: 2025-01-20 +**Date**: 2025-01-21 + +**Phase 1 Completion**: Issue #4400 closed with full implementation including fluent API and automatic disposal ## Summary @@ -1648,20 +1650,55 @@ fileDialog.Dispose (); - Rename `IApplication.Current` → `IApplication.TopRunnable` - Update `View.IsCurrentTop` → `View.IsTopRunnable` -### Phase 1: Add IRunnable Support +### Phase 1: Add IRunnable Support ✅ COMPLETE -- Issue #4400 +- Issue #4400 - **COMPLETED** -1. Add `IRunnable` (non-generic) interface alongside existing `Toplevel` -2. Add `IRunnable` (generic) interface -3. Add `Runnable` base class -4. Add `RunnableSessionToken` class -5. Update `IApplication.RunnableSessionStack` to hold `RunnableSessionToken` instead of `Toplevel` -6. Update `IApplication` to support both `Toplevel` and `IRunnable` -7. Implement CWP-based `IsRunningChanging`/`IsRunningChanged` events -8. Implement CWP-based `IsModalChanging`/`IsModalChanged` events -9. Update `Begin()`, `End()`, `RequestStop()` to raise these events -10. Add three `Run()` overloads: `Run(IRunnable)`, `Run()`, `Run()` +**Implemented:** + +1. ✅ Add `IRunnable` (non-generic) interface alongside existing `Toplevel` +2. ✅ Add `IRunnable` (generic) interface +3. ✅ Add `Runnable` base class +4. ✅ Add `RunnableSessionToken` class +5. ✅ Update `IApplication.RunnableSessionStack` to hold `RunnableSessionToken` +6. ✅ Update `IApplication` to support both `Toplevel` and `IRunnable` +7. ✅ Implement CWP-based `IsRunningChanging`/`IsRunningChanged` events +8. ✅ Implement CWP-based `IsModalChanging`/`IsModalChanged` events +9. ✅ Update `Begin()`, `End()`, `RequestStop()` to raise these events +10. ✅ Add `Run()` overloads: `Run(IRunnable)`, `Run()` + +**Bonus Features Added:** + +11. ✅ Fluent API - `Init()`, `Run()` return `IApplication` for method chaining +12. ✅ Automatic Disposal - `Shutdown()` returns result and disposes framework-owned runnables +13. ✅ Clear Ownership Semantics - "Whoever creates it, owns it" +14. ✅ 62 Parallelizable Unit Tests - Comprehensive test coverage +15. ✅ Example Application - `Examples/FluentExample` demonstrating the pattern +16. ✅ Complete API Documentation - XML docs for all new types + +**Key Design Decisions:** + +- Fluent API with `Init()` → `Run()` → `Shutdown()` chaining +- `Run()` returns `IApplication` (breaking change from returning `TRunnable`) +- `Shutdown()` returns `object?` (result from last run runnable) +- Framework automatically disposes runnables created by `Run()` +- Caller disposes runnables passed to `Run(IRunnable)` + +**Migration Example:** + +```csharp +// Before (manual disposal): +var dialog = new MyDialog(); +app.Run(dialog); +var result = dialog.Result; +dialog.Dispose(); + +// After (fluent with automatic disposal): +var result = Application.Create() + .Init() + .Run() + .Shutdown() as MyResultType; +``` ### Phase 2: Migrate Existing Views From b9f55a5a9635ef293d02a23d98da6de22e0b11eb Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Nov 2025 06:36:21 -0800 Subject: [PATCH 11/16] Fixes #4410, #4413, #4414, #4415 - `MessageBox` nullable, `Clipboard` refactor, fence for legacy/modern App, and makes internal classes thread safe. (#4411) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * Change MessageBox to return nullable int instead of -1 Co-authored-by: tig <585482+tig@users.noreply.github.com> * Initial plan * Add fencing to prevent mixing Application models Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fix fence logic to work with parallel tests Co-authored-by: tig <585482+tig@users.noreply.github.com> * WIP: Fixing Application issues. * Refactor error messages into constants Co-authored-by: tig <585482+tig@users.noreply.github.com> * Refactor ConfigurationProperty properties to use static backing fields and raise events Co-authored-by: tig <585482+tig@users.noreply.github.com> * Reset static Application properties in ResetStateStatic Co-authored-by: tig <585482+tig@users.noreply.github.com> * Refactor tests to decouple from global Application state Commented out `driver ??= Application.Driver` assignments in `DriverAssert` to prevent automatic global driver assignment. Removed `Application.ResetState(true)` calls and commented out state validation assertions in `GlobalTestSetup` to reduce dependency on global state. Reintroduced `ApplicationForceDriverTests` and `ApplicationModelFencingTests` to validate `ForceDriver` behavior and ensure proper handling of legacy and modern Application models. Skipped certain `ToAnsiTests` that rely on `Application`. Removed direct `Application.Driver` assignments in `ViewDrawingClippingTests` and `ViewDrawingFlowTests`. Performed general cleanup of redundant code and unused imports to simplify the codebase. * WIP: Fixed Parallel tests; non-Parallel still broken Refactor application model usage tracking Refactored `ApplicationModelUsage` into a public enum in the new `Terminal.Gui.App` namespace, making it accessible across the codebase. Replaced the private `_modelUsage` field in `ApplicationImpl` with a public static `ModelUsage` property to improve clarity and accessibility. Renamed error message constants for consistency and updated methods like `SetInstance` and `MarkInstanceBasedModelUsed` to use the new `ModelUsage` property. Removed the private `ApplicationModelUsage` enum from `ApplicationImpl`. Updated test cases to use `ApplicationImpl.Instance` instead of `Application.Create` to enforce the legacy static model. Skipped obsolete tests in `ApplicationForceDriverTests` and added null checks in `DriverAssert` and `SelectorBase` to handle edge cases. Commented out an unused line in `WindowsOutput` and made general improvements to code readability, maintainability, and consistency. * WIP: Almost there! Refactored tests and code to align with the modern instance-based application model. Key changes include: - Disabled Sixel rendering in `OutputBase.cs` due to dependency on legacy static `Application` object. - Hardcoded `force16Colors` to `false` in `WindowsOutput.cs` with a `BUGBUG` note. - Updated `ApplicationImplTests` to use `ApplicationImpl.SetInstance` and return `ApplicationImpl.Instance`. - Refactored `ApplicationModelFencingTests` to use `Application.Create()` and added `ResetModelUsageTracking()` for model switching. - Removed legacy `DriverTests` and reintroduced updated versions with cross-platform driver tests. - Reverted `ArrangementTests` and `ShortcutTests` to use legacy static `ApplicationImpl.Instance`. - Reintroduced driver tests in `DriverTests.cs` with modern `Application.Create()` and added `TestTop` for driver content verification. - General cleanup, including removal of outdated code and addition of `BUGBUG` notes for temporary workarounds. * Fixed all modelusage bugs? Replaced static `Application` references with instance-based `App` context across the codebase. Updated calls to `Application.RequestStop()` and `Application.Screen` to use `App?.RequestStop()` and `App?.Screen` for better encapsulation and flexibility. Refactored test infrastructure to align with the new context, including reintroducing `FakeApplicationFactory` and `FakeApplicationLifecycle` for testing purposes. Improved logging, error handling, and test clarity by adding `logWriter` support and simplifying test setup. Removed redundant or obsolete code, such as `NetSequences` and the old `FakeApplicationFactory` implementation. Updated documentation to reflect the new `IApplication.RequestStop()` usage. * merged * Refactor KeyboardImpl and modernize MessageBoxTests Refactored the `KeyboardImpl` class to remove hardcoded default key values, replacing them with uninitialized fields for dynamic configuration. Updated key binding logic to use `ReplaceCommands` instead of `Add` for better handling of dynamic changes. Removed unnecessary `KeyBindings.Clear()` calls to avoid side effects. Rewrote `MessageBoxTests.cs` to improve readability, maintainability, and adherence to modern C# standards. Enabled nullable reference checks, updated the namespace, and restructured test methods for clarity. Marked non-functional tests with `[Theory(Skip)]` and improved test organization with parameterized inputs. Enhanced test assertions, lifecycle handling, and error handling across the test suite. Updated `UICatalog_AboutBox` to use multiline string literals for expected outputs. These changes improve the overall maintainability and flexibility of the codebase. * Atempt to fix windows only CI/CD Unit tests failure Refactor Application lifecycle and test cleanup Refactored the `Application` class to phase out legacy static properties `SessionStack` and `TopRunnable` from `Application.Current.cs`. These were reintroduced in a new file `Application.TopRunnable.cs` for better modularity, while retaining their `[Obsolete]` status. Updated `ApplicationPopoverTests.cs` to replace `Application.ResetState(true)` with `Application.Shutdown()` for consistent application state cleanup. Added explicit cleanup for `Application.TopRunnable` in relevant test cases to ensure proper resource management. Adjusted namespaces and `using` directives to support the new structure. These changes improve code organization and align with updated application lifecycle management practices. * Fixes # - Dispose TopRunnable in cleanup logic Updated the `finally` block in `ApplicationPopoverTests` to dispose of the `Application.TopRunnable` object if it is not null, ensuring proper resource cleanup. Previously, the property was being set to `null` without disposal. The `Application.Shutdown()` call remains unchanged. * Improve thread safety, reduce static dependencies, and align the codebase with the updated `IApplication` interface. Refactored the `MainThreadId` property to improve encapsulation: - Updated `Application.MainThreadId` to use `ApplicationImpl.Instance` directly. - Added `MainThreadId` to `ApplicationImpl` and `IApplication`. - Removed redundant `MainThreadId` from `ApplicationImpl.Run.cs`. Updated `EnqueueMouseEvent` to include an `IApplication?` parameter: - Modified `FakeInputProcessor`, `InputProcessorImpl`, and `WindowsInputProcessor` to support the new parameter. - Updated `IInputProcessor` interface to reflect the new method signature. - Adjusted `GuiTestContext` and `EnqueueMouseEventTests` to pass `IApplication` where required. Improved test coverage and code maintainability: - Added test cases for negative positions and empty mouse events. - Commented out legacy code in `GraphView` and `FakeDriverBase`. - Enhanced readability in `EnqueueMouseEventTests`. These changes improve thread safety, reduce static dependencies, and align the codebase with the updated `IApplication` interface. * Fixed more bugs. Enabled nullable reference types across multiple files to improve code safety. Refactored and modularized test classes, improving readability and maintainability. Removed outdated test cases and added new tests for edge cases, including culture-specific and non-Gregorian calendar handling. Addressed timeout issues in `ScenarioTests` with a watchdog timer and improved error handling. Updated `ApplicationImplTests` to use instance fields instead of static references for better test isolation. Refactored `ScenarioTests` to dynamically load and test all UI Catalog scenarios, with macOS-specific skips for known issues. Aligned `MessageBox.Query` calls with updated API signatures. Performed general code cleanup, including removing unused directives, improving formatting, and consolidating repetitive logic into helper methods. * Made the `InputBindings` class thread-safe by replacing the internal `Dictionary` with `ConcurrentDictionary`. This fixes parallel test failures where "Collection was modified; enumeration operation may not execute" exceptions were thrown. ## Changes Made ### 1. InputBindings.cs - **File**: `Terminal.Gui/Input/InputBindings.cs` - **Change**: Replaced `Dictionary` with `ConcurrentDictionary` - **Key modifications**: - Changed `_bindings` from `Dictionary` to `ConcurrentDictionary` - Updated `Add()` methods to use `TryAdd()` instead of checking with `TryGet()` then `Add()` - Updated `Remove()` to use `TryRemove()` (no need to check existence first) - Updated `ReplaceCommands()` to use `ContainsKey()` instead of `TryGet()` - Added `.ToList()` to `GetAllFromCommands()` to create a snapshot for safe enumeration - Added comment explaining that `ConcurrentDictionary` provides snapshot enumeration in `GetBindings()` - Added `.ToArray()` to `Clear(Command[])` to create snapshot before iteration ### 2. Thread Safety Test Suite - **File**: `Tests/UnitTestsParallelizable/Input/InputBindingsThreadSafetyTests.cs` - **New file** with comprehensive thread safety tests: - `Add_ConcurrentAccess_NoExceptions` - Tests concurrent additions - `GetBindings_DuringConcurrentModification_NoExceptions` - Tests enumeration during modifications - `TryGet_ConcurrentAccess_ReturnsConsistentResults` - Tests concurrent reads - `Clear_ConcurrentAccess_NoExceptions` - Tests concurrent clearing - `Remove_ConcurrentAccess_NoExceptions` - Tests concurrent removals - `Replace_ConcurrentAccess_NoExceptions` - Tests concurrent replacements - `GetAllFromCommands_DuringModification_NoExceptions` - Tests LINQ queries during modifications - `MixedOperations_ConcurrentAccess_NoExceptions` - Tests mixed operations (add/read/remove) - `KeyBindings_ConcurrentAccess_NoExceptions` - Tests actual `KeyBindings` class - `MouseBindings_ConcurrentAccess_NoExceptions` - Tests actual `MouseBindings` class ## Benefits of ConcurrentDictionary Approach 1. **Lock-Free Reads**: Most read operations don't require locks, improving performance 2. **Snapshot Enumeration**: Built-in support for safe enumeration during concurrent modifications 3. **Simplified Code**: No need for explicit `lock` statements or lock objects 4. **Better Scalability**: Multiple threads can read/write simultaneously 5. **No "Collection was modified" Exceptions**: Enumeration creates a snapshot ## Performance Characteristics - **Read Operations**: Lock-free, very fast - **Write Operations**: Uses fine-grained locking internally, minimal contention - **Memory Overhead**: Slightly higher than `Dictionary` but negligible in practice - **Enumeration**: Creates a snapshot, safe for concurrent modifications ## Test Results - **Original failing test now passes**: `ApplicationImplTests.Init_CreatesKeybindings` - **10 new thread safety tests**: All passing - **All 11,741 parallelizable tests**: All passing (11,731 passed, 10 skipped) - **All 1,779 non-parallelizable tests**: All passing (1,762 passed, 17 skipped) - **No compilation errors**: Clean build with no xUnit1031 warnings (suppressed with pragmas) ## Verification The original failure was: ``` System.InvalidOperationException: Collection was modified; enumeration operation may not execute. ``` This occurred in parallelizable tests when multiple threads accessed `KeyBindings.GetBindings()` simultaneously. The `ConcurrentDictionary` implementation resolves this by providing thread-safe operations and snapshot enumeration. ## Notes - The xUnit1031 warnings about using `Task.WaitAll` instead of `async/await` have been suppressed with `#pragma warning disable xUnit1031` directives, as these are intentional blocking operations in stress tests that test concurrent scenarios - All existing functionality is preserved; this is a drop-in replacement - No changes to public API surface - Existing tests continue to pass * Make InputBindings and KeyboardImpl thread-safe for concurrent access Replace Dictionary with ConcurrentDictionary in InputBindings and KeyboardImpl to enable safe parallel test execution and multi-threaded usage. Changes: - InputBindings: Replace Dictionary with ConcurrentDictionary for _bindings - InputBindings: Make Replace() atomic using AddOrUpdate instead of Remove+Add - InputBindings: Make ReplaceCommands() atomic using AddOrUpdate - InputBindings: Add IsValid() check to both Add() overloads - InputBindings: Add defensive .ToList()/.ToArray() for safe LINQ enumeration - KeyboardImpl: Replace Dictionary with ConcurrentDictionary for _commandImplementations - KeyboardImpl: Change AddKeyBindings() to use ReplaceCommands for idempotent initialization - Add 10 comprehensive thread safety tests for InputBindings - Add 9 comprehensive thread safety tests for KeyboardImpl The ConcurrentDictionary implementation provides: - Lock-free reads for better performance under concurrent access - Atomic operations for Replace/ReplaceCommands preventing race conditions - Snapshot enumeration preventing "Collection was modified" exceptions - No breaking API changes - maintains backward compatibility All 11,750 parallelizable tests pass (11,740 passed, 10 skipped). Fixes race conditions that caused ApplicationImplTests.Init_CreatesKeybindings to fail intermittently during parallel test execution. * Decouple ApplicationImpl from Application static props Removed initialization of `Force16Colors` and `ForceDriver` from `Application` static properties in the `ApplicationImpl` constructor. The class still subscribes to the `Force16ColorsChanged` and `ForceDriverChanged` events, but no longer sets initial values for these properties. This change simplifies the constructor and reduces coupling between `ApplicationImpl` and `Application`. * Refactored keyboard initialization in `ApplicationImpl` to use `Application` static properties for default key assignments, ensuring synchronization with pre-`Init()` changes. Improved `KeyboardImpl` initialization to avoid premature `ApplicationImpl.Instance` access, enhancing testability. Standardized constant naming conventions and improved code readability in thread safety tests for `KeyboardImpl` and `InputBindings`. Updated `TestInputBindings` implementation for clarity and conciseness. Applied consistent code style improvements across files, including spacing, formatting, and variable naming, to enhance maintainability and readability. * Fix race conditions in parallel tests - thread-safe ApplicationImpl and KeyboardImpl Fixes intermittent failures in parallel tests caused by three separate race conditions: 1. **KeyboardImpl constructor race condition** - Constructor was accessing Application.QuitKey/ArrangeKey/etc which triggered ApplicationImpl.Instance getter, setting ModelUsage=LegacyStatic before Application.Create() was called - Changed constructor to initialize keys with hard-coded defaults instead - Added synchronization from Application static properties during Init() 2. **InputBindings.Replace() race condition** - Between GetOrAdd(oldEventArgs) and AddOrUpdate(newEventArgs), another thread could modify bindings, causing stale data to overwrite valid bindings - Added early return for same-key case (oldEventArgs == newEventArgs) - Kept atomic operations with proper updateValueFactory handling - Added detailed thread-safety documentation 3. **ApplicationImpl model usage fence checks race condition** - Two threads calling Init() simultaneously could both pass fence checks before either set ModelUsage, allowing improper model mixing - Added _modelUsageLock for thread-safe synchronization - Made all ModelUsage operations atomic (Instance getter, SetInstance, MarkInstanceBasedModelUsed, ResetModelUsageTracking, Init fence checks) **Files Changed:** - Terminal.Gui/App/ApplicationImpl.cs - Added _modelUsageLock, made all ModelUsage access thread-safe - Terminal.Gui/App/ApplicationImpl.Lifecycle.cs - Thread-safe fence checks in Init(), sync keyboard keys from Application properties - Terminal.Gui/App/Keyboard/KeyboardImpl.cs - Fixed constructor to not trigger ApplicationImpl.Instance - Terminal.Gui/Input/InputBindings.cs - Fixed Replace() race condition with proper atomic operations **Testing:** - All 11 ApplicationImplTests pass - All 9 KeyboardImplThreadSafetyTests pass - All 10 InputBindingsThreadSafetyTests pass - No more intermittent "Cannot use modern instance-based model after using legacy static Application model" errors in parallel test execution The root cause was KeyboardImpl constructor accessing Application static properties during object creation, which would lazily initialize ApplicationImpl.Instance and set the wrong ModelUsage before Application.Create() could mark it as InstanceBased. * Warning cleanup * docs: Add comprehensive MessageBox and Clipboard API documentation - Updated MessageBox class docs with nullable return value explanation - Created docfx/docs/messagebox-clipboard-changes-v2.md migration guide - Updated migratingfromv1.md with quick links to major changes - Created PR-SUMMARY.md documenting all changes - Added examples for both instance-based and legacy patterns - Documented application model fencing and thread safety improvements The documentation covers: • MessageBox nullable int? returns (null = cancelled) • Clipboard refactoring from static to instance-based • Application model usage fencing to prevent pattern mixing • Thread safety improvements in KeyboardImpl and InputBindings • Complete migration guide with code examples • Benefits and rationale for all changes * Refactor static properties to use backing fields Refactored static properties in multiple classes (`Button`, `CheckBox`, `Dialog`, `FrameView`, `MessageBox`, `StatusBar`, and `Window`) to use private backing fields for better encapsulation and configurability. Default values are now stored in private static fields, allowing overrides via configuration files (e.g., `Resources/config.json`). Updated property definitions to use `get`/`set` accessors interacting with the backing fields. Retained the `[ConfigurationProperty]` attribute to ensure runtime configurability. Removed redundant code, improved XML documentation, adjusted namespace declarations for consistency, and performed general code cleanup to enhance readability and maintainability. * Fix Windows-only parallel test failure by preventing ConfigurationManager from triggering ApplicationImpl.Instance Problem: `MessageBoxTests.Location_And_Size_Correct` was failing only on Windows in parallel tests with: System.InvalidOperationException: Cannot use modern instance-based model (Application.Create) after using legacy static Application model (Application.Init/ApplicationImpl.Instance). Root Cause (maybe): View classes (MessageBox, Dialog, Window, Button, CheckBox, FrameView, StatusBar) had `[ConfigurationProperty]` decorated auto-properties with inline initializers. When ConfigurationManager's module initializer scanned assemblies using reflection, accessing these auto-properties could trigger lazy initialization of other static members, which in some cases indirectly referenced `ApplicationImpl.Instance`, marking the model as "legacy" before parallel tests called `Application.Create()`. Solution: Converted all `[ConfigurationProperty]` auto-properties in View classes to use private backing fields with explicit getters/setters, matching the pattern used by `Application.QuitKey`. This prevents any code execution during reflection-based property discovery. Files Changed: - Terminal.Gui/Views/MessageBox.cs - 4 properties converted - Terminal.Gui/Views/Dialog.cs - 6 properties converted - Terminal.Gui/Views/Window.cs - 2 properties converted - Terminal.Gui/Views/Button.cs - 2 properties converted - Terminal.Gui/Views/CheckBox.cs - 1 property converted - Terminal.Gui/Views/FrameView.cs - 1 property converted - Terminal.Gui/Views/StatusBar.cs - 1 property converted Test Reorganization: - Moved `ConfigurationManagerTests.GetConfigPropertiesByScope_Gets` from UnitTestsParallelizable to UnitTests (defines custom ConfigurationProperty which affects global state) - Moved `SourcesManagerTests.Sources_StaysConsistentWhenUpdateFails` from UnitTestsParallelizable to UnitTests (modifies static ConfigurationManager.ThrowOnJsonErrors property) Best Practice: All `[ConfigurationProperty]` decorated static properties should use private backing fields to avoid triggering lazy initialization during ConfigurationManager's module initialization. Fixes: Windows-only parallel test failure in MessageBoxTests * Add thread-safety to CollectionNavigator classes - Add lock-based synchronization to CollectionNavigatorBase for _searchString and _lastKeystroke fields - Add lock-based synchronization to CollectionNavigator for Collection property access - Protect ElementAt and GetCollectionLength methods with locks - Add 6 comprehensive thread-safety tests covering: - Concurrent SearchString access - Concurrent Collection property access - Concurrent navigation operations (50 parallel tasks) - Concurrent collection modification with readers/writers - Concurrent search string changes - Stress test with 100 tasks × 1000 operations each All tests pass (31/31) including new thread-safety tests. The implementation uses lock-based synchronization rather than concurrent collections because: - IList interface is not thread-safe by design - CollectionNavigator is internal and used by UI components (ListView/TreeView) - Matches existing Terminal.Gui patterns (Scope, ConfigProperty) - Provides simpler and more predictable behavior Fixes thread-safety issues when CollectionNavigator is accessed from multiple threads. * cleanup * Run parallel unit tests 10 times with varying parallelization to expose concurrency issues Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fix parallel unit tests workflow - use proper xUnit parallelization parameters Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fix environment variable reference in workflow - use proper bash syntax Co-authored-by: tig <585482+tig@users.noreply.github.com> * Run parallel tests 10 times sequentially instead of matrix expansion Co-authored-by: tig <585482+tig@users.noreply.github.com> * Make ConfigurationManager thread-safe - use ConcurrentDictionary and add locks Co-authored-by: tig <585482+tig@users.noreply.github.com> * Add Debug.Fail to detect legacy Application usage in parallelizable tests Co-authored-by: tig <585482+tig@users.noreply.github.com> * Move ScrollSliderTests to UnitTests project - they access legacy Application model Co-authored-by: tig <585482+tig@users.noreply.github.com> * Revert ScrollSliderTests move and document root cause analysis Co-authored-by: tig <585482+tig@users.noreply.github.com> * Remove Debug.Fail and move ScrollSliderTests to UnitTests project Co-authored-by: tig <585482+tig@users.noreply.github.com> * Re-add Debug.Fail to detect legacy Application usage in parallelizable tests Co-authored-by: tig <585482+tig@users.noreply.github.com> * Refactor tests and improve parallelization support Commented out `Debug.Fail` statements in `Application.Lifecycle.cs` and `ApplicationImpl.cs` to prevent interruptions during parallel tests. Refactored `ToString` in `ApplicationImpl.cs` to use an expression-bodied member and removed unused imports. Rewrote tests in `ClipRegionTests.cs` and `ScrollSliderTests.cs` to remove global state dependencies and migrated them to the `UnitTests_Parallelizable` namespace. Enabled nullable annotations and updated assertions for clarity and modern patterns. Improved test coverage by adding scenarios for clamping, layout, and size calculations. Updated `README.md` to include `[SetupFakeApplication]` in the list of patterns that block parallelization and clarified migration guidelines. Replaced `[SetupFakeDriver]` with `[SetupFakeApplication]` in examples. Added `` to `UnitTests.csproj` for better organization. Adjusted test project references to reflect test migration. Enhanced test output validation in `ScrollSliderTests.cs`. Removed redundant test cases and improved documentation to align with modern C# practices and ensure maintainability. * marked as a "TODO" for potential future configurability. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tig <585482+tig@users.noreply.github.com> Co-authored-by: Tig --- .github/workflows/unit-tests.yml | 102 ++- .gitignore | 1 + Examples/Example/Example.cs | 4 +- Examples/NativeAot/Program.cs | 4 +- Examples/RunnableWrapperExample/Program.cs | 57 +- Examples/SelfContained/Program.cs | 4 +- Examples/UICatalog/Scenario.cs | 6 +- Examples/UICatalog/Scenarios/Adornments.cs | 16 +- .../AnimationScenario/AnimationScenario.cs | 2 +- Examples/UICatalog/Scenarios/Bars.cs | 10 +- Examples/UICatalog/Scenarios/Buttons.cs | 6 +- Examples/UICatalog/Scenarios/ChineseUI.cs | 5 +- .../Scenarios/ConfigurationEditor.cs | 6 +- Examples/UICatalog/Scenarios/ContextMenus.cs | 10 +- Examples/UICatalog/Scenarios/CsvEditor.cs | 30 +- Examples/UICatalog/Scenarios/Dialogs.cs | 2 +- .../UICatalog/Scenarios/DynamicStatusBar.cs | 10 +- Examples/UICatalog/Scenarios/Editor.cs | 21 +- .../Scenarios/EditorsAndHelpers/DimEditor.cs | 2 +- .../Scenarios/EditorsAndHelpers/PosEditor.cs | 2 +- .../UICatalog/Scenarios/FileDialogExamples.cs | 10 +- Examples/UICatalog/Scenarios/Generic.cs | 2 +- Examples/UICatalog/Scenarios/HexEditor.cs | 8 +- Examples/UICatalog/Scenarios/Images.cs | 6 +- .../UICatalog/Scenarios/InteractiveTree.cs | 2 +- Examples/UICatalog/Scenarios/KeyBindings.cs | 8 +- Examples/UICatalog/Scenarios/ListColumns.cs | 2 +- Examples/UICatalog/Scenarios/MessageBoxes.cs | 18 +- .../UICatalog/Scenarios/MultiColouredTable.cs | 2 +- Examples/UICatalog/Scenarios/Navigation.cs | 4 +- Examples/UICatalog/Scenarios/Notepad.cs | 16 +- Examples/UICatalog/Scenarios/RunTExample.cs | 6 +- .../Scenarios/RuneWidthGreaterThanOne.cs | 6 +- Examples/UICatalog/Scenarios/Shortcuts.cs | 2 +- .../Scenarios/SingleBackgroundWorker.cs | 8 +- Examples/UICatalog/Scenarios/TableEditor.cs | 14 +- Examples/UICatalog/Scenarios/Transparent.cs | 4 +- .../UICatalog/Scenarios/ViewportSettings.cs | 4 +- .../Scenarios/WindowsAndFrameViews.cs | 6 +- Examples/UICatalog/Scenarios/WizardAsView.cs | 9 +- Examples/UICatalog/Scenarios/Wizards.cs | 481 ++++++------ Examples/UICatalog/UICatalogTop.cs | 1 + Terminal.Gui/App/Application.Clipboard.cs | 15 + Terminal.Gui/App/Application.Driver.cs | 36 +- Terminal.Gui/App/Application.Lifecycle.cs | 21 +- Terminal.Gui/App/Application.Mouse.cs | 14 +- Terminal.Gui/App/Application.Navigation.cs | 58 +- Terminal.Gui/App/Application.Run.cs | 30 +- ....Current.cs => Application.TopRunnable.cs} | 2 +- Terminal.Gui/App/ApplicationImpl.Lifecycle.cs | 152 ++-- Terminal.Gui/App/ApplicationImpl.Run.cs | 13 +- Terminal.Gui/App/ApplicationImpl.cs | 148 +++- Terminal.Gui/App/ApplicationModelUsage.cs | 16 + Terminal.Gui/App/ApplicationNavigation.cs | 2 +- .../App/ApplicationRunnableExtensions.cs | 2 +- Terminal.Gui/App/Clipboard/Clipboard.cs | 32 + Terminal.Gui/App/IApplication.cs | 31 +- Terminal.Gui/App/IterationEventArgs.cs | 5 - Terminal.Gui/App/Keyboard/KeyboardImpl.cs | 161 ++-- Terminal.Gui/App/Mouse/MouseImpl.cs | 31 +- Terminal.Gui/App/Runnable/IRunnable.cs | 4 +- .../Configuration/ConfigurationManager.cs | 84 +- Terminal.Gui/Configuration/SourcesManager.cs | 12 +- .../Drivers/FakeDriver/FakeInputProcessor.cs | 8 +- Terminal.Gui/Drivers/IInputProcessor.cs | 15 +- Terminal.Gui/Drivers/InputProcessorImpl.cs | 2 +- Terminal.Gui/Drivers/OutputBase.cs | 18 +- .../WindowsDriver/WindowsInputProcessor.cs | 2 +- .../Drivers/WindowsDriver/WindowsOutput.cs | 5 +- Terminal.Gui/FileServices/IFileOperations.cs | 9 +- Terminal.Gui/Input/InputBindings.cs | 178 +++-- Terminal.Gui/ViewBase/Runnable.cs | 2 +- Terminal.Gui/ViewBase/RunnableWrapper.cs | 3 +- Terminal.Gui/ViewBase/View.Diagnostics.cs | 1 + .../ViewBase/View.Drawing.Attribute.cs | 4 +- Terminal.Gui/Views/Button.cs | 16 +- Terminal.Gui/Views/CharMap/CharMap.cs | 14 +- Terminal.Gui/Views/CheckBox.cs | 10 +- .../CollectionNavigator.cs | 39 +- .../CollectionNavigatorBase.cs | 48 +- .../Views/Color/ColorPicker.Prompt.cs | 4 +- Terminal.Gui/Views/Dialog.cs | 46 +- .../FileDialogs/DefaultFileOperations.cs | 18 +- Terminal.Gui/Views/FileDialogs/FileDialog.cs | 20 +- Terminal.Gui/Views/FrameView.cs | 14 +- Terminal.Gui/Views/GraphView/GraphView.cs | 1 - Terminal.Gui/Views/Menu/MenuBar.cs | 8 +- Terminal.Gui/Views/MessageBox.cs | 732 ++++++++++++------ Terminal.Gui/Views/Selectors/SelectorBase.cs | 2 +- Terminal.Gui/Views/StatusBar.cs | 10 +- Terminal.Gui/Views/TableView/TableView.cs | 4 +- Terminal.Gui/Views/TextInput/TextField.cs | 10 +- Terminal.Gui/Views/TextInput/TextView.cs | 8 +- Terminal.Gui/Views/Window.cs | 17 +- Terminal.Gui/Views/Wizard/Wizard.cs | 2 +- .../FluentTests/FileDialogFluentTests.cs | 23 +- .../FluentTests/GuiTestContextTests.cs | 2 +- Tests/StressTests/ScenariosStressTests.cs | 2 +- .../GuiTestContext.Input.cs | 7 +- .../GuiTestContext.cs | 52 +- .../TerminalGuiFluentTesting/NetSequences.cs | 53 -- Tests/TerminalGuiFluentTesting/With.cs | 5 +- .../Application.NavigationTests.cs | 4 +- .../ApplicationForceDriverTests.cs | 6 +- .../ApplicationImplBeginEndTests.cs | 2 +- .../ApplicationModelFencingTests.cs | 162 ++++ .../Application/ApplicationPopoverTests.cs | 22 +- .../UnitTests/Application/ApplicationTests.cs | 29 +- .../Mouse/ApplicationMouseTests.cs | 2 +- Tests/UnitTests/Clipboard/ClipboardTests.cs | 21 + .../Configuration/ConfigurationMangerTests.cs | 16 + .../Configuration/SourcesManagerTests.cs | 47 ++ Tests/UnitTests/Dialogs/DialogTests.cs | 8 +- Tests/UnitTests/Dialogs/MessageBoxTests.cs | 544 ------------- Tests/UnitTests/DriverAssert.cs | 28 +- Tests/UnitTests/Drivers/ClipRegionTests.cs | 105 --- Tests/UnitTests/Drivers/DriverTests.cs | 63 -- .../FakeDriver/FakeApplicationFactory.cs | 0 .../FakeDriver/FakeApplicationLifecycle.cs | 0 Tests/UnitTests/FakeDriverBase.cs | 12 +- .../UICatalog/ScenarioTests.cs | 19 +- Tests/UnitTests/UnitTests.csproj | 3 + Tests/UnitTests/View/ArrangementTests.cs | 6 +- Tests/UnitTests/View/Draw/DrawTests.cs | 2 +- Tests/UnitTests/View/Layout/Pos.Tests.cs | 9 +- Tests/UnitTests/View/Layout/Pos.ViewTests.cs | 6 +- .../View/Navigation/CanFocusTests.cs | 2 +- .../UnitTests/View/Navigation/EnabledTests.cs | 7 +- Tests/UnitTests/View/SubviewTests.cs | 2 +- Tests/UnitTests/View/ViewTests.cs | 2 +- Tests/UnitTests/Views/GraphViewTests.cs | 21 +- Tests/UnitTests/Views/LabelTests.cs | 4 +- Tests/UnitTests/Views/ShortcutTests.cs | 2 +- Tests/UnitTests/Views/StatusBarTests.cs | 7 +- Tests/UnitTests/Views/TableViewTests.cs | 22 +- Tests/UnitTests/Views/TextFieldTests.cs | 5 +- Tests/UnitTests/Views/TextViewTests.cs | 87 ++- Tests/UnitTests/Views/ToplevelTests.cs | 4 +- .../Application/ApplicationImplTests.cs | 33 +- .../KeyboardImplThreadSafetyTests.cs | 520 +++++++++++++ .../Runnable/RunnableEdgeCasesTests.cs | 1 + .../Runnable/RunnableIntegrationTests.cs | 327 ++++---- .../Runnable/RunnableLifecycleTests.cs | 1 + .../Configuration/ConfigurationMangerTests.cs | 21 - .../Configuration/SourcesManagerTests.cs | 41 - .../Drivers/ClipRegionTests.cs | 98 +++ .../Drivers/DriverTests.cs | 60 +- .../Drivers/ToAnsiTests.cs | 6 +- .../Input/EnqueueMouseEventTests.cs | 41 +- .../Input/InputBindingsThreadSafetyTests.cs | 533 +++++++++++++ .../Input/Keyboard/KeyBindingsTests.cs | 99 ++- Tests/UnitTestsParallelizable/README.md | 15 +- .../TestDateAttribute.cs | 2 +- Tests/UnitTestsParallelizable/TestSetup.cs | 30 +- .../Text/CollectionNavigatorTests.cs | 251 +++++- .../View/Draw/ViewDrawingClippingTests.cs | 18 - .../View/Draw/ViewDrawingFlowTests.cs | 3 - .../Views/DateFieldTests.cs | 39 +- .../Views/MessageBoxTests.cs | 625 +++++++++++++++ .../Views/ScrollSliderTests.cs | 1 - .../Views/TimeFieldTests.cs | 41 +- .../UnitTestsParallelizable/xunit.runner.json | 3 +- docfx/docs/migratingfromv1.md | 596 -------------- 163 files changed, 4900 insertions(+), 2988 deletions(-) create mode 100644 Terminal.Gui/App/Application.Clipboard.cs rename Terminal.Gui/App/{Application.Current.cs => Application.TopRunnable.cs} (91%) create mode 100644 Terminal.Gui/App/ApplicationModelUsage.cs delete mode 100644 Terminal.Gui/App/IterationEventArgs.cs delete mode 100644 Tests/TerminalGuiFluentTesting/NetSequences.cs rename Tests/{UnitTestsParallelizable => UnitTests}/Application/ApplicationForceDriverTests.cs (86%) create mode 100644 Tests/UnitTests/Application/ApplicationModelFencingTests.cs create mode 100644 Tests/UnitTests/Configuration/SourcesManagerTests.cs delete mode 100644 Tests/UnitTests/Dialogs/MessageBoxTests.cs delete mode 100644 Tests/UnitTests/Drivers/ClipRegionTests.cs delete mode 100644 Tests/UnitTests/Drivers/DriverTests.cs rename Tests/{TerminalGuiFluentTesting => UnitTests}/FakeDriver/FakeApplicationFactory.cs (100%) rename Tests/{TerminalGuiFluentTesting => UnitTests}/FakeDriver/FakeApplicationLifecycle.cs (100%) rename Tests/{IntegrationTests => UnitTests}/UICatalog/ScenarioTests.cs (97%) rename Tests/{UnitTests => UnitTestsParallelizable}/Application/ApplicationImplTests.cs (99%) create mode 100644 Tests/UnitTestsParallelizable/Application/KeyboardImplThreadSafetyTests.cs delete mode 100644 Tests/UnitTestsParallelizable/Configuration/ConfigurationMangerTests.cs create mode 100644 Tests/UnitTestsParallelizable/Drivers/ClipRegionTests.cs create mode 100644 Tests/UnitTestsParallelizable/Input/InputBindingsThreadSafetyTests.cs rename Tests/{UnitTests => UnitTestsParallelizable}/TestDateAttribute.cs (95%) rename Tests/{UnitTests => UnitTestsParallelizable}/Views/DateFieldTests.cs (92%) create mode 100644 Tests/UnitTestsParallelizable/Views/MessageBoxTests.cs rename Tests/{UnitTests => UnitTestsParallelizable}/Views/TimeFieldTests.cs (87%) delete mode 100644 docfx/docs/migratingfromv1.md diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 4488bb645..1542752d7 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -120,7 +120,7 @@ jobs: matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] - timeout-minutes: 15 + timeout-minutes: 60 steps: - name: Checkout code @@ -154,35 +154,81 @@ jobs: shell: bash run: echo "VSTEST_DUMP_PATH=logs/UnitTestsParallelizable/${{ runner.os }}/" >> $GITHUB_ENV - - name: Run UnitTestsParallelizable + - name: Run UnitTestsParallelizable (10 iterations with varying parallelization) shell: bash run: | - 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 + # Run tests 10 times with different parallelization settings to expose concurrency issues + for RUN in {1..10}; do + echo "============================================" + echo "Starting test run $RUN of 10" + echo "============================================" + + # Use a combination of run number and timestamp to create different execution patterns + SEED=$((1000 + $RUN + $(date +%s) % 1000)) + echo "Using randomization seed: $SEED" + + # Vary the xUnit parallelization based on run number to expose race conditions + # Runs 1-3: Default parallelization (2x CPU cores) + # Runs 4-6: Max parallelization (unlimited) + # Runs 7-9: Single threaded (1) + # Run 10: Random (1-4 threads) + if [ $RUN -le 3 ]; then + XUNIT_MAX_PARALLEL_THREADS="2x" + echo "Run $RUN: Using default parallelization (2x)" + elif [ $RUN -le 6 ]; then + XUNIT_MAX_PARALLEL_THREADS="unlimited" + echo "Run $RUN: Using maximum parallelization (unlimited)" + elif [ $RUN -le 9 ]; then + XUNIT_MAX_PARALLEL_THREADS="1" + echo "Run $RUN: Using single-threaded execution" + else + # Random parallelization based on seed + PROC_COUNT=$(( ($SEED % 4) + 1 )) + XUNIT_MAX_PARALLEL_THREADS="$PROC_COUNT" + echo "Run $RUN: Using random parallelization with $PROC_COUNT threads" + fi + + # Run tests with or without coverage based on OS and run number + if [ "${{ runner.os }}" == "Linux" ] && [ $RUN -eq 1 ]; then + echo "Run $RUN: Running with coverage collection" + dotnet test Tests/UnitTestsParallelizable \ + --no-build \ + --verbosity normal \ + --collect:"XPlat Code Coverage" \ + --settings Tests/UnitTests/runsettings.coverage.xml \ + --diag:logs/UnitTestsParallelizable/${{ runner.os }}/run${RUN}-logs.txt \ + --blame \ + --blame-crash \ + --blame-hang \ + --blame-hang-timeout 60s \ + --blame-crash-collect-always \ + -- xUnit.MaxParallelThreads=${XUNIT_MAX_PARALLEL_THREADS} + else + dotnet test Tests/UnitTestsParallelizable \ + --no-build \ + --verbosity normal \ + --settings Tests/UnitTestsParallelizable/runsettings.xml \ + --diag:logs/UnitTestsParallelizable/${{ runner.os }}/run${RUN}-logs.txt \ + --blame \ + --blame-crash \ + --blame-hang \ + --blame-hang-timeout 60s \ + --blame-crash-collect-always \ + -- xUnit.MaxParallelThreads=${XUNIT_MAX_PARALLEL_THREADS} + fi + + if [ $? -ne 0 ]; then + echo "ERROR: Test run $RUN failed!" + exit 1 + fi + + echo "Test run $RUN completed successfully" + echo "" + done + + echo "============================================" + echo "All 10 test runs completed successfully!" + echo "============================================" - name: Upload UnitTestsParallelizable Logs if: always() diff --git a/.gitignore b/.gitignore index cdec09ec2..79eff65ea 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,4 @@ log.* !/Tests/coverage/.gitkeep # keep folder in repo /Tests/report/ *.cobertura.xml +/docfx/docs/migratingfromv1.md diff --git a/Examples/Example/Example.cs b/Examples/Example/Example.cs index 82cd60d6e..f6c13eb6b 100644 --- a/Examples/Example/Example.cs +++ b/Examples/Example/Example.cs @@ -77,13 +77,13 @@ public class ExampleWindow : Window { if (userNameText.Text == "admin" && passwordText.Text == "password") { - MessageBox.Query ("Logging In", "Login Successful", "Ok"); + MessageBox.Query (App, "Logging In", "Login Successful", "Ok"); UserName = userNameText.Text; Application.RequestStop (); } else { - MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok"); + MessageBox.ErrorQuery (App, "Logging In", "Incorrect username or password", "Ok"); } // When Accepting is handled, set e.Handled to true to prevent further processing. e.Handled = true; diff --git a/Examples/NativeAot/Program.cs b/Examples/NativeAot/Program.cs index 3de9bfeec..501adb2ed 100644 --- a/Examples/NativeAot/Program.cs +++ b/Examples/NativeAot/Program.cs @@ -101,13 +101,13 @@ public class ExampleWindow : Window { if (userNameText.Text == "admin" && passwordText.Text == "password") { - MessageBox.Query ("Logging In", "Login Successful", "Ok"); + MessageBox.Query (App, "Logging In", "Login Successful", "Ok"); UserName = userNameText.Text; Application.RequestStop (); } else { - MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok"); + MessageBox.ErrorQuery (App, "Logging In", "Incorrect username or password", "Ok"); } // Anytime Accepting is handled, make sure to set e.Handled to true. e.Handled = true; diff --git a/Examples/RunnableWrapperExample/Program.cs b/Examples/RunnableWrapperExample/Program.cs index 41a2df32b..8dadab75e 100644 --- a/Examples/RunnableWrapperExample/Program.cs +++ b/Examples/RunnableWrapperExample/Program.cs @@ -13,41 +13,42 @@ var textField = new TextField { Width = 40, Text = "Default text" }; textField.Title = "Enter your name"; textField.BorderStyle = LineStyle.Single; -var textRunnable = textField.AsRunnable (tf => tf.Text); +RunnableWrapper textRunnable = textField.AsRunnable (tf => tf.Text); app.Run (textRunnable); if (textRunnable.Result is { } name) { - MessageBox.Query ("Result", $"You entered: {name}", "OK"); + MessageBox.Query (app, "Result", $"You entered: {name}", "OK"); } else { - MessageBox.Query ("Result", "Canceled", "OK"); + MessageBox.Query (app, "Result", "Canceled", "OK"); } + textRunnable.Dispose (); // Example 2: Use IApplication.RunView() for one-liner -var selectedColor = app.RunView ( - new ColorPicker - { - Title = "Pick a Color", - BorderStyle = LineStyle.Single - }, - cp => cp.SelectedColor); +Color selectedColor = app.RunView ( + new ColorPicker + { + Title = "Pick a Color", + BorderStyle = LineStyle.Single + }, + cp => cp.SelectedColor); -MessageBox.Query ("Result", $"Selected color: {selectedColor}", "OK"); +MessageBox.Query (app, "Result", $"Selected color: {selectedColor}", "OK"); // Example 3: FlagSelector with typed enum result -var flagSelector = new FlagSelector +FlagSelector flagSelector = new() { Title = "Choose Styles", BorderStyle = LineStyle.Single }; -var flagsRunnable = flagSelector.AsRunnable (fs => fs.Value); +RunnableWrapper, SelectorStyles?> flagsRunnable = flagSelector.AsRunnable (fs => fs.Value); app.Run (flagsRunnable); -MessageBox.Query ("Result", $"Selected styles: {flagsRunnable.Result}", "OK"); +MessageBox.Query (app, "Result", $"Selected styles: {flagsRunnable.Result}", "OK"); flagsRunnable.Dispose (); // Example 4: Any View without result extraction @@ -58,26 +59,28 @@ var label = new Label Y = Pos.Center () }; -var labelRunnable = label.AsRunnable (); +RunnableWrapper labelRunnable = label.AsRunnable (); app.Run (labelRunnable); // Can still access the wrapped view -MessageBox.Query ("Result", $"Label text was: {labelRunnable.WrappedView.Text}", "OK"); +MessageBox.Query (app, "Result", $"Label text was: {labelRunnable.WrappedView.Text}", "OK"); labelRunnable.Dispose (); // Example 5: Complex custom View made runnable -var formView = CreateCustomForm (); -var formRunnable = formView.AsRunnable (ExtractFormData); +View formView = CreateCustomForm (); +RunnableWrapper formRunnable = formView.AsRunnable (ExtractFormData); app.Run (formRunnable); if (formRunnable.Result is { } formData) { MessageBox.Query ( - "Form Results", - $"Name: {formData.Name}\nAge: {formData.Age}\nAgreed: {formData.Agreed}", - "OK"); + app, + "Form Results", + $"Name: {formData.Name}\nAge: {formData.Age}\nAgreed: {formData.Agreed}", + "OK"); } + formRunnable.Dispose (); app.Shutdown (); @@ -126,10 +129,10 @@ View CreateCustomForm () }; okButton.Accepting += (s, e) => - { - form.App?.RequestStop (); - e.Handled = true; - }; + { + form.App?.RequestStop (); + e.Handled = true; + }; form.Add (new Label { Text = "Name:", X = 2, Y = 1 }); form.Add (nameField); @@ -148,7 +151,7 @@ FormData ExtractFormData (View form) var ageField = form.SubViews.FirstOrDefault (v => v.Id == "ageField") as TextField; var agreeCheckbox = form.SubViews.FirstOrDefault (v => v.Id == "agreeCheckbox") as CheckBox; - return new FormData + return new() { Name = nameField?.Text ?? string.Empty, Age = int.TryParse (ageField?.Text, out int age) ? age : 0, @@ -157,7 +160,7 @@ FormData ExtractFormData (View form) } // Result type for custom form -record FormData +internal record FormData { public string Name { get; init; } = string.Empty; public int Age { get; init; } diff --git a/Examples/SelfContained/Program.cs b/Examples/SelfContained/Program.cs index 02109bf3a..aa226273b 100644 --- a/Examples/SelfContained/Program.cs +++ b/Examples/SelfContained/Program.cs @@ -100,13 +100,13 @@ public class ExampleWindow : Window { if (userNameText.Text == "admin" && passwordText.Text == "password") { - MessageBox.Query ("Logging In", "Login Successful", "Ok"); + MessageBox.Query (App, "Logging In", "Login Successful", "Ok"); UserName = userNameText.Text; Application.RequestStop (); } else { - MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok"); + MessageBox.ErrorQuery (App, "Logging In", "Incorrect username or password", "Ok"); } // When Accepting is handled, set e.Handled to true to prevent further processing. e.Handled = true; diff --git a/Examples/UICatalog/Scenario.cs b/Examples/UICatalog/Scenario.cs index 4d9f0c759..d52308b84 100644 --- a/Examples/UICatalog/Scenario.cs +++ b/Examples/UICatalog/Scenario.cs @@ -67,7 +67,7 @@ namespace UICatalog; /// }; /// /// var button = new Button { X = Pos.Center (), Y = Pos.Center (), Text = "Press me!" }; -/// button.Accept += (s, e) => MessageBox.ErrorQuery ("Error", "You pressed the button!", "Ok"); +/// button.Accept += (s, e) => MessageBox.ErrorQuery (App, "Error", "You pressed the button!", "Ok"); /// appWindow.Add (button); /// /// // Run - Start the application. @@ -210,12 +210,12 @@ public class Scenario : IDisposable void OnClearedContents (object? sender, EventArgs args) => BenchmarkResults.ClearedContentCount++; } - private void OnApplicationOnIteration (object? s, IterationEventArgs a) + private void OnApplicationOnIteration (object? s, EventArgs a) { BenchmarkResults.IterationCount++; if (BenchmarkResults.IterationCount > BENCHMARK_MAX_NATURAL_ITERATIONS + (_demoKeys!.Count * BENCHMARK_KEY_PACING)) { - Application.RequestStop (); + a.Value?.RequestStop (); } } diff --git a/Examples/UICatalog/Scenarios/Adornments.cs b/Examples/UICatalog/Scenarios/Adornments.cs index 938d23a53..6dd491f65 100644 --- a/Examples/UICatalog/Scenarios/Adornments.cs +++ b/Examples/UICatalog/Scenarios/Adornments.cs @@ -11,7 +11,7 @@ public class Adornments : Scenario { Application.Init (); - Window app = new () + Window appWindow = new () { Title = GetQuitKeyAndName (), BorderStyle = LineStyle.None @@ -28,7 +28,7 @@ public class Adornments : Scenario editor.Border!.Thickness = new (1, 2, 1, 1); - app.Add (editor); + appWindow.Add (editor); var window = new Window { @@ -38,7 +38,7 @@ public class Adornments : Scenario Width = Dim.Fill (Dim.Func (_ => editor.Frame.Width)), Height = Dim.Fill () }; - app.Add (window); + appWindow.Add (window); var tf1 = new TextField { Width = 10, Text = "TextField" }; var color = new ColorPicker16 { Title = "BG", BoxHeight = 1, BoxWidth = 1, X = Pos.AnchorEnd () }; @@ -60,7 +60,7 @@ public class Adornments : Scenario var button = new Button { X = Pos.Center (), Y = Pos.Center (), Text = "Press me!" }; button.Accepting += (s, e) => - MessageBox.Query (20, 7, "Hi", $"Am I a {window.GetType ().Name}?", "Yes", "No"); + MessageBox.Query (appWindow.App, 20, 7, "Hi", $"Am I a {window.GetType ().Name}?", "Yes", "No"); var label = new TextView { @@ -121,7 +121,7 @@ public class Adornments : Scenario Text = "text (Y = 1)", CanFocus = true }; - textFieldInPadding.Accepting += (s, e) => MessageBox.Query (20, 7, "TextField", textFieldInPadding.Text, "Ok"); + textFieldInPadding.Accepting += (s, e) => MessageBox.Query (appWindow.App, 20, 7, "TextField", textFieldInPadding.Text, "Ok"); window.Padding.Add (textFieldInPadding); var btnButtonInPadding = new Button @@ -132,7 +132,7 @@ public class Adornments : Scenario CanFocus = true, HighlightStates = MouseState.None, }; - btnButtonInPadding.Accepting += (s, e) => MessageBox.Query (20, 7, "Hi", "Button in Padding Pressed!", "Ok"); + btnButtonInPadding.Accepting += (s, e) => MessageBox.Query (appWindow.App, 20, 7, "Hi", "Button in Padding Pressed!", "Ok"); btnButtonInPadding.BorderStyle = LineStyle.Dashed; btnButtonInPadding.Border!.Thickness = new (1, 1, 1, 1); window.Padding.Add (btnButtonInPadding); @@ -155,8 +155,8 @@ public class Adornments : Scenario editor.AutoSelectSuperView = window; editor.AutoSelectAdornments = true; - Application.Run (app); - app.Dispose (); + Application.Run (appWindow); + appWindow.Dispose (); Application.Shutdown (); } diff --git a/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs b/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs index 7d1b36020..a35453970 100644 --- a/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs +++ b/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs @@ -78,7 +78,7 @@ public class AnimationScenario : Scenario if (!f.Exists) { Debug.WriteLine ($"Could not find {f.FullName}"); - MessageBox.ErrorQuery ("Could not find gif", $"Could not find\n{f.FullName}", "Ok"); + MessageBox.ErrorQuery (_imageView?.App, "Could not find gif", $"Could not find\n{f.FullName}", "Ok"); return; } diff --git a/Examples/UICatalog/Scenarios/Bars.cs b/Examples/UICatalog/Scenarios/Bars.cs index 669f4e9c0..688af56cf 100644 --- a/Examples/UICatalog/Scenarios/Bars.cs +++ b/Examples/UICatalog/Scenarios/Bars.cs @@ -309,7 +309,7 @@ public class Bars : Scenario // new TimeSpan (0), // () => // { - // MessageBox.Query ("File", "New"); + // MessageBox.Query (App, "File", "New"); // return false; // }); @@ -331,7 +331,7 @@ public class Bars : Scenario // new TimeSpan (0), // () => // { - // MessageBox.Query ("File", "Open"); + // MessageBox.Query (App, "File", "Open"); // return false; // }); @@ -353,7 +353,7 @@ public class Bars : Scenario // new TimeSpan (0), // () => // { - // MessageBox.Query ("File", "Save"); + // MessageBox.Query (App, "File", "Save"); // return false; // }); @@ -375,7 +375,7 @@ public class Bars : Scenario // new TimeSpan (0), // () => // { - // MessageBox.Query ("File", "Save As"); + // MessageBox.Query (App, "File", "Save As"); // return false; // }); @@ -555,7 +555,7 @@ public class Bars : Scenario return; - void Button_Clicked (object sender, EventArgs e) { MessageBox.Query ("Hi", $"You clicked {sender}"); } + void Button_Clicked (object sender, EventArgs e) { MessageBox.Query ((sender as View)?.App, "Hi", $"You clicked {sender}"); } } diff --git a/Examples/UICatalog/Scenarios/Buttons.cs b/Examples/UICatalog/Scenarios/Buttons.cs index f2ea4572f..404bbf4e4 100644 --- a/Examples/UICatalog/Scenarios/Buttons.cs +++ b/Examples/UICatalog/Scenarios/Buttons.cs @@ -59,7 +59,7 @@ public class Buttons : Scenario if (e.Handled) { - MessageBox.ErrorQuery ("Error", "This button is no longer the Quit button; the Swap Default button is.", "_Ok"); + MessageBox.ErrorQuery ((s as View)?.App, "Error", "This button is no longer the Quit button; the Swap Default button is.", "_Ok"); } }; main.Add (swapButton); @@ -69,7 +69,7 @@ public class Buttons : Scenario button.Accepting += (s, e) => { string btnText = button.Text; - MessageBox.Query ("Message", $"Did you click {txt}?", "Yes", "No"); + MessageBox.Query ((s as View)?.App, "Message", $"Did you click {txt}?", "Yes", "No"); e.Handled = true; }; } @@ -112,7 +112,7 @@ public class Buttons : Scenario ); button.Accepting += (s, e) => { - MessageBox.Query ("Message", "Question?", "Yes", "No"); + MessageBox.Query ((s as View)?.App, "Message", "Question?", "Yes", "No"); e.Handled = true; }; diff --git a/Examples/UICatalog/Scenarios/ChineseUI.cs b/Examples/UICatalog/Scenarios/ChineseUI.cs index cc80c7ea9..26545dc8b 100644 --- a/Examples/UICatalog/Scenarios/ChineseUI.cs +++ b/Examples/UICatalog/Scenarios/ChineseUI.cs @@ -32,8 +32,9 @@ public class ChineseUI : Scenario btn.Accepting += (s, e) => { - int result = MessageBox.Query ( - "Confirm", + int? result = MessageBox.Query ( + (s as View)?.App, + "Confirm", "Are you sure you want to quit ui?", 0, "Yes", diff --git a/Examples/UICatalog/Scenarios/ConfigurationEditor.cs b/Examples/UICatalog/Scenarios/ConfigurationEditor.cs index c0bfcb03d..600f4b98c 100644 --- a/Examples/UICatalog/Scenarios/ConfigurationEditor.cs +++ b/Examples/UICatalog/Scenarios/ConfigurationEditor.cs @@ -153,9 +153,9 @@ public class ConfigurationEditor : Scenario continue; } - int result = MessageBox.Query ( + int? result = MessageBox.Query (editor?.App, "Save Changes", - $"Save changes to {editor.FileInfo!.Name}", + $"Save changes to {editor?.FileInfo!.Name}", "_Yes", "_No", "_Cancel" @@ -164,7 +164,7 @@ public class ConfigurationEditor : Scenario switch (result) { case 0: - editor.Save (); + editor?.Save (); break; diff --git a/Examples/UICatalog/Scenarios/ContextMenus.cs b/Examples/UICatalog/Scenarios/ContextMenus.cs index 5baaabded..541b11943 100644 --- a/Examples/UICatalog/Scenarios/ContextMenus.cs +++ b/Examples/UICatalog/Scenarios/ContextMenus.cs @@ -49,7 +49,7 @@ public class ContextMenus : Scenario var text = "Context Menu"; var width = 20; - CreateWinContextMenu (); + CreateWinContextMenu (ApplicationImpl.Instance); var label = new Label { @@ -108,7 +108,7 @@ public class ContextMenus : Scenario } } - private void CreateWinContextMenu () + private void CreateWinContextMenu (IApplication? app) { _winContextMenu = new ( [ @@ -122,7 +122,7 @@ public class ContextMenus : Scenario { Title = "_Configuration...", HelpText = "Show configuration", - Action = () => MessageBox.Query ( + Action = () => MessageBox.Query (app, 50, 10, "Configuration", @@ -140,7 +140,7 @@ public class ContextMenus : Scenario Title = "_Setup...", HelpText = "Perform setup", Action = () => MessageBox - .Query ( + .Query (app, 50, 10, "Setup", @@ -154,7 +154,7 @@ public class ContextMenus : Scenario Title = "_Maintenance...", HelpText = "Maintenance mode", Action = () => MessageBox - .Query ( + .Query (app, 50, 10, "Maintenance", diff --git a/Examples/UICatalog/Scenarios/CsvEditor.cs b/Examples/UICatalog/Scenarios/CsvEditor.cs index 5690a7509..5831b8feb 100644 --- a/Examples/UICatalog/Scenarios/CsvEditor.cs +++ b/Examples/UICatalog/Scenarios/CsvEditor.cs @@ -215,7 +215,7 @@ public class CsvEditor : Scenario _tableView.Table.Columns ); - int result = MessageBox.Query ( + int? result = MessageBox.Query (ApplicationImpl.Instance, "Column Type", "Pick a data type for the column", "Date", @@ -225,7 +225,7 @@ public class CsvEditor : Scenario "Cancel" ); - if (result <= -1 || result >= 4) + if (result is null || result >= 4) { return; } @@ -308,7 +308,7 @@ public class CsvEditor : Scenario if (_tableView.SelectedColumn == -1) { - MessageBox.ErrorQuery ("No Column", "No column selected", "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "No Column", "No column selected", "Ok"); return; } @@ -320,7 +320,7 @@ public class CsvEditor : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery ("Could not remove column", ex.Message, "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Could not remove column", ex.Message, "Ok"); } } @@ -342,7 +342,7 @@ public class CsvEditor : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery (60, 20, "Failed to set text", ex.Message, "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, 60, 20, "Failed to set text", ex.Message, "Ok"); } _tableView.Update (); @@ -388,7 +388,7 @@ public class CsvEditor : Scenario if (_tableView.SelectedColumn == -1) { - MessageBox.ErrorQuery ("No Column", "No column selected", "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "No Column", "No column selected", "Ok"); return; } @@ -413,7 +413,7 @@ public class CsvEditor : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery ("Error moving column", ex.Message, "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Error moving column", ex.Message, "Ok"); } } @@ -426,7 +426,7 @@ public class CsvEditor : Scenario if (_tableView.SelectedRow == -1) { - MessageBox.ErrorQuery ("No Rows", "No row selected", "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "No Rows", "No row selected", "Ok"); return; } @@ -446,7 +446,7 @@ public class CsvEditor : Scenario return; } - object?[] arrayItems = currentRow.ItemArray; + object? [] arrayItems = currentRow.ItemArray; _currentTable.Rows.Remove (currentRow); // Removing and Inserting the same DataRow seems to result in it loosing its values so we have to create a new instance @@ -462,7 +462,7 @@ public class CsvEditor : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery ("Error moving column", ex.Message, "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Error moving column", ex.Message, "Ok"); } } @@ -470,7 +470,7 @@ public class CsvEditor : Scenario { if (_tableView?.Table is null) { - MessageBox.ErrorQuery ("No Table Loaded", "No table has currently be opened", "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "No Table Loaded", "No table has currently be opened", "Ok"); return true; } @@ -582,7 +582,7 @@ public class CsvEditor : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery ( + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Open Failed", $"Error on line {lineNumber}{Environment.NewLine}{ex.Message}", "Ok" @@ -612,7 +612,7 @@ public class CsvEditor : Scenario { if (_tableView?.Table is null || string.IsNullOrWhiteSpace (_currentFile) || _currentTable is null) { - MessageBox.ErrorQuery ("No file loaded", "No file is currently loaded", "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "No file loaded", "No file is currently loaded", "Ok"); return; } @@ -674,7 +674,7 @@ public class CsvEditor : Scenario if (col.DataType == typeof (string)) { - MessageBox.ErrorQuery ( + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Cannot Format Column", "String columns cannot be Formatted, try adding a new column to the table with a date/numerical Type", "Ok" @@ -711,7 +711,7 @@ public class CsvEditor : Scenario if (_tableView.SelectedColumn == -1) { - MessageBox.ErrorQuery ("No Column", "No column selected", "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "No Column", "No column selected", "Ok"); return; } diff --git a/Examples/UICatalog/Scenarios/Dialogs.cs b/Examples/UICatalog/Scenarios/Dialogs.cs index e7fd1ac77..8e8a6ec99 100644 --- a/Examples/UICatalog/Scenarios/Dialogs.cs +++ b/Examples/UICatalog/Scenarios/Dialogs.cs @@ -266,7 +266,7 @@ public class Dialogs : Scenario { Title = titleEdit.Text, Text = "Dialog Text", - ButtonAlignment = (Alignment)Enum.Parse (typeof (Alignment), alignmentGroup.Labels! [(int)alignmentGroup.Value!.Value] [1..]), + ButtonAlignment = (Alignment)Enum.Parse (typeof (Alignment), alignmentGroup.Labels! [(int)alignmentGroup.Value!.Value] [0..]), Buttons = buttons.ToArray () }; diff --git a/Examples/UICatalog/Scenarios/DynamicStatusBar.cs b/Examples/UICatalog/Scenarios/DynamicStatusBar.cs index 73dd3b802..a0cdb48e3 100644 --- a/Examples/UICatalog/Scenarios/DynamicStatusBar.cs +++ b/Examples/UICatalog/Scenarios/DynamicStatusBar.cs @@ -79,7 +79,7 @@ public class DynamicStatusBar : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery ("Binding Error", $"Binding failed: {ex}.", "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Binding Error", $"Binding failed: {ex}.", "Ok"); } } } @@ -140,7 +140,7 @@ public class DynamicStatusBar : Scenario public TextView TextAction { get; } public TextField TextShortcut { get; } public TextField TextTitle { get; } - public Action CreateAction (DynamicStatusItem item) { return () => MessageBox.ErrorQuery (item.Title, item.Action, "Ok"); } + public Action CreateAction (DynamicStatusItem item) { return () => MessageBox.ErrorQuery (ApplicationImpl.Instance, item.Title, item.Action, "Ok"); } public void EditStatusItem (Shortcut statusItem) { @@ -184,7 +184,7 @@ public class DynamicStatusBar : Scenario { if (string.IsNullOrEmpty (TextTitle.Text)) { - MessageBox.ErrorQuery ("Invalid title", "Must enter a valid title!.", "Ok"); + MessageBox.ErrorQuery (App, "Invalid title", "Must enter a valid title!.", "Ok"); } else { @@ -200,7 +200,7 @@ public class DynamicStatusBar : Scenario TextTitle.Text = string.Empty; Application.RequestStop (); }; - var dialog = new Dialog { Title = "Enter the menu details.", Buttons = [btnOk, btnCancel], Height = Dim.Auto (DimAutoStyle.Content, 17, Application.Screen.Height) }; + var dialog = new Dialog { Title = "Enter the menu details.", Buttons = [btnOk, btnCancel], Height = Dim.Auto (DimAutoStyle.Content, 17, App?.Screen.Height) }; Width = Dim.Fill (); Height = Dim.Fill () - 2; @@ -382,7 +382,7 @@ public class DynamicStatusBar : Scenario { if (string.IsNullOrEmpty (frmStatusBarDetails.TextTitle.Text) && _currentEditStatusItem != null) { - MessageBox.ErrorQuery ("Invalid title", "Must enter a valid title!.", "Ok"); + MessageBox.ErrorQuery (App, "Invalid title", "Must enter a valid title!.", "Ok"); } else if (_currentEditStatusItem != null) { diff --git a/Examples/UICatalog/Scenarios/Editor.cs b/Examples/UICatalog/Scenarios/Editor.cs index 3b2e13813..d2eac26dc 100644 --- a/Examples/UICatalog/Scenarios/Editor.cs +++ b/Examples/UICatalog/Scenarios/Editor.cs @@ -156,7 +156,7 @@ public class Editor : Scenario new (Key.F2, "Open", Open), new (Key.F3, "Save", () => Save ()), new (Key.F4, "Save As", () => SaveAs ()), - new (Key.Empty, $"OS Clipboard IsSupported : {Clipboard.IsSupported}", null), + new (Key.Empty, $"OS Clipboard IsSupported : {Application.Clipboard!.IsSupported}", null), siCursorPosition ] ) @@ -193,7 +193,8 @@ public class Editor : Scenario Debug.Assert (_textView.IsDirty); - int r = MessageBox.ErrorQuery ( + int? r = MessageBox.ErrorQuery ( + ApplicationImpl.Instance, "Save File", $"Do you want save changes in {_appWindow.Title}?", "Yes", @@ -228,7 +229,7 @@ public class Editor : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery ("Error", ex.Message, "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Error", ex.Message, "Ok"); } } @@ -307,11 +308,11 @@ public class Editor : Scenario if (!found) { - MessageBox.Query ("Find", $"The following specified text was not found: '{_textToFind}'", "Ok"); + MessageBox.Query (ApplicationImpl.Instance, "Find", $"The following specified text was not found: '{_textToFind}'", "Ok"); } else if (gaveFullTurn) { - MessageBox.Query ( + MessageBox.Query (ApplicationImpl.Instance, "Find", $"No more occurrences were found for the following specified text: '{_textToFind}'", "Ok" @@ -887,7 +888,7 @@ public class Editor : Scenario if (_textView.ReplaceAllText (_textToFind, _matchCase, _matchWholeWord, _textToReplace)) { - MessageBox.Query ( + MessageBox.Query (ApplicationImpl.Instance, "Replace All", $"All occurrences were replaced for the following specified text: '{_textToReplace}'", "Ok" @@ -895,7 +896,7 @@ public class Editor : Scenario } else { - MessageBox.Query ( + MessageBox.Query (ApplicationImpl.Instance, "Replace All", $"None of the following specified text was found: '{_textToFind}'", "Ok" @@ -1147,7 +1148,7 @@ public class Editor : Scenario { if (File.Exists (path)) { - if (MessageBox.Query ( + if (MessageBox.Query (ApplicationImpl.Instance, "Save File", "File already exists. Overwrite any way?", "No", @@ -1186,11 +1187,11 @@ public class Editor : Scenario _originalText = Encoding.Unicode.GetBytes (_textView.Text); _saved = true; _textView.ClearHistoryChanges (); - MessageBox.Query ("Save File", "File was successfully saved.", "Ok"); + MessageBox.Query (ApplicationImpl.Instance, "Save File", "File was successfully saved.", "Ok"); } catch (Exception ex) { - MessageBox.ErrorQuery ("Error", ex.Message, "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Error", ex.Message, "Ok"); return false; } diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs index ff372ecc7..7f1f795c9 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs @@ -157,7 +157,7 @@ public class DimEditor : EditorBase } catch (Exception e) { - MessageBox.ErrorQuery ("Exception", e.Message, "Ok"); + MessageBox.ErrorQuery (App, "Exception", e.Message, "Ok"); } } } diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs index 45f0ab950..467b54756 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs @@ -160,7 +160,7 @@ public class PosEditor : EditorBase } catch (Exception e) { - MessageBox.ErrorQuery ("Exception", e.Message, "Ok"); + MessageBox.ErrorQuery (App, "Exception", e.Message, "Ok"); } } } diff --git a/Examples/UICatalog/Scenarios/FileDialogExamples.cs b/Examples/UICatalog/Scenarios/FileDialogExamples.cs index 290e4a432..fd80d82f3 100644 --- a/Examples/UICatalog/Scenarios/FileDialogExamples.cs +++ b/Examples/UICatalog/Scenarios/FileDialogExamples.cs @@ -133,7 +133,7 @@ public class FileDialogExamples : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery ("Error", ex.ToString (), "_Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Error", ex.ToString (), "_Ok"); } finally { @@ -153,7 +153,7 @@ public class FileDialogExamples : Scenario { if (File.Exists (e.Dialog.Path)) { - int result = MessageBox.Query ("Overwrite?", "File already exists", "_Yes", "_No"); + int? result = MessageBox.Query (ApplicationImpl.Instance, "Overwrite?", "File already exists", "_Yes", "_No"); e.Cancel = result == 1; } } @@ -248,7 +248,7 @@ public class FileDialogExamples : Scenario if (canceled) { - MessageBox.Query ( + MessageBox.Query (ApplicationImpl.Instance, "Canceled", "You canceled navigation and did not pick anything", "Ok" @@ -256,7 +256,7 @@ public class FileDialogExamples : Scenario } else if (_cbAllowMultipleSelection.CheckedState == CheckState.Checked) { - MessageBox.Query ( + MessageBox.Query (ApplicationImpl.Instance, "Chosen!", "You chose:" + Environment.NewLine + string.Join (Environment.NewLine, multiSelected.Select (m => m)), "Ok" @@ -264,7 +264,7 @@ public class FileDialogExamples : Scenario } else { - MessageBox.Query ( + MessageBox.Query (ApplicationImpl.Instance, "Chosen!", "You chose:" + Environment.NewLine + path, "Ok" diff --git a/Examples/UICatalog/Scenarios/Generic.cs b/Examples/UICatalog/Scenarios/Generic.cs index f0da0dd53..a8c3c7266 100644 --- a/Examples/UICatalog/Scenarios/Generic.cs +++ b/Examples/UICatalog/Scenarios/Generic.cs @@ -29,7 +29,7 @@ public sealed class Generic : Scenario { // When Accepting is handled, set e.Handled to true to prevent further processing. e.Handled = true; - MessageBox.ErrorQuery ("Error", "You pressed the button!", "_Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Error", "You pressed the button!", "_Ok"); }; appWindow.Add (button); diff --git a/Examples/UICatalog/Scenarios/HexEditor.cs b/Examples/UICatalog/Scenarios/HexEditor.cs index 45abe08ac..fdd4b5e83 100644 --- a/Examples/UICatalog/Scenarios/HexEditor.cs +++ b/Examples/UICatalog/Scenarios/HexEditor.cs @@ -181,7 +181,7 @@ public class HexEditor : Scenario } } - private void Copy () { MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "Ok"); } + private void Copy () { MessageBox.ErrorQuery (ApplicationImpl.Instance, "Not Implemented", "Functionality not yet implemented.", "Ok"); } private void CreateDemoFile (string fileName) { @@ -208,7 +208,7 @@ public class HexEditor : Scenario ms.Close (); } - private void Cut () { MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "Ok"); } + private void Cut () { MessageBox.ErrorQuery (ApplicationImpl.Instance, "Not Implemented", "Functionality not yet implemented.", "Ok"); } private Stream LoadFile () { @@ -216,7 +216,7 @@ public class HexEditor : Scenario if (!_saved && _hexView!.Edits.Count > 0 && _hexView.Source is {}) { - if (MessageBox.ErrorQuery ( + if (MessageBox.ErrorQuery (ApplicationImpl.Instance, "Save", "The changes were not saved. Want to open without saving?", "_Yes", @@ -267,7 +267,7 @@ public class HexEditor : Scenario d.Dispose (); } - private void Paste () { MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "_Ok"); } + private void Paste () { MessageBox.ErrorQuery (ApplicationImpl.Instance, "Not Implemented", "Functionality not yet implemented.", "_Ok"); } private void Quit () { Application.RequestStop (); } private void Save () diff --git a/Examples/UICatalog/Scenarios/Images.cs b/Examples/UICatalog/Scenarios/Images.cs index a9854092d..5791166cb 100644 --- a/Examples/UICatalog/Scenarios/Images.cs +++ b/Examples/UICatalog/Scenarios/Images.cs @@ -183,7 +183,7 @@ public class Images : Scenario if (!_sixelSupportResult.SupportsTransparency) { - if (MessageBox.Query ( + if (MessageBox.Query (ApplicationImpl.Instance, "Transparency Not Supported", "It looks like your terminal does not support transparent sixel backgrounds. Do you want to try anyway?", "Yes", @@ -288,7 +288,7 @@ public class Images : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery ("Could not open file", ex.Message, "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Could not open file", ex.Message, "Ok"); return; } @@ -492,7 +492,7 @@ public class Images : Scenario { if (_imageView.FullResImage == null) { - MessageBox.Query ("No Image Loaded", "You must first open an image. Use the 'Open Image' button above.", "Ok"); + MessageBox.Query (ApplicationImpl.Instance, "No Image Loaded", "You must first open an image. Use the 'Open Image' button above.", "Ok"); return; } diff --git a/Examples/UICatalog/Scenarios/InteractiveTree.cs b/Examples/UICatalog/Scenarios/InteractiveTree.cs index a91448c10..d90af1fa4 100644 --- a/Examples/UICatalog/Scenarios/InteractiveTree.cs +++ b/Examples/UICatalog/Scenarios/InteractiveTree.cs @@ -173,7 +173,7 @@ public class InteractiveTree : Scenario if (parent is null) { - MessageBox.ErrorQuery ( + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Could not delete", $"Parent of '{toDelete}' was unexpectedly null", "Ok" diff --git a/Examples/UICatalog/Scenarios/KeyBindings.cs b/Examples/UICatalog/Scenarios/KeyBindings.cs index f68e67f17..635aa6f6e 100644 --- a/Examples/UICatalog/Scenarios/KeyBindings.cs +++ b/Examples/UICatalog/Scenarios/KeyBindings.cs @@ -164,17 +164,17 @@ public class KeyBindingsDemo : View AddCommand (Command.Save, ctx => { - MessageBox.Query ($"{ctx.Command}", $"Ctx: {ctx}", buttons: "Ok"); + MessageBox.Query (ApplicationImpl.Instance, $"{ctx.Command}", $"Ctx: {ctx}", buttons: "Ok"); return true; }); AddCommand (Command.New, ctx => { - MessageBox.Query ($"{ctx.Command}", $"Ctx: {ctx}", buttons: "Ok"); + MessageBox.Query (ApplicationImpl.Instance, $"{ctx.Command}", $"Ctx: {ctx}", buttons: "Ok"); return true; }); AddCommand (Command.HotKey, ctx => { - MessageBox.Query ($"{ctx.Command}", $"Ctx: {ctx}\nCommand: {ctx.Command}", buttons: "Ok"); + MessageBox.Query (ApplicationImpl.Instance, $"{ctx.Command}", $"Ctx: {ctx}\nCommand: {ctx.Command}", buttons: "Ok"); SetFocus (); return true; }); @@ -189,7 +189,7 @@ public class KeyBindingsDemo : View { return false; } - MessageBox.Query ($"{keyCommandContext.Binding}", $"Key: {keyCommandContext.Binding.Key}\nCommand: {ctx.Command}", buttons: "Ok"); + MessageBox.Query (ApplicationImpl.Instance, $"{keyCommandContext.Binding}", $"Key: {keyCommandContext.Binding.Key}\nCommand: {ctx.Command}", buttons: "Ok"); Application.RequestStop (); return true; }); diff --git a/Examples/UICatalog/Scenarios/ListColumns.cs b/Examples/UICatalog/Scenarios/ListColumns.cs index 8ed35942e..d300b4163 100644 --- a/Examples/UICatalog/Scenarios/ListColumns.cs +++ b/Examples/UICatalog/Scenarios/ListColumns.cs @@ -336,7 +336,7 @@ public class ListColumns : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery (60, 20, "Failed to set", ex.Message, "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, 60, 20, "Failed to set", ex.Message, "Ok"); } } } diff --git a/Examples/UICatalog/Scenarios/MessageBoxes.cs b/Examples/UICatalog/Scenarios/MessageBoxes.cs index c8356a86a..fcb6488ee 100644 --- a/Examples/UICatalog/Scenarios/MessageBoxes.cs +++ b/Examples/UICatalog/Scenarios/MessageBoxes.cs @@ -251,7 +251,7 @@ public class MessageBoxes : Scenario { buttonPressedLabel.Text = $"{MessageBox.Query ( - width, + ApplicationImpl.Instance, width, height, titleEdit.Text, messageEdit.Text, @@ -263,14 +263,14 @@ public class MessageBoxes : Scenario else { buttonPressedLabel.Text = - $"{MessageBox.ErrorQuery ( - width, - height, - titleEdit.Text, - messageEdit.Text, - defaultButton, - ckbWrapMessage.CheckedState == CheckState.Checked, - btns.ToArray () + $"{MessageBox.ErrorQuery (ApplicationImpl.Instance, + width, + height, + titleEdit.Text, + messageEdit.Text, + defaultButton, + ckbWrapMessage.CheckedState == CheckState.Checked, + btns.ToArray () )}"; } } diff --git a/Examples/UICatalog/Scenarios/MultiColouredTable.cs b/Examples/UICatalog/Scenarios/MultiColouredTable.cs index 9b717d1ed..5bac4e125 100644 --- a/Examples/UICatalog/Scenarios/MultiColouredTable.cs +++ b/Examples/UICatalog/Scenarios/MultiColouredTable.cs @@ -99,7 +99,7 @@ public class MultiColouredTable : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery (60, 20, "Failed to set text", ex.Message, "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, 60, 20, "Failed to set text", ex.Message, "Ok"); } _tableView.Update (); diff --git a/Examples/UICatalog/Scenarios/Navigation.cs b/Examples/UICatalog/Scenarios/Navigation.cs index 94e7dad8e..dfedfc4d1 100644 --- a/Examples/UICatalog/Scenarios/Navigation.cs +++ b/Examples/UICatalog/Scenarios/Navigation.cs @@ -59,7 +59,7 @@ public class Navigation : Scenario Y = 0, Title = $"TopButton _{GetNextHotKey ()}" }; - button.Accepting += (sender, args) => MessageBox.Query ("hi", button.Title, "_Ok"); + button.Accepting += (sender, args) => MessageBox.Query (ApplicationImpl.Instance, "hi", button.Title, "_Ok"); testFrame.Add (button); @@ -210,7 +210,7 @@ public class Navigation : Scenario return; - void OnApplicationIteration (object sender, IterationEventArgs args) + void OnApplicationIteration (object sender, EventArgs args) { if (progressBar.Fraction == 1.0) { diff --git a/Examples/UICatalog/Scenarios/Notepad.cs b/Examples/UICatalog/Scenarios/Notepad.cs index 4a2f7d2e6..6d3ac2c82 100644 --- a/Examples/UICatalog/Scenarios/Notepad.cs +++ b/Examples/UICatalog/Scenarios/Notepad.cs @@ -71,7 +71,7 @@ public class Notepad : Scenario new MenuItem { Title = "_About", - Action = () => MessageBox.Query ("Notepad", "About Notepad...", "Ok") + Action = () => MessageBox.Query (ApplicationImpl.Instance, "Notepad", "About Notepad...", "Ok") } ] ) @@ -193,15 +193,15 @@ public class Notepad : Scenario if (tab.UnsavedChanges) { - int result = MessageBox.Query ( - "Save Changes", - $"Save changes to {tab.Text.TrimEnd ('*')}", - "Yes", - "No", - "Cancel" + int? result = MessageBox.Query (ApplicationImpl.Instance, + "Save Changes", + $"Save changes to {tab.Text.TrimEnd ('*')}", + "Yes", + "No", + "Cancel" ); - if (result == -1 || result == 2) + if (result is null || result == 2) { // user cancelled return; diff --git a/Examples/UICatalog/Scenarios/RunTExample.cs b/Examples/UICatalog/Scenarios/RunTExample.cs index 6e4cfa1d7..7a66e54e5 100644 --- a/Examples/UICatalog/Scenarios/RunTExample.cs +++ b/Examples/UICatalog/Scenarios/RunTExample.cs @@ -63,12 +63,12 @@ public class RunTExample : Scenario { if (_usernameText.Text == "admin" && passwordText.Text == "password") { - MessageBox.Query ("Login Successful", $"Username: {_usernameText.Text}", "Ok"); - Application.RequestStop (); + MessageBox.Query (App, "Login Successful", $"Username: {_usernameText.Text}", "Ok"); + App?.RequestStop (); } else { - MessageBox.ErrorQuery ( + MessageBox.ErrorQuery (App, "Error Logging In", "Incorrect username or password (hint: admin/password)", "Ok" diff --git a/Examples/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs b/Examples/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs index 512a106f0..23f2e63fa 100644 --- a/Examples/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs +++ b/Examples/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs @@ -166,7 +166,7 @@ public class RuneWidthGreaterThanOne : Scenario { if (_text is { }) { - MessageBox.Query ("Say Hello 你", $"Hello {_text.Text}", "Ok"); + MessageBox.Query (ApplicationImpl.Instance, "Say Hello 你", $"Hello {_text.Text}", "Ok"); } } @@ -197,7 +197,7 @@ public class RuneWidthGreaterThanOne : Scenario { if (_text is { }) { - MessageBox.Query ("Say Hello", $"Hello {_text.Text}", "Ok"); + MessageBox.Query (ApplicationImpl.Instance, "Say Hello", $"Hello {_text.Text}", "Ok"); } } @@ -252,7 +252,7 @@ public class RuneWidthGreaterThanOne : Scenario { if (_text is { }) { - MessageBox.Query ("こんにちはと言う", $"こんにちは {_text.Text}", "Ok"); + MessageBox.Query (ApplicationImpl.Instance, "こんにちはと言う", $"こんにちは {_text.Text}", "Ok"); } } diff --git a/Examples/UICatalog/Scenarios/Shortcuts.cs b/Examples/UICatalog/Scenarios/Shortcuts.cs index 573834aa3..7af36ef92 100644 --- a/Examples/UICatalog/Scenarios/Shortcuts.cs +++ b/Examples/UICatalog/Scenarios/Shortcuts.cs @@ -566,6 +566,6 @@ public class Shortcuts : Scenario { e.Handled = true; var view = sender as View; - MessageBox.Query ("Hi", $"You clicked {view?.Text}", "_Ok"); + MessageBox.Query ((sender as View)?.App, "Hi", $"You clicked {view?.Text}", "_Ok"); } } diff --git a/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs b/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs index 5e795a9c0..e5b8c301f 100644 --- a/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs +++ b/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs @@ -224,7 +224,7 @@ public class SingleBackgroundWorker : Scenario bool Close () { - int n = MessageBox.Query ( + int? n = MessageBox.Query (App, 50, 7, "Close Window.", @@ -251,7 +251,7 @@ public class SingleBackgroundWorker : Scenario { if (Close ()) { - Application.RequestStop (); + App?.RequestStop (); } } } @@ -270,7 +270,7 @@ public class SingleBackgroundWorker : Scenario { if (Close ()) { - Application.RequestStop (); + App?.RequestStop (); } } ) @@ -304,7 +304,7 @@ public class SingleBackgroundWorker : Scenario { if (_top is { }) { - Application.Run (_top); + App?.Run (_top); _top.Dispose (); _top = null; } diff --git a/Examples/UICatalog/Scenarios/TableEditor.cs b/Examples/UICatalog/Scenarios/TableEditor.cs index 12ab5e9d8..7d130da78 100644 --- a/Examples/UICatalog/Scenarios/TableEditor.cs +++ b/Examples/UICatalog/Scenarios/TableEditor.cs @@ -1026,7 +1026,7 @@ public class TableEditor : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery (60, 20, "Failed to set text", ex.Message, "Ok"); + MessageBox.ErrorQuery ((sender as View)?.App, 60, 20, "Failed to set text", ex.Message, "Ok"); } _tableView!.Update (); @@ -1165,7 +1165,7 @@ public class TableEditor : Scenario } catch (Exception e) { - MessageBox.ErrorQuery ("Could not find local drives", e.Message, "Ok"); + MessageBox.ErrorQuery (_tableView?.App, "Could not find local drives", e.Message, "Ok"); } _tableView!.Table = source; @@ -1199,10 +1199,10 @@ public class TableEditor : Scenario ok.Accepting += (s, e) => { accepted = true; - Application.RequestStop (); + (s as View)?.App?.RequestStop (); }; var cancel = new Button { Text = "Cancel" }; - cancel.Accepting += (s, e) => { Application.RequestStop (); }; + cancel.Accepting += (s, e) => { (s as View)?.App?.RequestStop (); }; var d = new Dialog { @@ -1218,7 +1218,7 @@ public class TableEditor : Scenario d.Add (lbl, tf); tf.SetFocus (); - Application.Run (d); + _tableView.App?.Run (d); d.Dispose (); if (accepted) @@ -1229,7 +1229,7 @@ public class TableEditor : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery (60, 20, "Failed to set", ex.Message, "Ok"); + MessageBox.ErrorQuery (_tableView.App, 60, 20, "Failed to set", ex.Message, "Ok"); } _tableView!.Update (); @@ -1512,7 +1512,7 @@ public class TableEditor : Scenario _checkedFileSystemInfos!.Contains, CheckOrUncheckFile ) - { UseRadioButtons = radio }; + { UseRadioButtons = radio }; } else { diff --git a/Examples/UICatalog/Scenarios/Transparent.cs b/Examples/UICatalog/Scenarios/Transparent.cs index 1b6f08df8..801372d2c 100644 --- a/Examples/UICatalog/Scenarios/Transparent.cs +++ b/Examples/UICatalog/Scenarios/Transparent.cs @@ -46,7 +46,7 @@ public sealed class Transparent : Scenario }; appButton.Accepting += (sender, args) => { - MessageBox.Query ("AppButton", "Transparency is cool!", "_Ok"); + MessageBox.Query ((sender as View)?.App, "AppButton", "Transparency is cool!", "_Ok"); args.Handled = true; }; appWindow.Add (appButton); @@ -106,7 +106,7 @@ public sealed class Transparent : Scenario }; button.Accepting += (sender, args) => { - MessageBox.Query ("Clicked!", "Button in Transparent View", "_Ok"); + MessageBox.Query (App, "Clicked!", "Button in Transparent View", "_Ok"); args.Handled = true; }; //button.Visible = false; diff --git a/Examples/UICatalog/Scenarios/ViewportSettings.cs b/Examples/UICatalog/Scenarios/ViewportSettings.cs index 4e430171c..e687a078f 100644 --- a/Examples/UICatalog/Scenarios/ViewportSettings.cs +++ b/Examples/UICatalog/Scenarios/ViewportSettings.cs @@ -169,13 +169,13 @@ public class ViewportSettings : Scenario }; charMap.Accepting += (s, e) => - MessageBox.Query (20, 7, "Hi", $"Am I a {view.GetType ().Name}?", "Yes", "No"); + MessageBox.Query ((s as View)?.App, 20, 7, "Hi", $"Am I a {view.GetType ().Name}?", "Yes", "No"); var buttonAnchored = new Button { X = Pos.AnchorEnd () - 10, Y = Pos.AnchorEnd () - 4, Text = "Bottom Rig_ht" }; - buttonAnchored.Accepting += (sender, args) => MessageBox.Query ("Hi", $"You pressed {((Button)sender)?.Text}", "_Ok"); + buttonAnchored.Accepting += (sender, args) => MessageBox.Query ((sender as View)?.App, "Hi", $"You pressed {((Button)sender)?.Text}", "_Ok"); view.Margin!.Data = "Margin"; view.Margin!.Thickness = new (0); diff --git a/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs b/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs index d2b58d847..4404f8008 100644 --- a/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs +++ b/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs @@ -16,9 +16,9 @@ public class WindowsAndFrameViews : Scenario Title = GetQuitKeyAndName () }; - static int About () + static int? About () { - return MessageBox.Query ( + return MessageBox.Query (ApplicationImpl.Instance, "About UI Catalog", "UI Catalog is a comprehensive sample library for Terminal.Gui", "Ok" @@ -99,7 +99,7 @@ public class WindowsAndFrameViews : Scenario }; pressMeButton.Accepting += (s, e) => - MessageBox.ErrorQuery (loopWin.Title, "Neat?", "Yes", "No"); + MessageBox.ErrorQuery ((s as View)?.App, loopWin.Title, "Neat?", "Yes", "No"); loopWin.Add (pressMeButton); var subWin = new Window diff --git a/Examples/UICatalog/Scenarios/WizardAsView.cs b/Examples/UICatalog/Scenarios/WizardAsView.cs index 9df72b835..67e23685f 100644 --- a/Examples/UICatalog/Scenarios/WizardAsView.cs +++ b/Examples/UICatalog/Scenarios/WizardAsView.cs @@ -21,6 +21,7 @@ public class WizardAsView : Scenario { Title = "_Restart Configuration...", Action = () => MessageBox.Query ( + ApplicationImpl.Instance, "Wizard", "Are you sure you want to reset the Wizard and start over?", "Ok", @@ -31,6 +32,7 @@ public class WizardAsView : Scenario { Title = "Re_boot Server...", Action = () => MessageBox.Query ( + ApplicationImpl.Instance, "Wizard", "Are you sure you want to reboot the server start over?", "Ok", @@ -41,6 +43,7 @@ public class WizardAsView : Scenario { Title = "_Shutdown Server...", Action = () => MessageBox.Query ( + ApplicationImpl.Instance, "Wizard", "Are you sure you want to cancel setup and shutdown?", "Ok", @@ -80,13 +83,13 @@ public class WizardAsView : Scenario wizard.Finished += (s, args) => { //args.Cancel = true; - MessageBox.Query ("Setup Wizard", "Finished", "Ok"); + MessageBox.Query ((s as View)?.App, "Setup Wizard", "Finished", "Ok"); Application.RequestStop (); }; wizard.Cancelled += (s, args) => { - int btn = MessageBox.Query ("Setup Wizard", "Are you sure you want to cancel?", "Yes", "No"); + int? btn = MessageBox.Query ((s as View)?.App, "Setup Wizard", "Are you sure you want to cancel?", "Yes", "No"); args.Cancel = btn == 1; if (btn == 0) @@ -123,7 +126,7 @@ public class WizardAsView : Scenario { secondStep.Title = "2nd Step"; - MessageBox.Query ( + MessageBox.Query ((s as View)?.App, "Wizard Scenario", "This Wizard Step's title was changed to '2nd Step'", "Ok" diff --git a/Examples/UICatalog/Scenarios/Wizards.cs b/Examples/UICatalog/Scenarios/Wizards.cs index 6263093a4..2f90c3420 100644 --- a/Examples/UICatalog/Scenarios/Wizards.cs +++ b/Examples/UICatalog/Scenarios/Wizards.cs @@ -1,13 +1,9 @@ -using System; -using System.Linq; - -namespace UICatalog.Scenarios; +namespace UICatalog.Scenarios; [ScenarioMetadata ("Wizards", "Demonstrates the Wizard class")] [ScenarioCategory ("Dialogs")] [ScenarioCategory ("Wizards")] [ScenarioCategory ("Runnable")] - public class Wizards : Scenario { public override void Main () @@ -108,267 +104,277 @@ public class Wizards : Scenario }; showWizardButton.Accepting += (s, e) => - { - try - { - var width = 0; - int.TryParse (widthEdit.Text, out width); - var height = 0; - int.TryParse (heightEdit.Text, out height); + { + try + { + var width = 0; + int.TryParse (widthEdit.Text, out width); + var height = 0; + int.TryParse (heightEdit.Text, out height); - if (width < 1 || height < 1) - { - MessageBox.ErrorQuery ( - "Nope", - "Height and width must be greater than 0 (much bigger)", - "Ok" - ); + if (width < 1 || height < 1) + { + MessageBox.ErrorQuery ( + (s as View)?.App, + "Nope", + "Height and width must be greater than 0 (much bigger)", + "Ok" + ); - return; - } + return; + } - actionLabel.Text = string.Empty; + actionLabel.Text = string.Empty; - var wizard = new Wizard { Title = titleEdit.Text, Width = width, Height = height }; + var wizard = new Wizard { Title = titleEdit.Text, Width = width, Height = height }; - wizard.MovingBack += (s, args) => - { - //args.Cancel = true; - actionLabel.Text = "Moving Back"; - }; + wizard.MovingBack += (s, args) => + { + //args.Cancel = true; + actionLabel.Text = "Moving Back"; + }; - wizard.MovingNext += (s, args) => - { - //args.Cancel = true; - actionLabel.Text = "Moving Next"; - }; + wizard.MovingNext += (s, args) => + { + //args.Cancel = true; + actionLabel.Text = "Moving Next"; + }; - wizard.Finished += (s, args) => - { - //args.Cancel = true; - actionLabel.Text = "Finished"; - }; + wizard.Finished += (s, args) => + { + //args.Cancel = true; + actionLabel.Text = "Finished"; + }; - wizard.Cancelled += (s, args) => - { - //args.Cancel = true; - actionLabel.Text = "Cancelled"; - }; - - // Add 1st step - var firstStep = new WizardStep { Title = "End User License Agreement" }; - firstStep.NextButtonText = "Accept!"; - - firstStep.HelpText = - "This is the End User License Agreement.\n\n\n\n\n\nThis is a test of the emergency broadcast system. This is a test of the emergency broadcast system.\nThis is a test of the emergency broadcast system.\n\n\nThis is a test of the emergency broadcast system.\n\nThis is a test of the emergency broadcast system.\n\n\n\nThe end of the EULA."; - - OptionSelector optionSelector = new () - { - Labels = ["_One", "_Two", "_3"] - }; - firstStep.Add (optionSelector); - - wizard.AddStep (firstStep); - - // Add 2nd step - var secondStep = new WizardStep { Title = "Second Step" }; - wizard.AddStep (secondStep); - - secondStep.HelpText = - "This is the help text for the Second Step.\n\nPress the button to change the Title.\n\nIf First Name is empty the step will prevent moving to the next step."; - - var buttonLbl = new Label { Text = "Second Step Button: ", X = 1, Y = 1 }; - - var button = new Button - { - Text = "Press Me to Rename Step", X = Pos.Right (buttonLbl), Y = Pos.Top (buttonLbl) - }; - - OptionSelector optionSelecor2 = new () - { - Labels = ["_A", "_B", "_C"], - Orientation = Orientation.Horizontal - }; - secondStep.Add (optionSelecor2); - - button.Accepting += (s, e) => - { - secondStep.Title = "2nd Step"; - - MessageBox.Query ( - "Wizard Scenario", - "This Wizard Step's title was changed to '2nd Step'" - ); - }; - secondStep.Add (buttonLbl, button); - var lbl = new Label { Text = "First Name: ", X = 1, Y = Pos.Bottom (buttonLbl) }; - - var firstNameField = - new TextField { Text = "Number", Width = 30, X = Pos.Right (lbl), Y = Pos.Top (lbl) }; - secondStep.Add (lbl, firstNameField); - lbl = new () { Text = "Last Name: ", X = 1, Y = Pos.Bottom (lbl) }; - var lastNameField = new TextField { Text = "Six", Width = 30, X = Pos.Right (lbl), Y = Pos.Top (lbl) }; - secondStep.Add (lbl, lastNameField); - - var thirdStepEnabledCeckBox = new CheckBox - { - Text = "Enable Step _3", - CheckedState = CheckState.UnChecked, - X = Pos.Left (lastNameField), - Y = Pos.Bottom (lastNameField) - }; - secondStep.Add (thirdStepEnabledCeckBox); - - // Add a frame - var frame = new FrameView - { - X = 0, - Y = Pos.Bottom (thirdStepEnabledCeckBox) + 2, - Width = Dim.Fill (), - Height = 4, - Title = "A Broken Frame (by Depeche Mode)", - TabStop = TabBehavior.NoStop - }; - frame.Add (new TextField { Text = "This is a TextField inside of the frame." }); - secondStep.Add (frame); - - wizard.StepChanging += (s, args) => + wizard.Cancelled += (s, args) => { - if (args.OldStep == secondStep && string.IsNullOrEmpty (firstNameField.Text)) - { - args.Cancel = true; - - int btn = MessageBox.ErrorQuery ( - "Second Step", - "You must enter a First Name to continue", - "Ok" - ); - } + //args.Cancel = true; + actionLabel.Text = "Cancelled"; }; - // Add 3rd (optional) step - var thirdStep = new WizardStep { Title = "Third Step (Optional)" }; - wizard.AddStep (thirdStep); + // Add 1st step + var firstStep = new WizardStep { Title = "End User License Agreement" }; + firstStep.NextButtonText = "Accept!"; - thirdStep.HelpText = - "This is step is optional (WizardStep.Enabled = false). Enable it with the checkbox in Step 2."; - var step3Label = new Label { Text = "This step is optional.", X = 0, Y = 0 }; - thirdStep.Add (step3Label); - var progLbl = new Label { Text = "Third Step ProgressBar: ", X = 1, Y = 10 }; + firstStep.HelpText = + "This is the End User License Agreement.\n\n\n\n\n\nThis is a test of the emergency broadcast system. This is a test of the emergency broadcast system.\nThis is a test of the emergency broadcast system.\n\n\nThis is a test of the emergency broadcast system.\n\nThis is a test of the emergency broadcast system.\n\n\n\nThe end of the EULA."; - var progressBar = new ProgressBar - { - X = Pos.Right (progLbl), Y = Pos.Top (progLbl), Width = 40, Fraction = 0.42F - }; - thirdStep.Add (progLbl, progressBar); - thirdStep.Enabled = thirdStepEnabledCeckBox.CheckedState == CheckState.Checked; - thirdStepEnabledCeckBox.CheckedStateChanged += (s, e) => { thirdStep.Enabled = thirdStepEnabledCeckBox.CheckedState == CheckState.Checked; }; + OptionSelector optionSelector = new () + { + Labels = ["_One", "_Two", "_3"] + }; + firstStep.Add (optionSelector); - // Add 4th step - var fourthStep = new WizardStep { Title = "Step Four" }; - wizard.AddStep (fourthStep); + wizard.AddStep (firstStep); - var someText = new TextView - { - Text = - "This step (Step Four) shows how to show/hide the Help pane. The step contains this TextView (but it's hard to tell it's a TextView because of Issue #1800).", - X = 0, - Y = 0, - Width = Dim.Fill (), - WordWrap = true, - AllowsTab = false, - SchemeName = "Base" - }; + // Add 2nd step + var secondStep = new WizardStep { Title = "Second Step" }; + wizard.AddStep (secondStep); - someText.Height = Dim.Fill ( - Dim.Func ( - v => someText.SuperView is { IsInitialized: true } - ? someText.SuperView.SubViews - .First (view => view.Y.Has (out _)) - .Frame.Height - : 1)); - var help = "This is helpful."; - fourthStep.Add (someText); + secondStep.HelpText = + "This is the help text for the Second Step.\n\nPress the button to change the Title.\n\nIf First Name is empty the step will prevent moving to the next step."; - var hideHelpBtn = new Button - { - Text = "Press me to show/hide help", - X = Pos.Center (), - Y = Pos.AnchorEnd () - }; + var buttonLbl = new Label { Text = "Second Step Button: ", X = 1, Y = 1 }; - hideHelpBtn.Accepting += (s, e) => - { - if (fourthStep.HelpText.Length > 0) + var button = new Button + { + Text = "Press Me to Rename Step", X = Pos.Right (buttonLbl), Y = Pos.Top (buttonLbl) + }; + + OptionSelector optionSelecor2 = new () + { + Labels = ["_A", "_B", "_C"], + Orientation = Orientation.Horizontal + }; + secondStep.Add (optionSelecor2); + + button.Accepting += (s, e) => + { + secondStep.Title = "2nd Step"; + + MessageBox.Query ( + (s as View)?.App, + "Wizard Scenario", + "This Wizard Step's title was changed to '2nd Step'" + ); + }; + secondStep.Add (buttonLbl, button); + var lbl = new Label { Text = "First Name: ", X = 1, Y = Pos.Bottom (buttonLbl) }; + + var firstNameField = + new TextField { Text = "Number", Width = 30, X = Pos.Right (lbl), Y = Pos.Top (lbl) }; + secondStep.Add (lbl, firstNameField); + lbl = new () { Text = "Last Name: ", X = 1, Y = Pos.Bottom (lbl) }; + var lastNameField = new TextField { Text = "Six", Width = 30, X = Pos.Right (lbl), Y = Pos.Top (lbl) }; + secondStep.Add (lbl, lastNameField); + + var thirdStepEnabledCeckBox = new CheckBox + { + Text = "Enable Step _3", + CheckedState = CheckState.UnChecked, + X = Pos.Left (lastNameField), + Y = Pos.Bottom (lastNameField) + }; + secondStep.Add (thirdStepEnabledCeckBox); + + // Add a frame + var frame = new FrameView + { + X = 0, + Y = Pos.Bottom (thirdStepEnabledCeckBox) + 2, + Width = Dim.Fill (), + Height = 4, + Title = "A Broken Frame (by Depeche Mode)", + TabStop = TabBehavior.NoStop + }; + frame.Add (new TextField { Text = "This is a TextField inside of the frame." }); + secondStep.Add (frame); + + wizard.StepChanging += (s, args) => { - fourthStep.HelpText = string.Empty; - } - else - { - fourthStep.HelpText = help; - } - }; - fourthStep.Add (hideHelpBtn); - fourthStep.NextButtonText = "_Go To Last Step"; - //var scrollBar = new ScrollBarView (someText, true); + if (args.OldStep == secondStep && string.IsNullOrEmpty (firstNameField.Text)) + { + args.Cancel = true; - //scrollBar.ChangedPosition += (s, e) => - // { - // someText.TopRow = scrollBar.Position; + int? btn = MessageBox.ErrorQuery ( + (s as View)?.App, + "Second Step", + "You must enter a First Name to continue", + "Ok" + ); + } + }; - // if (someText.TopRow != scrollBar.Position) - // { - // scrollBar.Position = someText.TopRow; - // } + // Add 3rd (optional) step + var thirdStep = new WizardStep { Title = "Third Step (Optional)" }; + wizard.AddStep (thirdStep); - // someText.SetNeedsDraw (); - // }; + thirdStep.HelpText = + "This is step is optional (WizardStep.Enabled = false). Enable it with the checkbox in Step 2."; + var step3Label = new Label { Text = "This step is optional.", X = 0, Y = 0 }; + thirdStep.Add (step3Label); + var progLbl = new Label { Text = "Third Step ProgressBar: ", X = 1, Y = 10 }; - //someText.DrawingContent += (s, e) => - // { - // scrollBar.Size = someText.Lines; - // scrollBar.Position = someText.TopRow; + var progressBar = new ProgressBar + { + X = Pos.Right (progLbl), Y = Pos.Top (progLbl), Width = 40, Fraction = 0.42F + }; + thirdStep.Add (progLbl, progressBar); + thirdStep.Enabled = thirdStepEnabledCeckBox.CheckedState == CheckState.Checked; - // if (scrollBar.OtherScrollBarView != null) - // { - // scrollBar.OtherScrollBarView.Size = someText.Maxlength; - // scrollBar.OtherScrollBarView.Position = someText.LeftColumn; - // } - // }; - //fourthStep.Add (scrollBar); + thirdStepEnabledCeckBox.CheckedStateChanged += (s, e) => + { + thirdStep.Enabled = + thirdStepEnabledCeckBox.CheckedState == CheckState.Checked; + }; - // Add last step - var lastStep = new WizardStep { Title = "The last step" }; - wizard.AddStep (lastStep); + // Add 4th step + var fourthStep = new WizardStep { Title = "Step Four" }; + wizard.AddStep (fourthStep); - lastStep.HelpText = - "The wizard is complete!\n\nPress the Finish button to continue.\n\nPressing ESC will cancel the wizard."; + var someText = new TextView + { + Text = + "This step (Step Four) shows how to show/hide the Help pane. The step contains this TextView (but it's hard to tell it's a TextView because of Issue #1800).", + X = 0, + Y = 0, + Width = Dim.Fill (), + WordWrap = true, + AllowsTab = false, + SchemeName = "Base" + }; - var finalFinalStepEnabledCeckBox = - new CheckBox { Text = "Enable _Final Final Step", CheckedState = CheckState.UnChecked, X = 0, Y = 1 }; - lastStep.Add (finalFinalStepEnabledCeckBox); + someText.Height = Dim.Fill ( + Dim.Func (v => someText.SuperView is { IsInitialized: true } + ? someText.SuperView.SubViews + .First (view => view.Y.Has (out _)) + .Frame.Height + : 1)); + var help = "This is helpful."; + fourthStep.Add (someText); - // Add an optional FINAL last step - var finalFinalStep = new WizardStep { Title = "The VERY last step" }; - wizard.AddStep (finalFinalStep); + var hideHelpBtn = new Button + { + Text = "Press me to show/hide help", + X = Pos.Center (), + Y = Pos.AnchorEnd () + }; - finalFinalStep.HelpText = - "This step only shows if it was enabled on the other last step."; - finalFinalStep.Enabled = thirdStepEnabledCeckBox.CheckedState == CheckState.Checked; + hideHelpBtn.Accepting += (s, e) => + { + if (fourthStep.HelpText.Length > 0) + { + fourthStep.HelpText = string.Empty; + } + else + { + fourthStep.HelpText = help; + } + }; + fourthStep.Add (hideHelpBtn); + fourthStep.NextButtonText = "_Go To Last Step"; - finalFinalStepEnabledCeckBox.CheckedStateChanged += (s, e) => - { - finalFinalStep.Enabled = finalFinalStepEnabledCeckBox.CheckedState == CheckState.Checked; - }; + //var scrollBar = new ScrollBarView (someText, true); - Application.Run (wizard); - wizard.Dispose (); - } - catch (FormatException) - { - actionLabel.Text = "Invalid Options"; - } - }; + //scrollBar.ChangedPosition += (s, e) => + // { + // someText.TopRow = scrollBar.Position; + + // if (someText.TopRow != scrollBar.Position) + // { + // scrollBar.Position = someText.TopRow; + // } + + // someText.SetNeedsDraw (); + // }; + + //someText.DrawingContent += (s, e) => + // { + // scrollBar.Size = someText.Lines; + // scrollBar.Position = someText.TopRow; + + // if (scrollBar.OtherScrollBarView != null) + // { + // scrollBar.OtherScrollBarView.Size = someText.Maxlength; + // scrollBar.OtherScrollBarView.Position = someText.LeftColumn; + // } + // }; + //fourthStep.Add (scrollBar); + + // Add last step + var lastStep = new WizardStep { Title = "The last step" }; + wizard.AddStep (lastStep); + + lastStep.HelpText = + "The wizard is complete!\n\nPress the Finish button to continue.\n\nPressing ESC will cancel the wizard."; + + var finalFinalStepEnabledCeckBox = + new CheckBox { Text = "Enable _Final Final Step", CheckedState = CheckState.UnChecked, X = 0, Y = 1 }; + lastStep.Add (finalFinalStepEnabledCeckBox); + + // Add an optional FINAL last step + var finalFinalStep = new WizardStep { Title = "The VERY last step" }; + wizard.AddStep (finalFinalStep); + + finalFinalStep.HelpText = + "This step only shows if it was enabled on the other last step."; + finalFinalStep.Enabled = thirdStepEnabledCeckBox.CheckedState == CheckState.Checked; + + finalFinalStepEnabledCeckBox.CheckedStateChanged += (s, e) => + { + finalFinalStep.Enabled = + finalFinalStepEnabledCeckBox.CheckedState + == CheckState.Checked; + }; + + Application.Run (wizard); + wizard.Dispose (); + } + catch (FormatException) + { + actionLabel.Text = "Invalid Options"; + } + }; win.Add (showWizardButton); Application.Run (win); @@ -376,8 +382,5 @@ public class Wizards : Scenario Application.Shutdown (); } - private void Wizard_StepChanged (object sender, StepChangeEventArgs e) - { - throw new NotImplementedException (); - } + private void Wizard_StepChanged (object sender, StepChangeEventArgs e) { throw new NotImplementedException (); } } diff --git a/Examples/UICatalog/UICatalogTop.cs b/Examples/UICatalog/UICatalogTop.cs index d02a73919..0b46293bc 100644 --- a/Examples/UICatalog/UICatalogTop.cs +++ b/Examples/UICatalog/UICatalogTop.cs @@ -144,6 +144,7 @@ public class UICatalogTop : Toplevel "_About...", "About UI Catalog", () => MessageBox.Query ( + App, "", GetAboutBoxMessage (), wrapMessage: false, diff --git a/Terminal.Gui/App/Application.Clipboard.cs b/Terminal.Gui/App/Application.Clipboard.cs new file mode 100644 index 000000000..22cc85907 --- /dev/null +++ b/Terminal.Gui/App/Application.Clipboard.cs @@ -0,0 +1,15 @@ +namespace Terminal.Gui.App; + +public static partial class Application // Clipboard handling +{ + /// + /// Gets the clipboard for the application. + /// + /// + /// + /// Provides access to the OS clipboard through the driver. + /// + /// + [Obsolete ("The legacy static Application object is going away. Use IApplication.Clipboard instead.")] + public static IClipboard? Clipboard => ApplicationImpl.Instance.Clipboard; +} diff --git a/Terminal.Gui/App/Application.Driver.cs b/Terminal.Gui/App/Application.Driver.cs index 635ff854b..427ba4de5 100644 --- a/Terminal.Gui/App/Application.Driver.cs +++ b/Terminal.Gui/App/Application.Driver.cs @@ -13,38 +13,44 @@ public static partial class Application // Driver abstractions internal set => ApplicationImpl.Instance.Driver = value; } + private static bool _force16Colors = false; // Resources/config.json overrides + /// [ConfigurationProperty (Scope = typeof (SettingsScope))] [Obsolete ("The legacy static Application object is going away.")] public static bool Force16Colors { - get => ApplicationImpl.Instance.Force16Colors; - set => ApplicationImpl.Instance.Force16Colors = value; + get => _force16Colors; + set + { + bool oldValue = _force16Colors; + _force16Colors = value; + Force16ColorsChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _force16Colors)); + } } + /// Raised when changes. + public static event EventHandler>? Force16ColorsChanged; + + private static string _forceDriver = string.Empty; // Resources/config.json overrides + /// [ConfigurationProperty (Scope = typeof (SettingsScope))] [Obsolete ("The legacy static Application object is going away.")] public static string ForceDriver { - get => ApplicationImpl.Instance.ForceDriver; + get => _forceDriver; set { - if (!string.IsNullOrEmpty (ApplicationImpl.Instance.ForceDriver) && value != Driver?.GetName ()) - { - // ForceDriver cannot be changed if it has a valid value - return; - } - - if (ApplicationImpl.Instance.Initialized && value != Driver?.GetName ()) - { - throw new InvalidOperationException ($"The {nameof (ForceDriver)} can only be set before initialized."); - } - - ApplicationImpl.Instance.ForceDriver = value; + string oldValue = _forceDriver; + _forceDriver = value; + ForceDriverChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _forceDriver)); } } + /// Raised when changes. + public static event EventHandler>? ForceDriverChanged; + /// [Obsolete ("The legacy static Application object is going away.")] public static List Sixel => ApplicationImpl.Instance.Sixel; diff --git a/Terminal.Gui/App/Application.Lifecycle.cs b/Terminal.Gui/App/Application.Lifecycle.cs index 802ba7a53..c3c3cdf16 100644 --- a/Terminal.Gui/App/Application.Lifecycle.cs +++ b/Terminal.Gui/App/Application.Lifecycle.cs @@ -18,7 +18,15 @@ public static partial class Application // Lifecycle (Init/Shutdown) /// instance for all subsequent application operations. /// /// A new instance. - public static IApplication Create () { return new ApplicationImpl (); } + /// + /// Thrown if the legacy static Application model has already been used in this process. + /// + public static IApplication Create () + { + ApplicationImpl.MarkInstanceBasedModelUsed (); + + return new ApplicationImpl (); + } /// [RequiresUnreferencedCode ("AOT")] @@ -26,6 +34,7 @@ public static partial class Application // Lifecycle (Init/Shutdown) [Obsolete ("The legacy static Application object is going away.")] public static void Init (string? driverName = null) { + //Debug.Fail ("Application.Init() called - parallelizable tests should not use legacy static Application model"); ApplicationImpl.Instance.Init (driverName ?? ForceDriver); } @@ -35,8 +44,8 @@ public static partial class Application // Lifecycle (Init/Shutdown) [Obsolete ("The legacy static Application object is going away.")] public static int? MainThreadId { - get => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId; - set => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId = value; + get => ApplicationImpl.Instance.MainThreadId; + internal set => ApplicationImpl.Instance.MainThreadId = value; } /// @@ -65,5 +74,9 @@ public static partial class Application // Lifecycle (Init/Shutdown) // guaranteeing that the state of this singleton is deterministic when Init // starts running and after Shutdown returns. [Obsolete ("The legacy static Application object is going away.")] - internal static void ResetState (bool ignoreDisposed = false) => ApplicationImpl.Instance?.ResetState (ignoreDisposed); + internal static void ResetState (bool ignoreDisposed = false) + { + // Use the static reset method to bypass the fence check + ApplicationImpl.ResetStateStatic (ignoreDisposed); + } } diff --git a/Terminal.Gui/App/Application.Mouse.cs b/Terminal.Gui/App/Application.Mouse.cs index 5e6ec118f..2ea9bb650 100644 --- a/Terminal.Gui/App/Application.Mouse.cs +++ b/Terminal.Gui/App/Application.Mouse.cs @@ -4,15 +4,25 @@ namespace Terminal.Gui.App; public static partial class Application // Mouse handling { + private static bool _isMouseDisabled = false; // Resources/config.json overrides + /// Disable or enable the mouse. The mouse is enabled by default. [ConfigurationProperty (Scope = typeof (SettingsScope))] [Obsolete ("The legacy static Application object is going away.")] public static bool IsMouseDisabled { - get => Mouse.IsMouseDisabled; - set => Mouse.IsMouseDisabled = value; + get => _isMouseDisabled; + set + { + bool oldValue = _isMouseDisabled; + _isMouseDisabled = value; + IsMouseDisabledChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _isMouseDisabled)); + } } + /// Raised when changes. + public static event EventHandler>? IsMouseDisabledChanged; + /// /// Gets the instance that manages mouse event handling and state. /// diff --git a/Terminal.Gui/App/Application.Navigation.cs b/Terminal.Gui/App/Application.Navigation.cs index b1053ae42..031ebac1c 100644 --- a/Terminal.Gui/App/Application.Navigation.cs +++ b/Terminal.Gui/App/Application.Navigation.cs @@ -13,22 +13,42 @@ public static partial class Application // Navigation stuff internal set => ApplicationImpl.Instance.Navigation = value; } + private static Key _nextTabGroupKey = Key.F6; // Resources/config.json overrides + /// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key. [ConfigurationProperty (Scope = typeof (SettingsScope))] - [Obsolete ("The legacy static Application object is going away.")]public static Key NextTabGroupKey + public static Key NextTabGroupKey { - get => ApplicationImpl.Instance.Keyboard.NextTabGroupKey; - set => ApplicationImpl.Instance.Keyboard.NextTabGroupKey = value; + get => _nextTabGroupKey; + set + { + Key oldValue = _nextTabGroupKey; + _nextTabGroupKey = value; + NextTabGroupKeyChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _nextTabGroupKey)); + } } + /// Raised when changes. + public static event EventHandler>? NextTabGroupKeyChanged; + + private static Key _nextTabKey = Key.Tab; // Resources/config.json overrides + /// Alternative key to navigate forwards through views. Tab is the primary key. [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Key NextTabKey { - get => ApplicationImpl.Instance.Keyboard.NextTabKey; - set => ApplicationImpl.Instance.Keyboard.NextTabKey = value; + get => _nextTabKey; + set + { + Key oldValue = _nextTabKey; + _nextTabKey = value; + NextTabKeyChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _nextTabKey)); + } } + /// Raised when changes. + public static event EventHandler>? NextTabKeyChanged; + /// /// Raised when the user releases a key. /// @@ -48,19 +68,39 @@ public static partial class Application // Navigation stuff remove => ApplicationImpl.Instance.Keyboard.KeyUp -= value; } + private static Key _prevTabGroupKey = Key.F6.WithShift; // Resources/config.json overrides + /// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key. [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Key PrevTabGroupKey { - get => ApplicationImpl.Instance.Keyboard.PrevTabGroupKey; - set => ApplicationImpl.Instance.Keyboard.PrevTabGroupKey = value; + get => _prevTabGroupKey; + set + { + Key oldValue = _prevTabGroupKey; + _prevTabGroupKey = value; + PrevTabGroupKeyChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _prevTabGroupKey)); + } } + /// Raised when changes. + public static event EventHandler>? PrevTabGroupKeyChanged; + + private static Key _prevTabKey = Key.Tab.WithShift; // Resources/config.json overrides + /// Alternative key to navigate backwards through views. Shift+Tab is the primary key. [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Key PrevTabKey { - get => ApplicationImpl.Instance.Keyboard.PrevTabKey; - set => ApplicationImpl.Instance.Keyboard.PrevTabKey = value; + get => _prevTabKey; + set + { + Key oldValue = _prevTabKey; + _prevTabKey = value; + PrevTabKeyChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _prevTabKey)); + } } + + /// Raised when changes. + public static event EventHandler>? PrevTabKeyChanged; } diff --git a/Terminal.Gui/App/Application.Run.cs b/Terminal.Gui/App/Application.Run.cs index 9e6b2e064..81cea2171 100644 --- a/Terminal.Gui/App/Application.Run.cs +++ b/Terminal.Gui/App/Application.Run.cs @@ -4,22 +4,42 @@ namespace Terminal.Gui.App; public static partial class Application // Run (Begin -> Run -> Layout/Draw -> End -> Stop) { + private static Key _quitKey = Key.Esc; // Resources/config.json overrides + /// Gets or sets the key to quit the application. [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Key QuitKey { - get => ApplicationImpl.Instance.Keyboard.QuitKey; - set => ApplicationImpl.Instance.Keyboard.QuitKey = value; + get => _quitKey; + set + { + Key oldValue = _quitKey; + _quitKey = value; + QuitKeyChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _quitKey)); + } } + /// Raised when changes. + public static event EventHandler>? QuitKeyChanged; + + private static Key _arrangeKey = Key.F5.WithCtrl; // Resources/config.json overrides + /// Gets or sets the key to activate arranging views using the keyboard. [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Key ArrangeKey { - get => ApplicationImpl.Instance.Keyboard.ArrangeKey; - set => ApplicationImpl.Instance.Keyboard.ArrangeKey = value; + get => _arrangeKey; + set + { + Key oldValue = _arrangeKey; + _arrangeKey = value; + ArrangeKeyChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _arrangeKey)); + } } + /// Raised when changes. + public static event EventHandler>? ArrangeKeyChanged; + /// [Obsolete ("The legacy static Application object is going away.")] public static SessionToken Begin (Toplevel toplevel) => ApplicationImpl.Instance.Begin (toplevel); @@ -88,7 +108,7 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E /// [Obsolete ("The legacy static Application object is going away.")] - public static event EventHandler? Iteration + public static event EventHandler>? Iteration { add => ApplicationImpl.Instance.Iteration += value; remove => ApplicationImpl.Instance.Iteration -= value; diff --git a/Terminal.Gui/App/Application.Current.cs b/Terminal.Gui/App/Application.TopRunnable.cs similarity index 91% rename from Terminal.Gui/App/Application.Current.cs rename to Terminal.Gui/App/Application.TopRunnable.cs index 1b91a45ff..85a25cd06 100644 --- a/Terminal.Gui/App/Application.Current.cs +++ b/Terminal.Gui/App/Application.TopRunnable.cs @@ -2,7 +2,7 @@ using System.Collections.Concurrent; namespace Terminal.Gui.App; -public static partial class Application // Current handling +public static partial class Application // TopRunnable handling { /// [Obsolete ("The legacy static Application object is going away.")] public static ConcurrentStack SessionStack => ApplicationImpl.Instance.SessionStack; diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs index ed1e98741..e0a7390b7 100644 --- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs +++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs @@ -1,10 +1,14 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Reflection; namespace Terminal.Gui.App; public partial class ApplicationImpl { + /// + public int? MainThreadId { get; set; } + /// public bool Initialized { get; set; } @@ -23,6 +27,29 @@ public partial class ApplicationImpl throw new InvalidOperationException ("Init called multiple times without Shutdown"); } + // Thread-safe fence check: Ensure we're not mixing application models + // Use lock to make check-and-set atomic + lock (_modelUsageLock) + { + // If this is a legacy static instance and instance-based model was used, throw + if (this == _instance && ModelUsage == ApplicationModelUsage.InstanceBased) + { + throw new InvalidOperationException (ERROR_LEGACY_AFTER_MODERN); + } + + // If this is an instance-based instance and legacy static model was used, throw + if (this != _instance && ModelUsage == ApplicationModelUsage.LegacyStatic) + { + throw new InvalidOperationException (ERROR_MODERN_AFTER_LEGACY); + } + + // If no model has been set yet, set it now based on which instance this is + if (ModelUsage == ApplicationModelUsage.None) + { + ModelUsage = this == _instance ? ApplicationModelUsage.LegacyStatic : ApplicationModelUsage.InstanceBased; + } + } + if (!string.IsNullOrWhiteSpace (driverName)) { _driverName = driverName; @@ -41,26 +68,24 @@ public partial class ApplicationImpl // Preserve existing keyboard settings if they exist bool hasExistingKeyboard = _keyboard is { }; - Key existingQuitKey = _keyboard?.QuitKey ?? Key.Esc; - Key existingArrangeKey = _keyboard?.ArrangeKey ?? Key.F5.WithCtrl; - Key existingNextTabKey = _keyboard?.NextTabKey ?? Key.Tab; - Key existingPrevTabKey = _keyboard?.PrevTabKey ?? Key.Tab.WithShift; - Key existingNextTabGroupKey = _keyboard?.NextTabGroupKey ?? Key.F6; - Key existingPrevTabGroupKey = _keyboard?.PrevTabGroupKey ?? Key.F6.WithShift; + Key existingQuitKey = _keyboard?.QuitKey ?? Application.QuitKey; + Key existingArrangeKey = _keyboard?.ArrangeKey ?? Application.ArrangeKey; + Key existingNextTabKey = _keyboard?.NextTabKey ?? Application.NextTabKey; + Key existingPrevTabKey = _keyboard?.PrevTabKey ?? Application.PrevTabKey; + Key existingNextTabGroupKey = _keyboard?.NextTabGroupKey ?? Application.NextTabGroupKey; + Key existingPrevTabGroupKey = _keyboard?.PrevTabGroupKey ?? Application.PrevTabGroupKey; // Reset keyboard to ensure fresh state with default bindings _keyboard = new KeyboardImpl { App = this }; - // Restore previously set keys if they existed and were different from defaults - if (hasExistingKeyboard) - { - _keyboard.QuitKey = existingQuitKey; - _keyboard.ArrangeKey = existingArrangeKey; - _keyboard.NextTabKey = existingNextTabKey; - _keyboard.PrevTabKey = existingPrevTabKey; - _keyboard.NextTabGroupKey = existingNextTabGroupKey; - _keyboard.PrevTabGroupKey = existingPrevTabGroupKey; - } + // Sync keys from Application static properties (or existing keyboard if it had custom values) + // This ensures we respect any Application.QuitKey etc changes made before Init() + _keyboard.QuitKey = existingQuitKey; + _keyboard.ArrangeKey = existingArrangeKey; + _keyboard.NextTabKey = existingNextTabKey; + _keyboard.PrevTabKey = existingPrevTabKey; + _keyboard.NextTabGroupKey = existingNextTabGroupKey; + _keyboard.PrevTabGroupKey = existingPrevTabGroupKey; CreateDriver (_driverName); Screen = Driver!.Screen; @@ -85,8 +110,8 @@ public partial class ApplicationImpl if (runnableToDispose is { }) { // Extract the result using reflection to get the Result property value - var resultProperty = runnableToDispose.GetType().GetProperty("Result"); - result = resultProperty?.GetValue(runnableToDispose); + PropertyInfo? resultProperty = runnableToDispose.GetType ().GetProperty ("Result"); + result = resultProperty?.GetValue (runnableToDispose); } // Stop the coordinator if running @@ -115,8 +140,9 @@ public partial class ApplicationImpl { if (runnableToDispose is IDisposable disposable) { - disposable.Dispose(); + disposable.Dispose (); } + FrameworkOwnedRunnable = null; } @@ -140,36 +166,6 @@ public partial class ApplicationImpl return result; } -#if DEBUG - /// - /// DEBUG ONLY: Asserts that an event has no remaining subscribers. - /// - /// The name of the event for diagnostic purposes. - /// The event delegate to check. - private static void AssertNoEventSubscribers (string eventName, Delegate? eventDelegate) - { - if (eventDelegate is null) - { - return; - } - - Delegate [] subscribers = eventDelegate.GetInvocationList (); - - if (subscribers.Length > 0) - { - string subscriberInfo = string.Join ( - ", ", - subscribers.Select (d => $"{d.Method.DeclaringType?.Name}.{d.Method.Name}" - ) - ); - - Debug.Fail ( - $"Application.{eventName} has {subscribers.Length} remaining subscriber(s) after Shutdown: {subscriberInfo}" - ); - } - } -#endif - /// public void ResetState (bool ignoreDisposed = false) { @@ -241,6 +237,17 @@ public partial class ApplicationImpl ClearScreenNextIteration = false; // === 6. Reset input systems === + // Dispose keyboard and mouse to unsubscribe from events + if (_keyboard is IDisposable keyboardDisposable) + { + keyboardDisposable.Dispose (); + } + + if (_mouse is IDisposable mouseDisposable) + { + mouseDisposable.Dispose (); + } + // Mouse and Keyboard will be lazy-initialized on next access _mouse = null; _keyboard = null; @@ -273,10 +280,57 @@ public partial class ApplicationImpl // gui.cs does no longer process any callbacks. See #1084 for more details: // (https://github.com/gui-cs/Terminal.Gui/issues/1084). SynchronizationContext.SetSynchronizationContext (null); + + // === 12. Unsubscribe from Application static property change events === + UnsubscribeApplicationEvents (); } /// /// Raises the event. /// internal void RaiseInitializedChanged (object sender, EventArgs e) { InitializedChanged?.Invoke (sender, e); } + +#if DEBUG + /// + /// DEBUG ONLY: Asserts that an event has no remaining subscribers. + /// + /// The name of the event for diagnostic purposes. + /// The event delegate to check. + private static void AssertNoEventSubscribers (string eventName, Delegate? eventDelegate) + { + if (eventDelegate is null) + { + return; + } + + Delegate [] subscribers = eventDelegate.GetInvocationList (); + + if (subscribers.Length > 0) + { + string subscriberInfo = string.Join ( + ", ", + subscribers.Select (d => $"{d.Method.DeclaringType?.Name}.{d.Method.Name}" + ) + ); + + Debug.Fail ( + $"Application.{eventName} has {subscribers.Length} remaining subscriber(s) after Shutdown: {subscriberInfo}" + ); + } + } +#endif + + // Event handlers for Application static property changes + private void OnForce16ColorsChanged (object? sender, ValueChangedEventArgs e) { Force16Colors = e.NewValue; } + + private void OnForceDriverChanged (object? sender, ValueChangedEventArgs e) { ForceDriver = e.NewValue; } + + /// + /// Unsubscribes from Application static property change events. + /// + private void UnsubscribeApplicationEvents () + { + Application.Force16ColorsChanged -= OnForce16ColorsChanged; + Application.ForceDriverChanged -= OnForceDriverChanged; + } } diff --git a/Terminal.Gui/App/ApplicationImpl.Run.cs b/Terminal.Gui/App/ApplicationImpl.Run.cs index 944c64e09..e790e3ca0 100644 --- a/Terminal.Gui/App/ApplicationImpl.Run.cs +++ b/Terminal.Gui/App/ApplicationImpl.Run.cs @@ -5,15 +5,6 @@ namespace Terminal.Gui.App; public partial class ApplicationImpl { - /// - /// INTERNAL: Gets or sets the managed thread ID of the application's main UI thread, which is set during - /// and used to determine if code is executing on the main thread. - /// - /// - /// The managed thread ID of the main UI thread, or if the application is not initialized. - /// - internal int? MainThreadId { get; set; } - #region Begin->Run->Stop->End // TODO: This API is not used anywhere; it can be deleted @@ -156,11 +147,11 @@ public partial class ApplicationImpl /// public void RaiseIteration () { - Iteration?.Invoke (null, new ()); + Iteration?.Invoke (null, new (this)); } /// - public event EventHandler? Iteration; + public event EventHandler>? Iteration; /// [RequiresUnreferencedCode ("AOT")] diff --git a/Terminal.Gui/App/ApplicationImpl.cs b/Terminal.Gui/App/ApplicationImpl.cs index 2dc54cbda..6e968d0d3 100644 --- a/Terminal.Gui/App/ApplicationImpl.cs +++ b/Terminal.Gui/App/ApplicationImpl.cs @@ -9,23 +9,72 @@ namespace Terminal.Gui.App; public partial class ApplicationImpl : IApplication { /// - /// INTERNAL: Creates a new instance of the Application backend. + /// INTERNAL: Creates a new instance of the Application backend and subscribes to Application configuration property + /// events. /// - internal ApplicationImpl () { } + internal ApplicationImpl () + { + // Subscribe to Application static property change events + Application.Force16ColorsChanged += OnForce16ColorsChanged; + Application.ForceDriverChanged += OnForceDriverChanged; + } /// /// INTERNAL: Creates a new instance of the Application backend. /// /// - internal ApplicationImpl (IComponentFactory componentFactory) { _componentFactory = componentFactory; } + internal ApplicationImpl (IComponentFactory componentFactory) : this () { _componentFactory = componentFactory; } + + private string? _driverName; + + #region Clipboard + + /// + public IClipboard? Clipboard => Driver?.Clipboard; + + #endregion Clipboard + + /// + public new string ToString () => Driver?.ToString () ?? string.Empty; #region Singleton + /// + /// Lock object for synchronizing access to ModelUsage and _instance. + /// + private static readonly object _modelUsageLock = new (); + + /// + /// Tracks which application model has been used in this process. + /// + public static ApplicationModelUsage ModelUsage { get; private set; } = ApplicationModelUsage.None; + + /// + /// Error message for when trying to use modern model after legacy static model. + /// + internal const string ERROR_MODERN_AFTER_LEGACY = + "Cannot use modern instance-based model (Application.Create) after using legacy static Application model (Application.Init/ApplicationImpl.Instance). " + + "Use only one model per process."; + + /// + /// Error message for when trying to use legacy static model after modern model. + /// + internal const string ERROR_LEGACY_AFTER_MODERN = + "Cannot use legacy static Application model (Application.Init/ApplicationImpl.Instance) after using modern instance-based model (Application.Create). " + + "Use only one model per process."; + /// /// Configures the singleton instance of to use the specified backend implementation. /// /// - public static void SetInstance (IApplication? app) { _instance = app; } + public static void SetInstance (IApplication? app) + { + lock (_modelUsageLock) + { + ModelUsage = ApplicationModelUsage.LegacyStatic; + _instance = app; + } + } // Private static readonly Lazy instance of Application private static IApplication? _instance; @@ -33,12 +82,92 @@ public partial class ApplicationImpl : IApplication /// /// Gets the currently configured backend implementation of gateway methods. /// - public static IApplication Instance => _instance ??= new ApplicationImpl (); + public static IApplication Instance + { + get + { + //Debug.Fail ("ApplicationImpl.Instance accessed - parallelizable tests should not use legacy static Application model"); + + // Thread-safe: Use lock to make check-and-create atomic + lock (_modelUsageLock) + { + // If an instance already exists, return it without fence checking + // This allows for cleanup/reset operations + if (_instance is { }) + { + return _instance; + } + + // Check if the instance-based model has already been used + if (ModelUsage == ApplicationModelUsage.InstanceBased) + { + throw new InvalidOperationException (ERROR_LEGACY_AFTER_MODERN); + } + + // Mark the usage and create the instance + ModelUsage = ApplicationModelUsage.LegacyStatic; + + return _instance = new ApplicationImpl (); + } + } + } + + /// + /// INTERNAL: Marks that the instance-based model has been used. Called by Application.Create(). + /// + internal static void MarkInstanceBasedModelUsed () + { + lock (_modelUsageLock) + { + // Check if the legacy static model has already been initialized + if (ModelUsage == ApplicationModelUsage.LegacyStatic && _instance?.Initialized == true) + { + throw new InvalidOperationException (ERROR_MODERN_AFTER_LEGACY); + } + + ModelUsage = ApplicationModelUsage.InstanceBased; + } + } + + /// + /// INTERNAL: Resets the model usage tracking. Only for testing purposes. + /// + internal static void ResetModelUsageTracking () + { + lock (_modelUsageLock) + { + ModelUsage = ApplicationModelUsage.None; + _instance = null; + } + } + + /// + /// INTERNAL: Resets state without going through the fence-checked Instance property. + /// Used by Application.ResetState() to allow cleanup regardless of which model was used. + /// + internal static void ResetStateStatic (bool ignoreDisposed = false) + { + // If an instance exists, reset it + _instance?.ResetState (ignoreDisposed); + + // Reset Application static properties to their defaults + // This ensures tests start with clean state + Application.ForceDriver = string.Empty; + Application.Force16Colors = false; + Application.IsMouseDisabled = false; + Application.QuitKey = Key.Esc; + Application.ArrangeKey = Key.F5.WithCtrl; + Application.NextTabGroupKey = Key.F6; + Application.NextTabKey = Key.Tab; + Application.PrevTabGroupKey = Key.F6.WithShift; + Application.PrevTabKey = Key.Tab.WithShift; + + // Always reset the model tracking to allow tests to use either model after reset + ResetModelUsageTracking (); + } #endregion Singleton - private string? _driverName; - #region Input private IMouse? _mouse; @@ -122,8 +251,6 @@ public partial class ApplicationImpl : IApplication } } - // BUGBUG: Technically, this is not the full lst of sessions. There be dragons here, e.g. see how Toplevel.Id is used. What - /// public ConcurrentStack SessionStack { get; } = new (); @@ -137,7 +264,4 @@ public partial class ApplicationImpl : IApplication public IRunnable? FrameworkOwnedRunnable { get; set; } #endregion View Management - - /// - public new string ToString () => Driver?.ToString () ?? string.Empty; } diff --git a/Terminal.Gui/App/ApplicationModelUsage.cs b/Terminal.Gui/App/ApplicationModelUsage.cs new file mode 100644 index 000000000..909291d70 --- /dev/null +++ b/Terminal.Gui/App/ApplicationModelUsage.cs @@ -0,0 +1,16 @@ +namespace Terminal.Gui.App; + +/// +/// Defines the different application usage models. +/// +public enum ApplicationModelUsage +{ + /// No model has been used yet. + None, + + /// Legacy static model (Application.Init/ApplicationImpl.Instance). + LegacyStatic, + + /// Modern instance-based model (Application.Create). + InstanceBased +} diff --git a/Terminal.Gui/App/ApplicationNavigation.cs b/Terminal.Gui/App/ApplicationNavigation.cs index 1149c3ad6..6012d4629 100644 --- a/Terminal.Gui/App/ApplicationNavigation.cs +++ b/Terminal.Gui/App/ApplicationNavigation.cs @@ -13,7 +13,7 @@ public class ApplicationNavigation /// public ApplicationNavigation () { - // TODO: Move navigation key bindings here from AddApplicationKeyBindings + // TODO: Move navigation key bindings here from KeyboardImpl } /// diff --git a/Terminal.Gui/App/ApplicationRunnableExtensions.cs b/Terminal.Gui/App/ApplicationRunnableExtensions.cs index 7e706e9d5..3eb03c081 100644 --- a/Terminal.Gui/App/ApplicationRunnableExtensions.cs +++ b/Terminal.Gui/App/ApplicationRunnableExtensions.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui.App; /// /// /// These extensions provide convenience methods for wrapping views in -/// and running them in a single call, similar to how works. +/// and running them in a single call, similar to how works. /// public static class ApplicationRunnableExtensions { diff --git a/Terminal.Gui/App/Clipboard/Clipboard.cs b/Terminal.Gui/App/Clipboard/Clipboard.cs index 0db013351..42472e414 100644 --- a/Terminal.Gui/App/Clipboard/Clipboard.cs +++ b/Terminal.Gui/App/Clipboard/Clipboard.cs @@ -2,6 +2,9 @@ namespace Terminal.Gui.App; /// Provides cut, copy, and paste support for the OS clipboard. /// +/// +/// DEPRECATED: This static class is obsolete. Use instead. +/// /// On Windows, the class uses the Windows Clipboard APIs via P/Invoke. /// /// On Linux, when not running under Windows Subsystem for Linux (WSL), the class uses @@ -16,6 +19,7 @@ namespace Terminal.Gui.App; /// the Mac clipboard APIs vai P/Invoke. /// /// +[Obsolete ("Use IApplication.Clipboard instead. The static Clipboard class will be removed in a future release.")] public static class Clipboard { private static string? _contents = string.Empty; @@ -65,4 +69,32 @@ public static class Clipboard /// Returns true if the environmental dependencies are in place to interact with the OS clipboard. /// public static bool IsSupported => Application.Driver?.Clipboard?.IsSupported ?? false; + + /// Gets the OS clipboard data if possible. + /// The clipboard data if successful. + /// if the clipboard data was retrieved successfully; otherwise, . + public static bool TryGetClipboardData (out string result) + { + result = string.Empty; + + if (IsSupported && Application.Driver?.Clipboard is { }) + { + return Application.Driver.Clipboard.TryGetClipboardData (out result); + } + + return false; + } + + /// Sets the OS clipboard data if possible. + /// The text to set. + /// if the clipboard data was set successfully; otherwise, . + public static bool TrySetClipboardData (string text) + { + if (IsSupported && Application.Driver?.Clipboard is { }) + { + return Application.Driver.Clipboard.TrySetClipboardData (text); + } + + return false; + } } diff --git a/Terminal.Gui/App/IApplication.cs b/Terminal.Gui/App/IApplication.cs index f663a351f..a4a8c902e 100644 --- a/Terminal.Gui/App/IApplication.cs +++ b/Terminal.Gui/App/IApplication.cs @@ -39,6 +39,16 @@ public interface IApplication #region Initialization and Shutdown + /// + /// Gets or sets the managed thread ID of the application's main UI thread, which is set during + /// and used to determine if code is executing on the main thread. + /// + /// + /// The managed thread ID of the main UI thread, or if the application is not initialized. + /// + public int? MainThreadId { get; internal set; } + + /// Initializes a new instance of Application. /// /// The short name (e.g. "dotnet", "windows", "unix", or "fake") of the @@ -218,7 +228,7 @@ public interface IApplication [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] public TView Run (Func? errorHandler = null, string? driverName = null) - where TView : Toplevel, new (); + where TView : Toplevel, new(); /// /// Runs a new Session using the provided view and calling @@ -273,9 +283,11 @@ public interface IApplication /// /// This event is raised before input processing, timeout callbacks, and rendering occur each iteration. /// - /// See also and . + /// The event args contain the current application instance. /// - public event EventHandler? Iteration; + /// + /// . + public event EventHandler>? Iteration; /// Runs on the main UI loop thread. /// The action to be invoked on the main processing thread. @@ -523,7 +535,7 @@ public interface IApplication /// Supports fluent API: var result = Application.Create().Init().Run<MyView>().Shutdown() as MyResultType /// /// - IApplication Run (Func? errorHandler = null) where TRunnable : IRunnable, new (); + IApplication Run (Func? errorHandler = null) where TRunnable : IRunnable, new(); /// /// Requests that the specified runnable session stop. @@ -574,6 +586,17 @@ public interface IApplication /// IDriver? Driver { get; set; } + /// + /// Gets the clipboard for this application instance. + /// + /// + /// + /// Provides access to the OS clipboard through the driver. Returns if + /// is not initialized. + /// + /// + IClipboard? Clipboard { get; } + /// /// Gets or sets whether will be forced to output only the 16 colors defined in /// . The default is , meaning 24-bit (TrueColor) colors will be diff --git a/Terminal.Gui/App/IterationEventArgs.cs b/Terminal.Gui/App/IterationEventArgs.cs deleted file mode 100644 index e0c98d2ab..000000000 --- a/Terminal.Gui/App/IterationEventArgs.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Terminal.Gui.App; - -/// Event arguments for the event. -public class IterationEventArgs : EventArgs -{ } diff --git a/Terminal.Gui/App/Keyboard/KeyboardImpl.cs b/Terminal.Gui/App/Keyboard/KeyboardImpl.cs index 75c37d4b8..7b4d26e2d 100644 --- a/Terminal.Gui/App/Keyboard/KeyboardImpl.cs +++ b/Terminal.Gui/App/Keyboard/KeyboardImpl.cs @@ -1,7 +1,10 @@ +using System.Collections.Concurrent; + namespace Terminal.Gui.App; /// /// INTERNAL: Implements to manage keyboard input and key bindings at the Application level. +/// This implementation is thread-safe for all public operations. /// /// This implementation decouples keyboard handling state from the static class, /// enabling parallelizable unit tests and better testability. @@ -10,19 +13,61 @@ namespace Terminal.Gui.App; /// See for usage details. /// /// -internal class KeyboardImpl : IKeyboard +internal class KeyboardImpl : IKeyboard, IDisposable { - private Key _quitKey = Key.Esc; // Resources/config.json overrides - private Key _arrangeKey = Key.F5.WithCtrl; // Resources/config.json overrides - private Key _nextTabGroupKey = Key.F6; // Resources/config.json overrides - private Key _nextTabKey = Key.Tab; // Resources/config.json overrides - private Key _prevTabGroupKey = Key.F6.WithShift; // Resources/config.json overrides - private Key _prevTabKey = Key.Tab.WithShift; // Resources/config.json overrides + /// + /// Initializes keyboard bindings and subscribes to Application configuration property events. + /// + public KeyboardImpl () + { + // DON'T access Application static properties here - they trigger ApplicationImpl.Instance + // which sets ModelUsage to LegacyStatic, breaking parallel tests. + // These will be initialized from Application static properties in Init() or when accessed. + + // Initialize to reasonable defaults that match Application defaults + // These will be updated by property change events if Application properties change + _quitKey = Key.Esc; + _arrangeKey = Key.F5.WithCtrl; + _nextTabGroupKey = Key.F6; + _nextTabKey = Key.Tab; + _prevTabGroupKey = Key.F6.WithShift; + _prevTabKey = Key.Tab.WithShift; + + // Subscribe to Application static property change events + // so we get updated if they change + Application.QuitKeyChanged += OnQuitKeyChanged; + Application.ArrangeKeyChanged += OnArrangeKeyChanged; + Application.NextTabGroupKeyChanged += OnNextTabGroupKeyChanged; + Application.NextTabKeyChanged += OnNextTabKeyChanged; + Application.PrevTabGroupKeyChanged += OnPrevTabGroupKeyChanged; + Application.PrevTabKeyChanged += OnPrevTabKeyChanged; + + AddKeyBindings (); + } /// - /// Commands for Application. + /// Commands for Application. Thread-safe for concurrent access. /// - private readonly Dictionary _commandImplementations = new (); + private readonly ConcurrentDictionary _commandImplementations = new (); + + private Key _quitKey; + private Key _arrangeKey; + private Key _nextTabGroupKey; + private Key _nextTabKey; + private Key _prevTabGroupKey; + private Key _prevTabKey; + + /// + public void Dispose () + { + // Unsubscribe from Application static property change events + Application.QuitKeyChanged -= OnQuitKeyChanged; + Application.ArrangeKeyChanged -= OnArrangeKeyChanged; + Application.NextTabGroupKeyChanged -= OnNextTabGroupKeyChanged; + Application.NextTabKeyChanged -= OnNextTabKeyChanged; + Application.PrevTabGroupKeyChanged -= OnPrevTabGroupKeyChanged; + Application.PrevTabKeyChanged -= OnPrevTabKeyChanged; + } /// public IApplication? App { get; set; } @@ -102,14 +147,6 @@ internal class KeyboardImpl : IKeyboard /// public event EventHandler? KeyUp; - /// - /// Initializes keyboard bindings. - /// - public KeyboardImpl () - { - AddKeyBindings (); - } - /// public bool RaiseKeyDownEvent (Key key) { @@ -165,7 +202,8 @@ internal class KeyboardImpl : IKeyboard } bool? commandHandled = InvokeCommandsBoundToKey (key); - if(commandHandled is true) + + if (commandHandled is true) { return true; } @@ -188,7 +226,6 @@ internal class KeyboardImpl : IKeyboard return true; } - // TODO: Add Popover support if (App?.SessionStack is { }) @@ -214,6 +251,7 @@ internal class KeyboardImpl : IKeyboard public bool? InvokeCommandsBoundToKey (Key key) { bool? handled = null; + // Invoke any Application-scoped KeyBindings. // The first view that handles the key will stop the loop. // foreach (KeyValuePair binding in KeyBindings.GetBindings (key)) @@ -264,24 +302,6 @@ internal class KeyboardImpl : IKeyboard return null; } - /// - /// - /// Sets the function that will be invoked for a . - /// - /// - /// If AddCommand has already been called for will - /// replace the old one. - /// - /// - /// - /// - /// This version of AddCommand is for commands that do not require a . - /// - /// - /// The command. - /// The function. - private void AddCommand (Command command, Func f) { _commandImplementations [command] = ctx => f (); } - internal void AddKeyBindings () { _commandImplementations.Clear (); @@ -296,6 +316,7 @@ internal class KeyboardImpl : IKeyboard return true; } ); + AddCommand ( Command.Suspend, () => @@ -305,6 +326,7 @@ internal class KeyboardImpl : IKeyboard return true; } ); + AddCommand ( Command.NextTabStop, () => App?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop)); @@ -351,31 +373,64 @@ internal class KeyboardImpl : IKeyboard return false; }); - //SetKeysToHardCodedDefaults (); - // Need to clear after setting the above to ensure actually clear - // because set_QuitKey etc.. may call Add - KeyBindings.Clear (); + // because set_QuitKey etc. may call Add + //KeyBindings.Clear (); - KeyBindings.Add (QuitKey, Command.Quit); - KeyBindings.Add (NextTabKey, Command.NextTabStop); - KeyBindings.Add (PrevTabKey, Command.PreviousTabStop); - KeyBindings.Add (NextTabGroupKey, Command.NextTabGroup); - KeyBindings.Add (PrevTabGroupKey, Command.PreviousTabGroup); - KeyBindings.Add (ArrangeKey, Command.Arrange); + // Use ReplaceCommands instead of Add, because it's possible that + // during construction the Application static properties changed, and + // we added those keys already. + KeyBindings.ReplaceCommands (QuitKey, Command.Quit); + KeyBindings.ReplaceCommands (NextTabKey, Command.NextTabStop); + KeyBindings.ReplaceCommands (PrevTabKey, Command.PreviousTabStop); + KeyBindings.ReplaceCommands (NextTabGroupKey, Command.NextTabGroup); + KeyBindings.ReplaceCommands (PrevTabGroupKey, Command.PreviousTabGroup); + KeyBindings.ReplaceCommands (ArrangeKey, Command.Arrange); - KeyBindings.Add (Key.CursorRight, Command.NextTabStop); - KeyBindings.Add (Key.CursorDown, Command.NextTabStop); - KeyBindings.Add (Key.CursorLeft, Command.PreviousTabStop); - KeyBindings.Add (Key.CursorUp, Command.PreviousTabStop); + // TODO: Should these be configurable? + KeyBindings.ReplaceCommands (Key.CursorRight, Command.NextTabStop); + KeyBindings.ReplaceCommands (Key.CursorDown, Command.NextTabStop); + KeyBindings.ReplaceCommands (Key.CursorLeft, Command.PreviousTabStop); + KeyBindings.ReplaceCommands (Key.CursorUp, Command.PreviousTabStop); // TODO: Refresh Key should be configurable - KeyBindings.Add (Key.F5, Command.Refresh); + KeyBindings.ReplaceCommands (Key.F5, Command.Refresh); // TODO: Suspend Key should be configurable if (Environment.OSVersion.Platform == PlatformID.Unix) { - KeyBindings.Add (Key.Z.WithCtrl, Command.Suspend); + KeyBindings.ReplaceCommands (Key.Z.WithCtrl, Command.Suspend); } } + + /// + /// + /// Sets the function that will be invoked for a . + /// + /// + /// If AddCommand has already been called for will + /// replace the old one. + /// + /// + /// + /// + /// This version of AddCommand is for commands that do not require a . + /// + /// + /// The command. + /// The function. + private void AddCommand (Command command, Func f) { _commandImplementations [command] = ctx => f (); } + + private void OnArrangeKeyChanged (object? sender, ValueChangedEventArgs e) { ArrangeKey = e.NewValue; } + + private void OnNextTabGroupKeyChanged (object? sender, ValueChangedEventArgs e) { NextTabGroupKey = e.NewValue; } + + private void OnNextTabKeyChanged (object? sender, ValueChangedEventArgs e) { NextTabKey = e.NewValue; } + + private void OnPrevTabGroupKeyChanged (object? sender, ValueChangedEventArgs e) { PrevTabGroupKey = e.NewValue; } + + private void OnPrevTabKeyChanged (object? sender, ValueChangedEventArgs e) { PrevTabKey = e.NewValue; } + + // Event handlers for Application static property changes + private void OnQuitKeyChanged (object? sender, ValueChangedEventArgs e) { QuitKey = e.NewValue; } } diff --git a/Terminal.Gui/App/Mouse/MouseImpl.cs b/Terminal.Gui/App/Mouse/MouseImpl.cs index 12aa4ada9..42dc1c388 100644 --- a/Terminal.Gui/App/Mouse/MouseImpl.cs +++ b/Terminal.Gui/App/Mouse/MouseImpl.cs @@ -9,15 +9,25 @@ namespace Terminal.Gui.App; /// enabling better testability and parallel test execution. /// /// -internal class MouseImpl : IMouse +internal class MouseImpl : IMouse, IDisposable { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class and subscribes to Application configuration property events. /// - public MouseImpl () { } + public MouseImpl () + { + // Subscribe to Application static property change events + Application.IsMouseDisabledChanged += OnIsMouseDisabledChanged; + } + + private IApplication? _app; /// - public IApplication? App { get; set; } + public IApplication? App + { + get => _app; + set => _app = value; + } /// public Point? LastMousePosition { get; set; } @@ -391,4 +401,17 @@ internal class MouseImpl : IMouse return false; } + + // Event handler for Application static property changes + private void OnIsMouseDisabledChanged (object? sender, ValueChangedEventArgs e) + { + IsMouseDisabled = e.NewValue; + } + + /// + public void Dispose () + { + // Unsubscribe from Application static property change events + Application.IsMouseDisabledChanged -= OnIsMouseDisabledChanged; + } } diff --git a/Terminal.Gui/App/Runnable/IRunnable.cs b/Terminal.Gui/App/Runnable/IRunnable.cs index 2e6711d0c..75ef00b1b 100644 --- a/Terminal.Gui/App/Runnable/IRunnable.cs +++ b/Terminal.Gui/App/Runnable/IRunnable.cs @@ -66,7 +66,7 @@ public interface IRunnable /// /// Raised when is changing (e.g., when or /// is called). - /// Can be canceled by setting to . + /// Can be canceled by setting `args.Cancel` to . /// /// /// @@ -140,7 +140,7 @@ public interface IRunnable /// /// Raised when this runnable is about to become modal (top of stack) or cease being modal. - /// Can be canceled by setting to . + /// Can be canceled by setting `args.Cancel` to . /// /// /// diff --git a/Terminal.Gui/Configuration/ConfigurationManager.cs b/Terminal.Gui/Configuration/ConfigurationManager.cs index b8d3cdf5d..aa79c26e9 100644 --- a/Terminal.Gui/Configuration/ConfigurationManager.cs +++ b/Terminal.Gui/Configuration/ConfigurationManager.cs @@ -595,15 +595,53 @@ public static class ConfigurationManager TypeInfoResolver = SourceGenerationContext.Default }); + private static SourcesManager? _sourcesManager = new (); + private static readonly object _sourcesManagerLock = new (); + /// /// Gets the Sources Manager - manages the loading of configuration sources from files and resources. /// - public static SourcesManager? SourcesManager { get; internal set; } = new (); + public static SourcesManager? SourcesManager + { + get + { + lock (_sourcesManagerLock) + { + return _sourcesManager; + } + } + internal set + { + lock (_sourcesManagerLock) + { + _sourcesManager = value; + } + } + } + + private static string? _runtimeConfig = """{ }"""; + private static readonly object _runtimeConfigLock = new (); /// /// Gets or sets the in-memory config.json. See . /// - public static string? RuntimeConfig { get; set; } = """{ }"""; + public static string? RuntimeConfig + { + get + { + lock (_runtimeConfigLock) + { + return _runtimeConfig; + } + } + set + { + lock (_runtimeConfigLock) + { + _runtimeConfig = value; + } + } + } [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] private static readonly string _configFilename = "config.json"; @@ -678,13 +716,32 @@ public static class ConfigurationManager [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] internal static StringBuilder _jsonErrors = new (); + private static bool? _throwOnJsonErrors = false; + private static readonly object _throwOnJsonErrorsLock = new (); + /// /// Gets or sets whether the should throw an exception if it encounters an /// error on deserialization. If (the default), the error is logged and printed to the console /// when is called. /// [ConfigurationProperty (Scope = typeof (SettingsScope))] - public static bool? ThrowOnJsonErrors { get; set; } = false; + public static bool? ThrowOnJsonErrors + { + get + { + lock (_throwOnJsonErrorsLock) + { + return _throwOnJsonErrors; + } + } + set + { + lock (_throwOnJsonErrorsLock) + { + _throwOnJsonErrors = value; + } + } + } #pragma warning disable IDE1006 // Naming Styles private static readonly object _jsonErrorsLock = new (); @@ -758,8 +815,27 @@ public static class ConfigurationManager return JsonSerializer.Serialize (emptyScope, typeof (SettingsScope), SerializerContext!); } + private static string _appName = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!; + private static readonly object _appNameLock = new (); + /// Name of the running application. By default, this property is set to the application's assembly name. - public static string AppName { get; set; } = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!; + public static string AppName + { + get + { + lock (_appNameLock) + { + return _appName; + } + } + set + { + lock (_appNameLock) + { + _appName = value; + } + } + } /// /// INTERNAL: Retrieves all uninitialized configuration properties that belong to a specific scope from the cache. diff --git a/Terminal.Gui/Configuration/SourcesManager.cs b/Terminal.Gui/Configuration/SourcesManager.cs index 541d452a2..71c32ed36 100644 --- a/Terminal.Gui/Configuration/SourcesManager.cs +++ b/Terminal.Gui/Configuration/SourcesManager.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System.Collections.Concurrent; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json; @@ -13,7 +14,7 @@ public class SourcesManager /// /// Provides a map from each of the to file system and resource paths that have been loaded by . /// - public Dictionary Sources { get; } = new (); + public ConcurrentDictionary Sources { get; } = new (); /// INTERNAL: Loads into the specified . /// The Settings Scope object that will be loaded into. @@ -62,11 +63,8 @@ public class SourcesManager internal void AddSource (ConfigLocations location, string source) { - if (!Sources.TryAdd (location, source)) - { - //Logging.Warning ($"{location} has already been added to Sources."); - Sources [location] = source; - } + // ConcurrentDictionary's AddOrUpdate is thread-safe + Sources.AddOrUpdate (location, source, (key, oldValue) => source); } diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeInputProcessor.cs b/Terminal.Gui/Drivers/FakeDriver/FakeInputProcessor.cs index 83b0739a7..f6c777e33 100644 --- a/Terminal.Gui/Drivers/FakeDriver/FakeInputProcessor.cs +++ b/Terminal.Gui/Drivers/FakeDriver/FakeInputProcessor.cs @@ -1,4 +1,3 @@ -#nullable disable using System.Collections.Concurrent; namespace Terminal.Gui.Drivers; @@ -27,17 +26,18 @@ public class FakeInputProcessor : InputProcessorImpl } /// - public override void EnqueueMouseEvent (MouseEventArgs mouseEvent) + public override void EnqueueMouseEvent (IApplication? app, MouseEventArgs mouseEvent) { // FakeDriver uses ConsoleKeyInfo as its input record type, which cannot represent mouse events. + // TODO: Verify this is correct. This didn't check the threadId before. // If Application.Invoke is available (running in Application context), defer to next iteration // to ensure proper timing - the event is raised after views are laid out. // Otherwise (unit tests), raise immediately so tests can verify synchronously. - if (Application.MainThreadId is { }) + if (app is {} && app.MainThreadId != Thread.CurrentThread.ManagedThreadId) { // Application is running - use Invoke to defer to next iteration - ApplicationImpl.Instance.Invoke ((_) => RaiseMouseEvent (mouseEvent)); + app?.Invoke ((_) => RaiseMouseEvent (mouseEvent)); } else { diff --git a/Terminal.Gui/Drivers/IInputProcessor.cs b/Terminal.Gui/Drivers/IInputProcessor.cs index 9c800946c..b10ab0842 100644 --- a/Terminal.Gui/Drivers/IInputProcessor.cs +++ b/Terminal.Gui/Drivers/IInputProcessor.cs @@ -1,5 +1,4 @@ - -namespace Terminal.Gui.Drivers; +namespace Terminal.Gui.Drivers; /// /// Interface for main loop class that will process the queued input. @@ -12,7 +11,7 @@ public interface IInputProcessor public event EventHandler? AnsiSequenceSwallowed; /// - /// Gets the name of the driver associated with this input processor. + /// Gets the name of the driver associated with this input processor. /// string? DriverName { get; init; } @@ -58,7 +57,8 @@ public interface IInputProcessor /// Called when a key up event has been dequeued. Raises the event. /// /// - /// Drivers that do not support key release events will call this method after processing + /// Drivers that do not support key release events will call this method after + /// processing /// is complete. /// /// The key event data. @@ -89,7 +89,10 @@ public interface IInputProcessor /// /// Adds a mouse input event to the input queue. For unit tests. /// + /// + /// The application instance to use. Used to use Invoke to raise the mouse + /// event in the case where this method is not called on the main thread. + /// /// - void EnqueueMouseEvent (MouseEventArgs mouseEvent); - + void EnqueueMouseEvent (IApplication? app, MouseEventArgs mouseEvent); } diff --git a/Terminal.Gui/Drivers/InputProcessorImpl.cs b/Terminal.Gui/Drivers/InputProcessorImpl.cs index 24c249a84..57b74f1f4 100644 --- a/Terminal.Gui/Drivers/InputProcessorImpl.cs +++ b/Terminal.Gui/Drivers/InputProcessorImpl.cs @@ -122,7 +122,7 @@ public abstract class InputProcessorImpl : IInputProcessor, IDispo public event EventHandler? MouseEvent; /// - public virtual void EnqueueMouseEvent (MouseEventArgs mouseEvent) + public virtual void EnqueueMouseEvent (IApplication? app, MouseEventArgs mouseEvent) { // Base implementation: For drivers where TInputRecord cannot represent mouse events // (e.g., ConsoleKeyInfo), derived classes should override this method. diff --git a/Terminal.Gui/Drivers/OutputBase.cs b/Terminal.Gui/Drivers/OutputBase.cs index 6c10fad0b..ad1f4120e 100644 --- a/Terminal.Gui/Drivers/OutputBase.cs +++ b/Terminal.Gui/Drivers/OutputBase.cs @@ -90,14 +90,16 @@ public abstract class OutputBase } } - foreach (SixelToRender s in Application.Sixel) - { - if (!string.IsNullOrWhiteSpace (s.SixelData)) - { - SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y); - Console.Out.Write (s.SixelData); - } - } + // BUGBUG: The Sixel impl depends on the legacy static Application object + // BUGBUG: Disabled for now + //foreach (SixelToRender s in Application.Sixel) + //{ + // if (!string.IsNullOrWhiteSpace (s.SixelData)) + // { + // SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y); + // Console.Out.Write (s.SixelData); + // } + //} SetCursorVisibility (savedVisibility ?? CursorVisibility.Default); _cachedCursorVisibility = savedVisibility; diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsInputProcessor.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsInputProcessor.cs index 739a393fb..3777a034a 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsInputProcessor.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsInputProcessor.cs @@ -18,7 +18,7 @@ internal class WindowsInputProcessor : InputProcessorImpl } /// - public override void EnqueueMouseEvent (MouseEventArgs mouseEvent) + public override void EnqueueMouseEvent (IApplication? app, MouseEventArgs mouseEvent) { InputQueue.Enqueue (new () { diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs index 5a81ae0ab..b351696a2 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs @@ -149,7 +149,7 @@ internal partial class WindowsOutput : OutputBase, IOutput // Force 16 colors if not in virtual terminal mode. // BUGBUG: This is bad. It does not work if the app was crated without // BUGBUG: Apis. - ApplicationImpl.Instance.Force16Colors = true; + //ApplicationImpl.Instance.Force16Colors = true; } @@ -357,7 +357,8 @@ internal partial class WindowsOutput : OutputBase, IOutput { // BUGBUG: This is bad. It does not work if the app was crated without // BUGBUG: Apis. - bool force16Colors = ApplicationImpl.Instance.Force16Colors; + // bool force16Colors = ApplicationImpl.Instance.Force16Colors; + bool force16Colors = false; if (force16Colors) { diff --git a/Terminal.Gui/FileServices/IFileOperations.cs b/Terminal.Gui/FileServices/IFileOperations.cs index 610d097be..920f3e947 100644 --- a/Terminal.Gui/FileServices/IFileOperations.cs +++ b/Terminal.Gui/FileServices/IFileOperations.cs @@ -9,28 +9,31 @@ namespace Terminal.Gui.FileServices; public interface IFileOperations { /// Specifies how to handle file/directory deletion attempts in . + /// /// /// if operation was completed or if cancelled /// /// Ensure you use a try/catch block with appropriate error handling (e.g. showing a /// - bool Delete (IEnumerable toDelete); + bool Delete (IApplication? app, IEnumerable toDelete); /// Specifies how to handle 'new directory' operation in . + /// /// /// The parent directory in which the new directory should be created /// The newly created directory or null if cancelled. /// /// Ensure you use a try/catch block with appropriate error handling (e.g. showing a /// - IFileSystemInfo New (IFileSystem fileSystem, IDirectoryInfo inDirectory); + IFileSystemInfo New (IApplication? app, IFileSystem fileSystem, IDirectoryInfo inDirectory); /// Specifies how to handle file/directory rename attempts in . + /// /// /// /// The new name for the file or null if cancelled /// /// Ensure you use a try/catch block with appropriate error handling (e.g. showing a /// - IFileSystemInfo Rename (IFileSystem fileSystem, IFileSystemInfo toRename); + IFileSystemInfo Rename (IApplication? app, IFileSystem fileSystem, IFileSystemInfo toRename); } diff --git a/Terminal.Gui/Input/InputBindings.cs b/Terminal.Gui/Input/InputBindings.cs index 8711cf87c..75e2e8aea 100644 --- a/Terminal.Gui/Input/InputBindings.cs +++ b/Terminal.Gui/Input/InputBindings.cs @@ -1,19 +1,15 @@ -namespace Terminal.Gui.Input; +using System.Collections.Concurrent; + +namespace Terminal.Gui.Input; /// /// Abstract class for and . +/// This class is thread-safe for all public operations. /// /// The type of the event (e.g. or ). /// The binding type (e.g. ). -public abstract class InputBindings where TBinding : IInputBinding, new () where TEvent : notnull +public abstract class InputBindings where TBinding : IInputBinding, new() where TEvent : notnull { - /// - /// The bindings. - /// - private readonly Dictionary _bindings; - - private readonly Func _constructBinding; - /// /// Initializes a new instance. /// @@ -26,11 +22,11 @@ public abstract class InputBindings where TBinding : IInputBin } /// - /// Tests whether is valid or not. + /// The bindings. /// - /// - /// - public abstract bool IsValid (TEvent eventArgs); + private readonly ConcurrentDictionary _bindings; + + private readonly Func _constructBinding; /// Adds a bound to to the collection. /// @@ -42,24 +38,21 @@ public abstract class InputBindings where TBinding : IInputBin throw new ArgumentException (@"Invalid newEventArgs", nameof (eventArgs)); } -#pragma warning disable CS8601 // Possible null reference assignment. - if (TryGet (eventArgs, out TBinding _)) + // IMPORTANT: Add a COPY of the eventArgs. This is needed because ConfigurationManager.Apply uses DeepMemberWiseCopy + // IMPORTANT: update the memory referenced by the key, and Dictionary uses caching for performance, and thus + // IMPORTANT: Apply will update the Dictionary with the new eventArgs, but the old eventArgs will still be in the dictionary. + // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details. + if (!_bindings.TryAdd (eventArgs, binding)) { throw new InvalidOperationException (@$"A binding for {eventArgs} exists ({binding})."); } -#pragma warning restore CS8601 // Possible null reference assignment. - - // IMPORTANT: Add a COPY of the eventArgs. This is needed because ConfigurationManager.Apply uses DeepMemberWiseCopy - // IMPORTANT: update the memory referenced by the key, and Dictionary uses caching for performance, and thus - // IMPORTANT: Apply will update the Dictionary with the new eventArgs, but the old eventArgs will still be in the dictionary. - // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details. - _bindings.Add (eventArgs, binding); } /// /// Adds a new that will trigger the commands in . /// - /// If the is already bound to a different set of s it will be rebound + /// If the is already bound to a different set of s it will be + /// rebound /// . /// /// @@ -77,31 +70,32 @@ public abstract class InputBindings where TBinding : IInputBin throw new ArgumentException (@"At least one command must be specified", nameof (commands)); } - if (TryGet (eventArgs, out TBinding? binding)) + if (!IsValid (eventArgs)) + { + throw new ArgumentException (@"Invalid newEventArgs", nameof (eventArgs)); + } + + TBinding binding = _constructBinding (commands, eventArgs); + + if (!_bindings.TryAdd (eventArgs, binding)) { throw new InvalidOperationException (@$"A binding for {eventArgs} exists ({binding})."); } - - Add (eventArgs, _constructBinding (commands, eventArgs)); } - /// - /// Gets the bindings. - /// - /// - public IEnumerable> GetBindings () { return _bindings; } - /// Removes all objects from the collection. public void Clear () { _bindings.Clear (); } /// - /// Removes all bindings that trigger the given command set. Views can have multiple different + /// Removes all bindings that trigger the given command set. Views can have multiple different + /// /// bound to /// the same command sets and this method will clear all of them. /// /// public void Clear (params Command [] command) { + // ToArray() creates a snapshot to avoid modification during enumeration KeyValuePair [] kvps = _bindings .Where (kvp => kvp.Value.Commands.SequenceEqual (command)) .ToArray (); @@ -125,16 +119,29 @@ public abstract class InputBindings where TBinding : IInputBin throw new InvalidOperationException ($"{eventArgs} is not bound."); } - /// Gets the commands bound with the specified . - /// - /// The to check. - /// - /// When this method returns, contains the commands bound with the , if the is - /// not - /// found; otherwise, null. This parameter is passed uninitialized. - /// - /// if the is bound; otherwise . - public bool TryGet (TEvent eventArgs, out TBinding? binding) { return _bindings.TryGetValue (eventArgs, out binding); } + /// Gets all bound to the set of commands specified by . + /// The set of commands to search. + /// + /// The s bound to the set of commands specified by . An empty + /// list if + /// the + /// set of commands was not found. + /// + public IEnumerable GetAllFromCommands (params Command [] commands) + { + // ToList() creates a snapshot to ensure thread-safe enumeration + return _bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key).ToList (); + } + + /// + /// Gets the bindings. + /// + /// + public IEnumerable> GetBindings () + { + // ConcurrentDictionary provides a snapshot enumeration that is safe for concurrent access + return _bindings; + } /// Gets the array of s bound to if it exists. /// The to check. @@ -163,17 +170,16 @@ public abstract class InputBindings where TBinding : IInputBin /// public TEvent? GetFirstFromCommands (params Command [] commands) { return _bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; } - /// Gets all bound to the set of commands specified by . - /// The set of commands to search. - /// - /// The s bound to the set of commands specified by . An empty list if - /// the - /// set of commands was not found. - /// - public IEnumerable GetAllFromCommands (params Command [] commands) - { - return _bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key); - } + /// + /// Tests whether is valid or not. + /// + /// + /// + public abstract bool IsValid (TEvent eventArgs); + + /// Removes a from the collection. + /// + public void Remove (TEvent eventArgs) { _bindings.TryRemove (eventArgs, out _); } /// Replaces a combination already bound to a set of s. /// @@ -188,15 +194,28 @@ public abstract class InputBindings where TBinding : IInputBin throw new ArgumentException (@"Invalid newEventArgs", nameof (newEventArgs)); } - if (TryGet (oldEventArgs, out TBinding? binding)) + // Thread-safe: Handle the case where oldEventArgs == newEventArgs + if (EqualityComparer.Default.Equals (oldEventArgs, newEventArgs)) { - Remove (oldEventArgs); - Add (newEventArgs, binding!); - } - else - { - Add (newEventArgs, binding!); + // Same key - nothing to do, binding stays as-is + return; } + + // Thread-safe: Get the binding from oldEventArgs, or create default if it doesn't exist + // This is atomic - either gets existing or adds new + TBinding binding = _bindings.GetOrAdd (oldEventArgs, _ => new TBinding ()); + + // Thread-safe: Atomically add/update newEventArgs with the binding from oldEventArgs + // The updateValueFactory is only called if the key already exists, ensuring we don't + // accidentally overwrite a binding that was added by another thread + _bindings.AddOrUpdate ( + newEventArgs, + binding, // Add this binding if newEventArgs doesn't exist + (_, _) => binding); + + // Thread-safe: Remove oldEventArgs only after newEventArgs has been set + // This ensures we don't lose the binding if another thread is reading it + _bindings.TryRemove (oldEventArgs, out _); } /// Replaces the commands already bound to a combination of . @@ -209,28 +228,21 @@ public abstract class InputBindings where TBinding : IInputBin /// The set of commands to replace the old ones with. public void ReplaceCommands (TEvent eventArgs, params Command [] newCommands) { -#pragma warning disable CS8601 // Possible null reference assignment. - if (TryGet (eventArgs, out TBinding _)) - { - Remove (eventArgs); - Add (eventArgs, newCommands); - } - else - { - Add (eventArgs, newCommands); - } -#pragma warning restore CS8601 // Possible null reference assignment. + TBinding newBinding = _constructBinding (newCommands, eventArgs); + + // Thread-safe: Add or update atomically + _bindings.AddOrUpdate (eventArgs, newBinding, (_, _) => newBinding); } - /// Removes a from the collection. - /// - public void Remove (TEvent eventArgs) - { - if (!TryGet (eventArgs, out _)) - { - return; - } - - _bindings.Remove (eventArgs); - } + /// Gets the commands bound with the specified . + /// + /// The to check. + /// + /// When this method returns, contains the commands bound with the , if the + /// is + /// not + /// found; otherwise, null. This parameter is passed uninitialized. + /// + /// if the is bound; otherwise . + public bool TryGet (TEvent eventArgs, out TBinding? binding) { return _bindings.TryGetValue (eventArgs, out binding); } } diff --git a/Terminal.Gui/ViewBase/Runnable.cs b/Terminal.Gui/ViewBase/Runnable.cs index eedbc09f1..cb2fef4ca 100644 --- a/Terminal.Gui/ViewBase/Runnable.cs +++ b/Terminal.Gui/ViewBase/Runnable.cs @@ -92,7 +92,7 @@ public class Runnable : View, IRunnable /// // Or check if user wants to save first /// if (HasUnsavedChanges ()) /// { - /// int result = MessageBox.Query ("Save?", "Save changes?", "Yes", "No", "Cancel"); + /// int result = MessageBox.Query (App, "Save?", "Save changes?", "Yes", "No", "Cancel"); /// if (result == 2) return true; // Cancel stopping /// if (result == 0) Save (); /// } diff --git a/Terminal.Gui/ViewBase/RunnableWrapper.cs b/Terminal.Gui/ViewBase/RunnableWrapper.cs index 5f1d46136..6d030c600 100644 --- a/Terminal.Gui/ViewBase/RunnableWrapper.cs +++ b/Terminal.Gui/ViewBase/RunnableWrapper.cs @@ -8,7 +8,8 @@ namespace Terminal.Gui.ViewBase; /// The type of result data returned when the session completes. /// /// -/// This class enables any View to be run as a blocking session with +/// This class enables any View to be run as a blocking session with +/// /// without requiring the View to implement or derive from /// . /// diff --git a/Terminal.Gui/ViewBase/View.Diagnostics.cs b/Terminal.Gui/ViewBase/View.Diagnostics.cs index 19f77eac7..07379c5dc 100644 --- a/Terminal.Gui/ViewBase/View.Diagnostics.cs +++ b/Terminal.Gui/ViewBase/View.Diagnostics.cs @@ -2,6 +2,7 @@ public partial class View { + // TODO: Make this a configuration property /// Gets or sets whether diagnostic information will be drawn. This is a bit-field of .e diagnostics. /// /// diff --git a/Terminal.Gui/ViewBase/View.Drawing.Attribute.cs b/Terminal.Gui/ViewBase/View.Drawing.Attribute.cs index 81167c439..075b461f4 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.Attribute.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.Attribute.cs @@ -104,7 +104,7 @@ public partial class View /// /// Selects the specified Attribute - /// as the Attribute to use for subsequent calls to and . + /// as the Attribute to use for subsequent calls to and . /// /// THe Attribute to set. /// The previously set Attribute. @@ -112,7 +112,7 @@ public partial class View /// /// Selects the Attribute associated with the specified - /// as the Attribute to use for subsequent calls to and . + /// as the Attribute to use for subsequent calls to and . /// /// Calls to get the Attribute associated with the specified role, which will /// raise /. diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index 626af1852..b003d0c75 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -1,4 +1,3 @@ - #nullable disable namespace Terminal.Gui.Views; @@ -24,6 +23,9 @@ namespace Terminal.Gui.Views; /// public class Button : View, IDesignable { + private static ShadowStyle _defaultShadow = ShadowStyle.Opaque; // Resources/config.json overrides + private static MouseState _defaultHighlightStates = MouseState.In | MouseState.Pressed | MouseState.PressedOutside; // Resources/config.json overrides + private readonly Rune _leftBracket; private readonly Rune _leftDefault; private readonly Rune _rightBracket; @@ -34,13 +36,21 @@ public class Button : View, IDesignable /// Gets or sets whether s are shown with a shadow effect by default. /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static ShadowStyle DefaultShadow { get; set; } = ShadowStyle.Opaque; + public static ShadowStyle DefaultShadow + { + get => _defaultShadow; + set => _defaultShadow = value; + } /// /// Gets or sets the default Highlight Style. /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static MouseState DefaultHighlightStates { get; set; } = MouseState.In | MouseState.Pressed | MouseState.PressedOutside; + public static MouseState DefaultHighlightStates + { + get => _defaultHighlightStates; + set => _defaultHighlightStates = value; + } /// Initializes a new instance of . public Button () diff --git a/Terminal.Gui/Views/CharMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs index 370c50c6b..094b7a9ba 100644 --- a/Terminal.Gui/Views/CharMap/CharMap.cs +++ b/Terminal.Gui/Views/CharMap/CharMap.cs @@ -281,8 +281,8 @@ public class CharMap : View, IDesignable } } - private void CopyCodePoint () { Clipboard.Contents = $"U+{SelectedCodePoint:x5}"; } - private void CopyGlyph () { Clipboard.Contents = $"{new Rune (SelectedCodePoint)}"; } + private void CopyCodePoint () { App?.Clipboard?.SetClipboardData($"U+{SelectedCodePoint:x5}"); } + private void CopyGlyph () { App?.Clipboard?.SetClipboardData($"{new Rune (SelectedCodePoint)}"); } private bool? Move (ICommandContext? commandContext, int cpOffset) { @@ -335,7 +335,7 @@ public class CharMap : View, IDesignable [RequiresDynamicCode ("AOT")] private void ShowDetails () { - if (!Application.Initialized) + if (App is not { Initialized: true }) { // Some unit tests invoke Accept without Init return; @@ -380,15 +380,15 @@ public class CharMap : View, IDesignable try { decResponse = await client.GetCodepointDec (SelectedCodePoint).ConfigureAwait (false); - Application.Invoke ((_) => waitIndicator.RequestStop ()); + App?.Invoke ((_) => (s as Dialog)?.RequestStop ()); } catch (HttpRequestException e) { getCodePointError = errorLabel.Text = e.Message; - Application.Invoke ((_) => waitIndicator.RequestStop ()); + App?.Invoke ((_) => (s as Dialog)?.RequestStop ()); } }; - Application.Run (waitIndicator); + App?.Run (waitIndicator); waitIndicator.Dispose (); var name = string.Empty; @@ -521,7 +521,7 @@ public class CharMap : View, IDesignable dlg.Add (json); - Application.Run (dlg); + App?.Run (dlg); dlg.Dispose (); } diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index 2a0535469..0dfb47e51 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -1,5 +1,3 @@ - - namespace Terminal.Gui.Views; /// Shows a checkbox that can be cycled between two or three states. @@ -10,11 +8,17 @@ namespace Terminal.Gui.Views; /// public class CheckBox : View { + private static MouseState _defaultHighlightStates = MouseState.PressedOutside | MouseState.Pressed | MouseState.In; // Resources/config.json overrides + /// /// Gets or sets the default Highlight Style. /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static MouseState DefaultHighlightStates { get; set; } = MouseState.PressedOutside | MouseState.Pressed | MouseState.In; + public static MouseState DefaultHighlightStates + { + get => _defaultHighlightStates; + set => _defaultHighlightStates = value; + } /// /// Initializes a new instance of . diff --git a/Terminal.Gui/Views/CollectionNavigation/CollectionNavigator.cs b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigator.cs index c0eb7a312..ae3626fd1 100644 --- a/Terminal.Gui/Views/CollectionNavigation/CollectionNavigator.cs +++ b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigator.cs @@ -1,5 +1,5 @@ #nullable disable -using System.Collections; +using System.Collections; namespace Terminal.Gui.Views; @@ -7,6 +7,9 @@ namespace Terminal.Gui.Views; /// This implementation is based on a static of objects. internal class CollectionNavigator : CollectionNavigatorBase, IListCollectionNavigator { + private readonly object _collectionLock = new (); + private IList _collection; + /// Constructs a new CollectionNavigator. public CollectionNavigator () { } @@ -15,11 +18,39 @@ internal class CollectionNavigator : CollectionNavigatorBase, IListCollectionNav public CollectionNavigator (IList collection) { Collection = collection; } /// - public IList Collection { get; set; } + public IList Collection + { + get + { + lock (_collectionLock) + { + return _collection; + } + } + set + { + lock (_collectionLock) + { + _collection = value; + } + } + } /// - protected override object ElementAt (int idx) { return Collection [idx]; } + protected override object ElementAt (int idx) + { + lock (_collectionLock) + { + return Collection [idx]; + } + } /// - protected override int GetCollectionLength () { return Collection.Count; } + protected override int GetCollectionLength () + { + lock (_collectionLock) + { + return Collection.Count; + } + } } diff --git a/Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs index 7fd71f491..b7abad344 100644 --- a/Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs +++ b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs @@ -1,10 +1,9 @@ - - namespace Terminal.Gui.Views; /// internal abstract class CollectionNavigatorBase : ICollectionNavigator { + private readonly object _lock = new (); private DateTime _lastKeystroke = DateTime.Now; private string _searchString = ""; @@ -14,10 +13,20 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator /// public string SearchString { - get => _searchString; + get + { + lock (_lock) + { + return _searchString; + } + } private set { - _searchString = value; + lock (_lock) + { + _searchString = value; + } + OnSearchStringChanged (new (value)); } } @@ -40,15 +49,22 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator // but if we find none then we must fallback on cycling // d instead and discard the candidate state var candidateState = ""; - TimeSpan elapsedTime = DateTime.Now - _lastKeystroke; + TimeSpan elapsedTime; + string currentSearchString; + + lock (_lock) + { + elapsedTime = DateTime.Now - _lastKeystroke; + currentSearchString = _searchString; + } Logging.Debug ($"CollectionNavigator began processing '{keyStruck}', it has been {elapsedTime} since last keystroke"); // is it a second or third (etc) keystroke within a short time - if (SearchString.Length > 0 && elapsedTime < TimeSpan.FromMilliseconds (TypingDelay)) + if (currentSearchString.Length > 0 && elapsedTime < TimeSpan.FromMilliseconds (TypingDelay)) { // "dd" is a candidate - candidateState = SearchString + keyStruck; + candidateState = currentSearchString + keyStruck; Logging.Debug ($"Appending, search is now for '{candidateState}'"); } else @@ -72,7 +88,11 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator if (idxCandidate is { }) { // found "dd" so candidate search string is accepted - _lastKeystroke = DateTime.Now; + lock (_lock) + { + _lastKeystroke = DateTime.Now; + } + SearchString = candidateState; Logging.Debug ($"Found collection item that matched search:{idxCandidate}"); @@ -82,7 +102,11 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator //// nothing matches "dd" so discard it as a candidate //// and just cycle "d" instead - _lastKeystroke = DateTime.Now; + lock (_lock) + { + _lastKeystroke = DateTime.Now; + } + idxCandidate = GetNextMatchingItem (currentIndex, candidateState); Logging.Debug ($"CollectionNavigator searching (any match) matched:{idxCandidate}"); @@ -206,6 +230,10 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator private void ClearSearchString () { SearchString = ""; - _lastKeystroke = DateTime.Now; + + lock (_lock) + { + _lastKeystroke = DateTime.Now; + } } } diff --git a/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs b/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs index 400040e63..11609be67 100644 --- a/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs +++ b/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs @@ -37,7 +37,7 @@ public partial class ColorPicker { accept = true; e.Handled = true; - Application.RequestStop (); + (s as View)?.App?.RequestStop (); }; var btnCancel = new Button @@ -51,7 +51,7 @@ public partial class ColorPicker btnCancel.Accepting += (s, e) => { e.Handled = true; - Application.RequestStop (); + (s as View)?.App ?.RequestStop (); }; d.Add (btnOk); diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index 1a5b4b362..03797368d 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -1,4 +1,3 @@ - namespace Terminal.Gui.Views; /// @@ -11,10 +10,17 @@ namespace Terminal.Gui.Views; /// . This will execute the dialog until /// it terminates via the (`Esc` by default), /// or when one of the views or buttons added to the dialog calls -/// . +/// . /// public class Dialog : Window { + private static LineStyle _defaultBorderStyle = LineStyle.Heavy; // Resources/config.json overrides + private static Alignment _defaultButtonAlignment = Alignment.End; // Resources/config.json overrides + private static AlignmentModes _defaultButtonAlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems; // Resources/config.json overrides + private static int _defaultMinimumHeight = 80; // Resources/config.json overrides + private static int _defaultMinimumWidth = 80; // Resources/config.json overrides + private static ShadowStyle _defaultShadow = ShadowStyle.Transparent; // Resources/config.json overrides + /// /// Initializes a new instance of the class with no s. /// @@ -107,37 +113,61 @@ public class Dialog : Window /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public new static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Heavy; + public new static LineStyle DefaultBorderStyle + { + get => _defaultBorderStyle; + set => _defaultBorderStyle = value; + } /// The default for . /// This property can be set in a Theme. [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Alignment DefaultButtonAlignment { get; set; } = Alignment.End; + public static Alignment DefaultButtonAlignment + { + get => _defaultButtonAlignment; + set => _defaultButtonAlignment = value; + } /// The default for . /// This property can be set in a Theme. [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static AlignmentModes DefaultButtonAlignmentModes { get; set; } = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems; + public static AlignmentModes DefaultButtonAlignmentModes + { + get => _defaultButtonAlignmentModes; + set => _defaultButtonAlignmentModes = value; + } /// /// Defines the default minimum Dialog height, as a percentage of the container width. Can be configured via /// . /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static int DefaultMinimumHeight { get; set; } = 80; + public static int DefaultMinimumHeight + { + get => _defaultMinimumHeight; + set => _defaultMinimumHeight = value; + } /// /// Defines the default minimum Dialog width, as a percentage of the container width. Can be configured via /// . /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static int DefaultMinimumWidth { get; set; } = 80; + public static int DefaultMinimumWidth + { + get => _defaultMinimumWidth; + set => _defaultMinimumWidth = value; + } /// /// Gets or sets whether all s are shown with a shadow effect by default. /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public new static ShadowStyle DefaultShadow { get; set; } = ShadowStyle.Transparent; + public new static ShadowStyle DefaultShadow + { + get => _defaultShadow; + set => _defaultShadow = value; + } // Dialogs are Modal and Focus is indicated by their Border. The following code ensures the diff --git a/Terminal.Gui/Views/FileDialogs/DefaultFileOperations.cs b/Terminal.Gui/Views/FileDialogs/DefaultFileOperations.cs index 467e8d74c..25214cf17 100644 --- a/Terminal.Gui/Views/FileDialogs/DefaultFileOperations.cs +++ b/Terminal.Gui/Views/FileDialogs/DefaultFileOperations.cs @@ -7,7 +7,7 @@ namespace Terminal.Gui.Views; public class DefaultFileOperations : IFileOperations { /// - public bool Delete (IEnumerable toDelete) + public bool Delete (IApplication app, IEnumerable toDelete) { // Default implementation does not allow deleting multiple files if (toDelete.Count () != 1) @@ -18,7 +18,7 @@ public class DefaultFileOperations : IFileOperations IFileSystemInfo d = toDelete.Single (); string adjective = d.Name; - int result = MessageBox.Query ( + int? result = MessageBox.Query (app, string.Format (Strings.fdDeleteTitle, adjective), string.Format (Strings.fdDeleteBody, adjective), Strings.btnYes, @@ -43,14 +43,14 @@ public class DefaultFileOperations : IFileOperations } catch (Exception ex) { - MessageBox.ErrorQuery (Strings.fdDeleteFailedTitle, ex.Message, Strings.btnOk); + MessageBox.ErrorQuery (app, Strings.fdDeleteFailedTitle, ex.Message, Strings.btnOk); } return false; } /// - public IFileSystemInfo Rename (IFileSystem fileSystem, IFileSystemInfo toRename) + public IFileSystemInfo Rename (IApplication app, IFileSystem fileSystem, IFileSystemInfo toRename) { // Don't allow renaming C: or D: or / (on linux) etc if (toRename is IDirectoryInfo dir && dir.Parent is null) @@ -95,7 +95,7 @@ public class DefaultFileOperations : IFileOperations } catch (Exception ex) { - MessageBox.ErrorQuery (Strings.fdRenameFailedTitle, ex.Message, "Ok"); + MessageBox.ErrorQuery (app, Strings.fdRenameFailedTitle, ex.Message, "Ok"); } } } @@ -104,7 +104,7 @@ public class DefaultFileOperations : IFileOperations } /// - public IFileSystemInfo New (IFileSystem fileSystem, IDirectoryInfo inDirectory) + public IFileSystemInfo New (IApplication app, IFileSystem fileSystem, IDirectoryInfo inDirectory) { if (Prompt (Strings.fdNewTitle, "", out string named)) { @@ -122,7 +122,7 @@ public class DefaultFileOperations : IFileOperations } catch (Exception ex) { - MessageBox.ErrorQuery (Strings.fdNewFailed, ex.Message, "Ok"); + MessageBox.ErrorQuery (app, Strings.fdNewFailed, ex.Message, "Ok"); } } } @@ -138,7 +138,7 @@ public class DefaultFileOperations : IFileOperations btnOk.Accepting += (s, e) => { confirm = true; - Application.RequestStop (); + (s as View)?.App?.RequestStop (); // When Accepting is handled, set e.Handled to true to prevent further processing. e.Handled = true; }; @@ -147,7 +147,7 @@ public class DefaultFileOperations : IFileOperations btnCancel.Accepting += (s, e) => { confirm = false; - Application.RequestStop (); + (s as View)?.App?.RequestStop (); // When Accepting is handled, set e.Handled to true to prevent further processing. e.Handled = true; }; diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.cs b/Terminal.Gui/Views/FileDialogs/FileDialog.cs index a24c82f0a..b0dfeb9d6 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialog.cs @@ -108,7 +108,7 @@ public class FileDialog : Dialog, IDesignable if (Modal) { - Application.RequestStop (); + (s as View)?.App?.RequestStop (); } }; @@ -468,7 +468,6 @@ public class FileDialog : Dialog, IDesignable Style.IconProvider.IsOpenGetter = _treeView.IsExpanded; _treeView.AddObjects (_treeRoots.Keys); -#if MENU_V1 // if filtering on file type is configured then create the ComboBox and establish // initial filtering by extension(s) @@ -479,6 +478,7 @@ public class FileDialog : Dialog, IDesignable // Fiddle factor int width = AllowedTypes.Max (a => a.ToString ()!.Length) + 6; +#if MENU_V1 _allowedTypeMenu = new ( "", _allowedTypeMenuItems = AllowedTypes.Select ( @@ -512,8 +512,8 @@ public class FileDialog : Dialog, IDesignable }; Add (_allowedTypeMenuBar); - } #endif + } // if no path has been provided if (_tbPath.Text.Length <= 0) @@ -849,7 +849,7 @@ public class FileDialog : Dialog, IDesignable { IFileSystemInfo [] toDelete = GetFocusedFiles ()!; - if (FileOperationsHandler.Delete (toDelete)) + if (FileOperationsHandler.Delete (App, toDelete)) { RefreshState (); } @@ -879,7 +879,7 @@ public class FileDialog : Dialog, IDesignable if (Modal) { - Application.RequestStop (); + App?.RequestStop (); } } @@ -1039,7 +1039,7 @@ public class FileDialog : Dialog, IDesignable private void New () { { - IFileSystemInfo created = FileOperationsHandler.New (_fileSystem!, State!.Directory); + IFileSystemInfo created = FileOperationsHandler.New (App, _fileSystem!, State!.Directory); if (created is { }) { @@ -1174,13 +1174,13 @@ public class FileDialog : Dialog, IDesignable PushState (State, false, false, false); } - private void Rename () + private void Rename (IApplication? app) { IFileSystemInfo [] toRename = GetFocusedFiles ()!; if (toRename?.Length == 1) { - IFileSystemInfo newNamed = FileOperationsHandler.Rename (_fileSystem!, toRename.Single ()); + IFileSystemInfo newNamed = FileOperationsHandler.Rename (app, _fileSystem!, toRename.Single ()); if (newNamed is { }) { @@ -1230,7 +1230,7 @@ public class FileDialog : Dialog, IDesignable PopoverMenu? contextMenu = new ( [ new (Strings.fdCtxNew, string.Empty, New), - new (Strings.fdCtxRename, string.Empty, Rename), + new (Strings.fdCtxRename, string.Empty, () => Rename (App)), new (Strings.fdCtxDelete, string.Empty, Delete) ]); @@ -1327,7 +1327,7 @@ public class FileDialog : Dialog, IDesignable if (keyEvent.KeyCode == (KeyCode.CtrlMask | KeyCode.R)) { - Rename (); + Rename (App); return true; } diff --git a/Terminal.Gui/Views/FrameView.cs b/Terminal.Gui/Views/FrameView.cs index cd99731b9..db2c0c2df 100644 --- a/Terminal.Gui/Views/FrameView.cs +++ b/Terminal.Gui/Views/FrameView.cs @@ -1,5 +1,3 @@ - - namespace Terminal.Gui.Views; // TODO: FrameView is mis-named, really. It's far more about it being a TabGroup than a frame. @@ -19,6 +17,8 @@ namespace Terminal.Gui.Views; /// public class FrameView : View { + private static LineStyle _defaultBorderStyle = LineStyle.Rounded; // Resources/config.json overrides + /// /// Initializes a new instance of the class. /// layout. @@ -31,13 +31,17 @@ public class FrameView : View } /// - /// The default for 's border. The default is - /// . + /// Defines the default border styling for . Can be configured via + /// . /// /// /// This property can be set in a Theme to change the default for all /// s. /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Rounded; + public static LineStyle DefaultBorderStyle + { + get => _defaultBorderStyle; + set => _defaultBorderStyle = value; + } } diff --git a/Terminal.Gui/Views/GraphView/GraphView.cs b/Terminal.Gui/Views/GraphView/GraphView.cs index 69f708da8..03cda9fed 100644 --- a/Terminal.Gui/Views/GraphView/GraphView.cs +++ b/Terminal.Gui/Views/GraphView/GraphView.cs @@ -8,7 +8,6 @@ public class GraphView : View, IDesignable /// Creates a new graph with a 1 to 1 graph space with absolute layout. public GraphView () { - App = ApplicationImpl.Instance; CanFocus = true; AxisX = new (); diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index 67312b01e..7e45d2d6f 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -591,7 +591,7 @@ public class MenuBar : Menu, IDesignable { Title = "_File Settings...", HelpText = "More file settings", - Action = () => MessageBox.Query ( + Action = () => MessageBox.Query (App, "File Settings", "This is the File Settings Dialog\n", "_Ok", @@ -665,12 +665,12 @@ public class MenuBar : Menu, IDesignable new MenuItem { Title = "_Online Help...", - Action = () => MessageBox.Query ("Online Help", "https://gui-cs.github.io/Terminal.Gui", "Ok") + Action = () => MessageBox.Query (App, "Online Help", "https://gui-cs.github.io/Terminal.Gui", "Ok") }, new MenuItem { Title = "About...", - Action = () => MessageBox.Query ("About", "Something About Mary.", "Ok") + Action = () => MessageBox.Query (App, "About", "Something About Mary.", "Ok") } ] ) @@ -734,7 +734,7 @@ public class MenuBar : Menu, IDesignable { Title = "_Deeper Detail", Text = "Deeper Detail", - Action = () => { MessageBox.Query ("Deeper Detail", "Lots of details", "_Ok"); } + Action = () => { MessageBox.Query (App, "Deeper Detail", "Lots of details", "_Ok"); } }; var belowLineDetail = new MenuItem diff --git a/Terminal.Gui/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs index bdbf323a0..e9e874285 100644 --- a/Terminal.Gui/Views/MessageBox.cs +++ b/Terminal.Gui/Views/MessageBox.cs @@ -1,112 +1,205 @@ -#nullable disable - namespace Terminal.Gui.Views; /// -/// MessageBox displays a modal message to the user, with a title, a message and a series of options that the user -/// can choose from. +/// Displays a modal message box with a title, message, and buttons. Returns the index of the selected button, +/// or if the user cancels with . /// -/// -/// The difference between the and -/// method is the default set of colors used for the message box. -/// -/// -/// The following example pops up a with the specified title and text, plus two -/// s. The value -1 is returned when the user cancels the by pressing the -/// ESC key. -/// -/// -/// -/// var n = MessageBox.Query ("Quit Demo", "Are you sure you want to quit this demo?", "Yes", "No"); -/// if (n == 0) -/// quit = true; -/// else -/// quit = false; -/// -/// +/// +/// +/// MessageBox provides static methods for displaying modal dialogs with customizable buttons and messages. +/// All methods return where the value is the 0-based index of the button pressed, +/// or if the user pressed (typically Esc). +/// +/// +/// uses the default Dialog color scheme. +/// uses the Error color scheme. +/// +/// +/// Important: All MessageBox methods require an instance to be passed. +/// This enables proper modal dialog management and respects the application's lifecycle. Pass your +/// application instance (from ) or use the legacy +/// if using the static Application pattern. +/// +/// +/// Example using instance-based pattern: +/// +/// IApplication app = Application.Create(); +/// app.Init(); +/// +/// int? result = MessageBox.Query(app, "Quit Demo", "Are you sure you want to quit?", "Yes", "No"); +/// if (result == 0) // User clicked "Yes" +/// app.RequestStop(); +/// else if (result == null) // User pressed Esc +/// // Handle cancellation +/// +/// app.Shutdown(); +/// +/// +/// +/// Example using legacy static pattern: +/// +/// Application.Init(); +/// +/// int? result = MessageBox.Query(ApplicationImpl.Instance, "Quit Demo", "Are you sure?", "Yes", "No"); +/// if (result == 0) // User clicked "Yes" +/// Application.RequestStop(); +/// +/// Application.Shutdown(); +/// +/// +/// +/// The property provides a global variable alternative for web-based consoles +/// without SynchronizationContext. However, using the return value is preferred as it's more thread-safe +/// and follows modern async patterns. +/// +/// public static class MessageBox { + private static LineStyle _defaultBorderStyle = LineStyle.Heavy; // Resources/config.json overrides + private static Alignment _defaultButtonAlignment = Alignment.Center; // Resources/config.json overrides + private static int _defaultMinimumWidth = 0; // Resources/config.json overrides + private static int _defaultMinimumHeight = 0; // Resources/config.json overrides + /// /// Defines the default border styling for . Can be configured via /// . /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Heavy; + public static LineStyle DefaultBorderStyle + { + get => _defaultBorderStyle; + set => _defaultBorderStyle = value; + } /// The default for . /// This property can be set in a Theme. [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Alignment DefaultButtonAlignment { get; set; } = Alignment.Center; + public static Alignment DefaultButtonAlignment + { + get => _defaultButtonAlignment; + set => _defaultButtonAlignment = value; + } /// /// Defines the default minimum MessageBox width, as a percentage of the screen width. Can be configured via /// . /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static int DefaultMinimumWidth { get; set; } = 0; + public static int DefaultMinimumWidth + { + get => _defaultMinimumWidth; + set => _defaultMinimumWidth = value; + } /// /// Defines the default minimum Dialog height, as a percentage of the screen width. Can be configured via /// . /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static int DefaultMinimumHeight { get; set; } = 0; - /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. This is useful for web - /// based console where there is no SynchronizationContext or TaskScheduler. - /// - /// - /// Warning: This is a global variable and should be used with caution. It is not thread safe. - /// - public static int Clicked { get; private set; } = -1; + public static int DefaultMinimumHeight + { + get => _defaultMinimumHeight; + set => _defaultMinimumHeight = value; + } /// - /// Presents an error with the specified title and message and a list of buttons. + /// The index of the selected button, or if the user pressed . /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. + /// + /// This global variable is useful for web-based consoles without a SynchronizationContext or TaskScheduler. + /// Warning: Not thread-safe. + /// + public static int? Clicked { get; private set; } + + /// + /// Displays an error with fixed dimensions. + /// + /// The application instance. If , uses . /// Width for the MessageBox. /// Height for the MessageBox. /// Title for the MessageBox. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Array of buttons to add. + /// Message to display. May contain multiple lines and will be word-wrapped. + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . /// - /// Use instead; it automatically sizes the MessageBox based on - /// the contents. + /// Consider using which automatically sizes the + /// MessageBox. /// - public static int ErrorQuery (int width, int height, string title, string message, params string [] buttons) + public static int? ErrorQuery ( + IApplication? app, + int width, + int height, + string title, + string message, + params string [] buttons + ) { - return QueryFull (true, width, height, title, message, 0, true, buttons); + return QueryFull ( + app, + true, + width, + height, + title, + message, + 0, + true, + buttons); } /// - /// Presents an error with the specified title and message and a list of buttons to show - /// to the user. + /// Displays an auto-sized error . /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. - /// Title for the query. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Array of buttons to add. + /// The application instance. If , uses . + /// Title for the MessageBox. + /// Message to display. May contain multiple lines and will be word-wrapped. + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . /// - /// The message box will be vertically and horizontally centered in the container and the size will be - /// automatically determined from the size of the title, message. and buttons. + /// The MessageBox is centered and auto-sized based on title, message, and buttons. /// - public static int ErrorQuery (string title, string message, params string [] buttons) { return QueryFull (true, 0, 0, title, message, 0, true, buttons); } + public static int? ErrorQuery (IApplication? app, string title, string message, params string [] buttons) + { + return QueryFull ( + app, + true, + 0, + 0, + title, + message, + 0, + true, + buttons); + } /// - /// Presents an error with the specified title and message and a list of buttons. + /// Displays an error with fixed dimensions and a default button. /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. + /// The application instance. If , uses . /// Width for the MessageBox. /// Height for the MessageBox. /// Title for the MessageBox. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Index of the default button. - /// Array of buttons to add. + /// Message to display. May contain multiple lines and will be word-wrapped. + /// Index of the default button (0-based). + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . /// - /// Use instead; it automatically sizes the MessageBox based on - /// the contents. + /// Consider using which automatically sizes the + /// MessageBox. /// - public static int ErrorQuery ( + public static int? ErrorQuery ( + IApplication? app, int width, int height, string title, @@ -115,184 +208,73 @@ public static class MessageBox params string [] buttons ) { - return QueryFull (true, width, height, title, message, defaultButton, true, buttons); + return QueryFull ( + app, + true, + width, + height, + title, + message, + defaultButton, + true, + buttons); } /// - /// Presents an error with the specified title and message and a list of buttons to show - /// to the user. + /// Displays an auto-sized error with a default button. /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. + /// The application instance. If , uses . /// Title for the MessageBox. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Index of the default button. - /// Array of buttons to add. + /// Message to display. May contain multiple lines and will be word-wrapped. + /// Index of the default button (0-based). + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . /// - /// The message box will be vertically and horizontally centered in the container and the size will be - /// automatically determined from the size of the title, message. and buttons. + /// The MessageBox is centered and auto-sized based on title, message, and buttons. /// - public static int ErrorQuery (string title, string message, int defaultButton = 0, params string [] buttons) + public static int? ErrorQuery (IApplication? app, string title, string message, int defaultButton = 0, params string [] buttons) { - return QueryFull (true, 0, 0, title, message, defaultButton, true, buttons); + return QueryFull ( + app, + true, + 0, + 0, + title, + message, + defaultButton, + true, + buttons); } /// - /// Presents an error with the specified title and message and a list of buttons to show - /// to the user. + /// Displays an error with fixed dimensions, a default button, and word-wrap control. /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. - /// Width for the window. - /// Height for the window. - /// Title for the query. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Index of the default button. - /// If wrap the message or not. - /// Array of buttons to add. - /// - /// Use instead; it automatically sizes the MessageBox based on - /// the contents. - /// - public static int ErrorQuery ( - int width, - int height, - string title, - string message, - int defaultButton = 0, - bool wrapMessage = true, - params string [] buttons - ) - { - return QueryFull (true, width, height, title, message, defaultButton, wrapMessage, buttons); - } - - /// - /// Presents an error with the specified title and message and a list of buttons to show - /// to the user. - /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. - /// Title for the query. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Index of the default button. - /// If wrap the message or not. The default is - /// Array of buttons to add. - /// - /// The message box will be vertically and horizontally centered in the container and the size will be - /// automatically determined from the size of the title, message. and buttons. - /// - public static int ErrorQuery ( - string title, - string message, - int defaultButton = 0, - bool wrapMessage = true, - params string [] buttons - ) - { - return QueryFull (true, 0, 0, title, message, defaultButton, wrapMessage, buttons); - } - - /// - /// Presents a with the specified title and message and a list of buttons. - /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. + /// The application instance. If , uses . /// Width for the MessageBox. /// Height for the MessageBox. /// Title for the MessageBox. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Array of buttons to add. + /// Message to display. May contain multiple lines. + /// Index of the default button (0-based). + /// + /// If , word-wraps the message; otherwise displays as-is with multi-line + /// support. + /// + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . /// - /// Use instead; it automatically sizes the MessageBox based on - /// the contents. + /// Consider using which automatically + /// sizes the MessageBox. /// - public static int Query (int width, int height, string title, string message, params string [] buttons) - { - return QueryFull (false, width, height, title, message, 0, true, buttons); - } - - /// - /// Presents a with the specified title and message and a list of buttons. - /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. - /// Title for the MessageBox. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Array of buttons to add. - /// - /// - /// The message box will be vertically and horizontally centered in the container and the size will be - /// automatically determined from the size of the title, message. and buttons. - /// - /// - /// Use instead; it automatically sizes the MessageBox based on - /// the contents. - /// - /// - public static int Query (string title, string message, params string [] buttons) { return QueryFull (false, 0, 0, title, message, 0, true, buttons); } - - /// - /// Presents a with the specified title and message and a list of buttons. - /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. - /// Width for the window. - /// Height for the window. - /// Title for the MessageBox. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Index of the default button. - /// Array of buttons to add. - /// - /// - /// The message box will be vertically and horizontally centered in the container and the size will be - /// automatically determined from the size of the title, message. and buttons. - /// - /// - /// Use instead; it automatically sizes the MessageBox based on - /// the contents. - /// - /// - public static int Query ( - int width, - int height, - string title, - string message, - int defaultButton = 0, - params string [] buttons - ) - { - return QueryFull (false, width, height, title, message, defaultButton, true, buttons); - } - - /// - /// Presents a with the specified title and message and a list of buttons. - /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. - /// Title for the MessageBox. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Index of the default button. - /// Array of buttons to add. - /// - /// The message box will be vertically and horizontally centered in the container and the size will be - /// automatically determined from the size of the message and buttons. - /// - public static int Query (string title, string message, int defaultButton = 0, params string [] buttons) - { - return QueryFull (false, 0, 0, title, message, defaultButton, true, buttons); - } - - /// - /// Presents a with the specified title and message and a list of buttons to show - /// to the user. - /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. - /// Width for the window. - /// Height for the window. - /// Title for the query. - /// Message to display, might contain multiple lines. - /// Index of the default button. - /// If wrap the message or not. - /// Array of buttons to add. - /// - /// Use instead; it automatically sizes the MessageBox based on the - /// contents. - /// - public static int Query ( + public static int? ErrorQuery ( + IApplication? app, int width, int height, string title, @@ -302,20 +284,40 @@ public static class MessageBox params string [] buttons ) { - return QueryFull (false, width, height, title, message, defaultButton, wrapMessage, buttons); + return QueryFull ( + app, + true, + width, + height, + title, + message, + defaultButton, + wrapMessage, + buttons); } /// - /// Presents a with the specified title and message and a list of buttons to show - /// to the user. + /// Displays an auto-sized error with a default button and word-wrap control. /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. - /// Title for the query. - /// Message to display, might contain multiple lines. - /// Index of the default button. - /// If wrap the message or not. - /// Array of buttons to add. - public static int Query ( + /// The application instance. If , uses . + /// Title for the MessageBox. + /// Message to display. May contain multiple lines. + /// Index of the default button (0-based). + /// + /// If , word-wraps the message; otherwise displays as-is with multi-line + /// support. + /// + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . + /// + /// The MessageBox is centered and auto-sized based on title, message, and buttons. + /// + public static int? ErrorQuery ( + IApplication? app, string title, string message, int defaultButton = 0, @@ -323,10 +325,239 @@ public static class MessageBox params string [] buttons ) { - return QueryFull (false, 0, 0, title, message, defaultButton, wrapMessage, buttons); + return QueryFull ( + app, + true, + 0, + 0, + title, + message, + defaultButton, + wrapMessage, + buttons); } - private static int QueryFull ( + /// + /// Displays a with fixed dimensions. + /// + /// The application instance. If , uses . + /// Width for the MessageBox. + /// Height for the MessageBox. + /// Title for the MessageBox. + /// Message to display. May contain multiple lines and will be word-wrapped. + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . + /// + /// Consider using which automatically sizes the + /// MessageBox. + /// + public static int? Query (IApplication? app, int width, int height, string title, string message, params string [] buttons) + { + return QueryFull ( + app, + false, + width, + height, + title, + message, + 0, + true, + buttons); + } + + /// + /// Displays an auto-sized . + /// + /// The application instance. If , uses . + /// Title for the MessageBox. + /// Message to display. May contain multiple lines and will be word-wrapped. + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . + /// + /// The MessageBox is centered and auto-sized based on title, message, and buttons. + /// + public static int? Query (IApplication? app, string title, string message, params string [] buttons) + { + return QueryFull ( + app, + false, + 0, + 0, + title, + message, + 0, + true, + buttons); + } + + /// + /// Displays a with fixed dimensions and a default button. + /// + /// The application instance. If , uses . + /// Width for the MessageBox. + /// Height for the MessageBox. + /// Title for the MessageBox. + /// Message to display. May contain multiple lines and will be word-wrapped. + /// Index of the default button (0-based). + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . + /// + /// Consider using which automatically sizes the + /// MessageBox. + /// + public static int? Query ( + IApplication? app, + int width, + int height, + string title, + string message, + int defaultButton = 0, + params string [] buttons + ) + { + return QueryFull ( + app, + false, + width, + height, + title, + message, + defaultButton, + true, + buttons); + } + + /// + /// Displays an auto-sized with a default button. + /// + /// The application instance. If , uses . + /// Title for the MessageBox. + /// Message to display. May contain multiple lines and will be word-wrapped. + /// Index of the default button (0-based). + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . + /// + /// The MessageBox is centered and auto-sized based on title, message, and buttons. + /// + public static int? Query (IApplication? app, string title, string message, int defaultButton = 0, params string [] buttons) + { + return QueryFull ( + app, + false, + 0, + 0, + title, + message, + defaultButton, + true, + buttons); + } + + /// + /// Displays a with fixed dimensions, a default button, and word-wrap control. + /// + /// The application instance. If , uses . + /// Width for the MessageBox. + /// Height for the MessageBox. + /// Title for the MessageBox. + /// Message to display. May contain multiple lines. + /// Index of the default button (0-based). + /// + /// If , word-wraps the message; otherwise displays as-is with multi-line + /// support. + /// + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . + /// + /// Consider using which automatically sizes + /// the MessageBox. + /// + public static int? Query ( + IApplication? app, + int width, + int height, + string title, + string message, + int defaultButton = 0, + bool wrapMessage = true, + params string [] buttons + ) + { + return QueryFull ( + app, + false, + width, + height, + title, + message, + defaultButton, + wrapMessage, + buttons); + } + + /// + /// Displays an auto-sized with a default button and word-wrap control. + /// + /// The application instance. If , uses . + /// Title for the MessageBox. + /// Message to display. May contain multiple lines. + /// Index of the default button (0-based). + /// + /// If , word-wraps the message; otherwise displays as-is with multi-line + /// support. + /// + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . + /// + /// The MessageBox is centered and auto-sized based on title, message, and buttons. + /// + public static int? Query ( + IApplication? app, + string title, + string message, + int defaultButton = 0, + bool wrapMessage = true, + params string [] buttons + ) + { + return QueryFull ( + app, + false, + 0, + 0, + title, + message, + defaultButton, + wrapMessage, + buttons); + } + + private static int? QueryFull ( + IApplication? app, bool useErrorColors, int width, int height, @@ -337,10 +568,12 @@ public static class MessageBox params string [] buttons ) { + ArgumentNullException.ThrowIfNull (app); + // Create button array for Dialog var count = 0; List public class StatusBar : Bar, IDesignable { + private static LineStyle _defaultSeparatorLineStyle = LineStyle.Single; // Resources/config.json overrides + /// public StatusBar () : this ([]) { } @@ -55,7 +57,11 @@ public class StatusBar : Bar, IDesignable /// Gets or sets the default Line Style for the separators between the shortcuts of the StatusBar. /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultSeparatorLineStyle { get; set; } = LineStyle.Single; + public static LineStyle DefaultSeparatorLineStyle + { + get => _defaultSeparatorLineStyle; + set => _defaultSeparatorLineStyle = value; + } /// protected override void OnSubViewLayout (LayoutEventArgs args) @@ -160,7 +166,7 @@ public class StatusBar : Bar, IDesignable return true; - void OnButtonClicked (object? sender, EventArgs? e) { MessageBox.Query ("Hi", $"You clicked {sender}"); } + void OnButtonClicked (object? sender, EventArgs? e) { MessageBox.Query (App, "Hi", $"You clicked {sender}"); } } /// diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index 1c5f298e0..5d1e79f7f 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -1534,7 +1534,7 @@ public class TableView : View, IDesignable /// private void ClearLine (int row, int width) { - if (Application.Screen.Height == 0) + if (App?.Screen.Height == 0) { return; } @@ -1810,7 +1810,7 @@ public class TableView : View, IDesignable } } - if (Application.Screen.Height > 0) + if (App?.Screen.Height > 0) { AddRuneAt (c, row, rune); } diff --git a/Terminal.Gui/Views/TextInput/TextField.cs b/Terminal.Gui/Views/TextInput/TextField.cs index c4aa19754..794b09b40 100644 --- a/Terminal.Gui/Views/TextInput/TextField.cs +++ b/Terminal.Gui/Views/TextInput/TextField.cs @@ -617,7 +617,7 @@ public class TextField : View, IDesignable return; } - Clipboard.Contents = SelectedText; + App?.Clipboard?.SetClipboardData (SelectedText); } /// Cut the selected text to the clipboard. @@ -628,7 +628,7 @@ public class TextField : View, IDesignable return; } - Clipboard.Contents = SelectedText; + App?.Clipboard?.SetClipboardData (SelectedText); List newText = DeleteSelectedText (); Text = StringExtensions.ToString (newText); Adjust (); @@ -1079,7 +1079,7 @@ public class TextField : View, IDesignable return; } - string cbTxt = Clipboard.Contents.Split ("\n") [0] ?? ""; + string cbTxt = App?.Clipboard?.GetClipboardData ()?.Split ("\n") [0]; if (string.IsNullOrEmpty (cbTxt)) { @@ -1731,9 +1731,9 @@ public class TextField : View, IDesignable private void SetClipboard (IEnumerable text) { - if (!Secret) + if (!Secret && App?.Clipboard is { }) { - Clipboard.Contents = StringExtensions.ToString (text.ToList ()); + App.Clipboard.SetClipboardData (StringExtensions.ToString (text.ToList ())); } } diff --git a/Terminal.Gui/Views/TextInput/TextView.cs b/Terminal.Gui/Views/TextInput/TextView.cs index 5e2124d38..23fd1e89a 100644 --- a/Terminal.Gui/Views/TextInput/TextView.cs +++ b/Terminal.Gui/Views/TextInput/TextView.cs @@ -1960,7 +1960,7 @@ public class TextView : View, IDesignable } SetWrapModel (); - string? contents = Clipboard.Contents; + string? contents = App?.Clipboard?.GetClipboardData (); if (_copyWithoutSelection && contents!.FirstOrDefault (x => x is '\n' or '\r') == 0) { @@ -2363,7 +2363,7 @@ public class TextView : View, IDesignable OnUnwrappedCursorPosition (); } - private void AppendClipboard (string text) { Clipboard.Contents += text; } + private void AppendClipboard (string text) { App?.Clipboard?.SetClipboardData (App?.Clipboard?.GetClipboardData () + text); } private PopoverMenu CreateContextMenu () { @@ -3842,7 +3842,7 @@ public class TextView : View, IDesignable List currentLine = GetCurrentLine (); - if (currentLine.Count > 0 && currentLine[CurrentColumn - 1].Grapheme == "\t") + if (currentLine.Count > 0 && currentLine [CurrentColumn - 1].Grapheme == "\t") { _historyText.Add (new () { new (currentLine) }, CursorPosition); @@ -4470,7 +4470,7 @@ public class TextView : View, IDesignable { if (text is { }) { - Clipboard.Contents = text; + App?.Clipboard?.SetClipboardData (text); } } diff --git a/Terminal.Gui/Views/Window.cs b/Terminal.Gui/Views/Window.cs index b374459a2..597ba121d 100644 --- a/Terminal.Gui/Views/Window.cs +++ b/Terminal.Gui/Views/Window.cs @@ -1,5 +1,3 @@ - - namespace Terminal.Gui.Views; /// @@ -18,6 +16,9 @@ namespace Terminal.Gui.Views; /// public class Window : Toplevel { + private static ShadowStyle _defaultShadow = ShadowStyle.None; // Resources/config.json overrides + private static LineStyle _defaultBorderStyle = LineStyle.Single; // Resources/config.json overrides + /// /// Initializes a new instance of the class. /// @@ -35,7 +36,11 @@ public class Window : Toplevel /// Gets or sets whether all s are shown with a shadow effect by default. /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static ShadowStyle DefaultShadow { get; set; } = ShadowStyle.None; + public static ShadowStyle DefaultShadow + { + get => _defaultShadow; + set => _defaultShadow = value; + } // TODO: enable this ///// @@ -56,5 +61,9 @@ public class Window : Toplevel /// s. /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single; + public static LineStyle DefaultBorderStyle + { + get => _defaultBorderStyle; + set => _defaultBorderStyle = value; + } } diff --git a/Terminal.Gui/Views/Wizard/Wizard.cs b/Terminal.Gui/Views/Wizard/Wizard.cs index 50b21c7ed..3415c572a 100644 --- a/Terminal.Gui/Views/Wizard/Wizard.cs +++ b/Terminal.Gui/Views/Wizard/Wizard.cs @@ -458,7 +458,7 @@ public class Wizard : Dialog if (IsCurrentTop) { - Application.RequestStop (this); + (sender as View)?.App?.RequestStop (this); e.Handled = true; } diff --git a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs index 6cc94ec45..50aba981c 100644 --- a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs @@ -60,10 +60,10 @@ public class FileDialogFluentTests public void CancelFileDialog_QuitKey_Quits (TestDriver d) { SaveDialog? sd = null; - using var c = With.A (() => NewSaveDialog (out sd), 100, 20, d) - .ScreenShot ("Save dialog", _out) - .EnqueueKeyEvent (Application.QuitKey) - .AssertTrue (sd!.Canceled); + using GuiTestContext c = With.A (() => NewSaveDialog (out sd), 100, 20, d, logWriter: _out) + .ScreenShot ("Save dialog", _out) + .EnqueueKeyEvent (Application.QuitKey) + .AssertTrue (sd!.Canceled); } [Theory] @@ -93,7 +93,7 @@ public class FileDialogFluentTests public void CancelFileDialog_UsingCancelButton_AltC (TestDriver d) { SaveDialog? sd = null; - using var c = With.A (() => NewSaveDialog (out sd), 100, 20, d) + using var c = With.A (() => NewSaveDialog (out sd), 100, 20, d, _out) .ScreenShot ("Save dialog", _out) .EnqueueKeyEvent (Key.C.WithAlt) .AssertTrue (sd!.Canceled); @@ -132,12 +132,13 @@ public class FileDialogFluentTests { SaveDialog? sd = null; MockFileSystem? fs = null; - using var c = With.A (() => NewSaveDialog (out sd, out fs, modal: false), 100, 20, d) - .ScreenShot ("Save dialog", _out) - .Focus