diff --git a/.github/workflows/api-docs.yml b/.github/workflows/api-docs.yml index e0d5def02..145aedc0d 100644 --- a/.github/workflows/api-docs.yml +++ b/.github/workflows/api-docs.yml @@ -10,10 +10,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup .NET Core - uses: actions/setup-dotnet@v3.0.1 + uses: actions/setup-dotnet@v3.0.3 with: dotnet-version: 6.0.100 diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml index b1a1cf807..7c4c8ff44 100644 --- a/.github/workflows/dotnet-core.yml +++ b/.github/workflows/dotnet-core.yml @@ -15,12 +15,13 @@ jobs: - uses: actions/checkout@v3 - name: Setup .NET Core - uses: actions/setup-dotnet@v3.0.1 + uses: actions/setup-dotnet@v3.0.3 with: dotnet-version: 6.0.100 - name: Install dependencies - run: dotnet restore + run: | + dotnet restore - name: Build Debug run: dotnet build --configuration Debug --no-restore diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b5adb13fd..8951b7109 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,12 +16,12 @@ jobs: fetch-depth: 0 #fetch-depth is needed for GitVersion - name: Install and calculate the new version with GitVersion - uses: gittools/actions/gitversion/setup@v0.9.13 + uses: gittools/actions/gitversion/setup@v0.9.15 with: versionSpec: 5.x - name: Determine Version - uses: gittools/actions/gitversion/execute@v0.9.13 + uses: gittools/actions/gitversion/execute@v0.9.15 id: gitversion # step id used as reference for output values - name: Display GitVersion outputs @@ -30,7 +30,7 @@ jobs: echo "CommitsSinceVersionSource: ${{ steps.gitversion.outputs.CommitsSinceVersionSource }}" - name: Setup dotnet - uses: actions/setup-dotnet@v3.0.1 + uses: actions/setup-dotnet@v3.0.3 with: dotnet-version: 6.0.100 diff --git a/Example/Example.cs b/Example/Example.cs index 97db1f3d3..069e366d5 100644 --- a/Example/Example.cs +++ b/Example/Example.cs @@ -1,76 +1,72 @@ -// A simple Terminal.Gui example in C# - using C# 9.0 Top-level statements -// This is the same code found in the Termiminal Gui README.md file. +// This is a simple example application. For the full range of functionality +// see the UICatalog project + +// A simple Terminal.Gui example in C# - using C# 9.0 Top-level statements using Terminal.Gui; -using NStack; -Application.Init (); +Application.Run (); -// Creates the top-level window to show -var win = new Window ("Example App") { - X = 0, - Y = 1, // Leave one row for the toplevel menu +System.Console.WriteLine ($"Username: {((ExampleWindow)Application.Top).usernameText.Text}"); - // By using Dim.Fill(), this Window will automatically resize without manual intervention - Width = Dim.Fill (), - Height = Dim.Fill () -}; +// Before the application exits, reset Terminal.Gui for clean shutdown +Application.Shutdown (); -Application.Top.Add (win); +// Defines a top-level window with border and title +public class ExampleWindow : Window { + public TextField usernameText; + + public ExampleWindow () + { + Title = "Example App (Ctrl+Q to quit)"; -// Creates a menubar, the item "New" has a help menu. -var menu = new MenuBar (new MenuBarItem [] { - new MenuBarItem ("_File", new MenuItem [] { - new MenuItem ("_New", "Creates a new file", null), - new MenuItem ("_Close", "",null), - new MenuItem ("_Quit", "", () => { if (Quit ()) Application.Top.Running = false; }) - }), - new MenuBarItem ("_Edit", new MenuItem [] { - new MenuItem ("_Copy", "", null), - new MenuItem ("C_ut", "", null), - new MenuItem ("_Paste", "", null) - }) - }); -Application.Top.Add (menu); + // Create input components and labels + var usernameLabel = new Label () { + Text = "Username:" + }; -static bool Quit () -{ - var n = MessageBox.Query (50, 7, "Quit Example", "Are you sure you want to quit this example?", "Yes", "No"); - return n == 0; -} + usernameText = new TextField ("") { + // Position text field adjacent to the label + X = Pos.Right (usernameLabel) + 1, -var login = new Label ("Login: ") { X = 3, Y = 2 }; -var password = new Label ("Password: ") { - X = Pos.Left (login), - Y = Pos.Top (login) + 1 -}; -var loginText = new TextField ("") { - X = Pos.Right (password), - Y = Pos.Top (login), - Width = 40 -}; -var passText = new TextField ("") { - Secret = true, - X = Pos.Left (loginText), - Y = Pos.Top (password), - Width = Dim.Width (loginText) -}; + // Fill remaining horizontal space + Width = Dim.Fill (), + }; -// Add the views to the main window, -win.Add ( - // Using Computed Layout: - login, password, loginText, passText, + var passwordLabel = new Label () { + Text = "Password:", + X = Pos.Left (usernameLabel), + Y = Pos.Bottom (usernameLabel) + 1 + }; - // Using Absolute Layout: - new CheckBox (3, 6, "Remember me"), - new RadioGroup (3, 8, new ustring [] { "_Personal", "_Company" }, 0), - new Button (3, 14, "Ok"), - new Button (10, 14, "Cancel"), - new Label (3, 18, "Press F9 or ESC plus 9 to activate the menubar") -); + var passwordText = new TextField ("") { + Secret = true, + // align with the text box above + X = Pos.Left (usernameText), + Y = Pos.Top (passwordLabel), + Width = Dim.Fill (), + }; -// Run blocks until the user quits the application -Application.Run (); + // Create login button + var btnLogin = new Button () { + Text = "Login", + Y = Pos.Bottom(passwordLabel) + 1, + // center the login button horizontally + X = Pos.Center (), + IsDefault = true, + }; -// Always bracket Application.Init with .Shutdown. -Application.Shutdown (); \ No newline at end of file + // When login button is clicked display a message popup + btnLogin.Clicked += () => { + if (usernameText.Text == "admin" && passwordText.Text == "password") { + MessageBox.Query ("Logging In", "Login Successful", "Ok"); + Application.RequestStop (); + } else { + MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok"); + } + }; + + // Add the views to the Window + Add (usernameLabel, usernameText, passwordLabel, passwordText, btnLogin); + } +} \ No newline at end of file diff --git a/Example/Example.csproj b/Example/Example.csproj index 046a97db0..aebe02889 100644 --- a/Example/Example.csproj +++ b/Example/Example.csproj @@ -10,9 +10,6 @@ 2.0 2.0 - - - diff --git a/Example/README.md b/Example/README.md index 7e9c80dd8..2cb0a9870 100644 --- a/Example/README.md +++ b/Example/README.md @@ -4,6 +4,8 @@ This example shows how to use the Terminal.Gui library to create a simple GUI ap This is the same code found in the Terminal.Gui README.md file. +To explore the full range of functionality in Terminal.Gui, see the [UICatalog](../UICatalog) project + See [README.md](https://github.com/gui-cs/Terminal.Gui) for a list of all Terminal.Gui samples. Note, the old `demo.cs` example has been deleted because it was not a very good example. It can still be found in the [git history](https://github.com/gui-cs/Terminal.Gui/tree/v1.8.2). \ No newline at end of file diff --git a/README.md b/README.md index cbd0ab92a..3b7633dd2 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,19 @@ A toolkit for building rich console apps for .NET, .NET Core, and Mono that work ![Sample app](docfx/images/sample.gif) +## Quick Start + +Paste these commands into your favorite terminal on Windows, Mac, or Linux. This will install the [Terminal.Gui.Templates](https://github.com/gui-cs/Terminal.Gui.templates), create a new "Hello World" TUI app, and run it. + +(Press `CTRL-Q` to exit the app) + +```powershell +dotnet new --install Terminal.Gui.templates +dotnet new tui -n myproj +cd myproj +dotnet run +``` + ## Documentation * [Documentation Home](https://gui-cs.github.io/Terminal.Gui/index.html) @@ -51,100 +64,86 @@ See the [`Terminal.Gui/` README](https://github.com/gui-cs/Terminal.Gui/tree/mas ## Sample Usage in C# +The following example shows a basic Terminal.Gui application written in C#: + ```csharp // A simple Terminal.Gui example in C# - using C# 9.0 Top-level statements using Terminal.Gui; -using NStack; -Application.Init (); +Application.Run (); -// Creates the top-level window to show -var win = new Window ("Example App") { - X = 0, - Y = 1, // Leave one row for the toplevel menu +System.Console.WriteLine ($"Username: {((ExampleWindow)Application.Top).usernameText.Text}"); - // By using Dim.Fill(), this Window will automatically resize without manual intervention - Width = Dim.Fill (), - Height = Dim.Fill () -}; - -Application.Top.Add (win); - -// Creates a menubar, the item "New" has a help menu. -var menu = new MenuBar (new MenuBarItem [] { - new MenuBarItem ("_File", new MenuItem [] { - new MenuItem ("_New", "Creates a new file", null), - new MenuItem ("_Close", "",null), - new MenuItem ("_Quit", "", () => { if (Quit ()) Application.Top.Running = false; }) - }), - new MenuBarItem ("_Edit", new MenuItem [] { - new MenuItem ("_Copy", "", null), - new MenuItem ("C_ut", "", null), - new MenuItem ("_Paste", "", null) - }) - }); -Application.Top.Add (menu); - -static bool Quit () -{ - var n = MessageBox.Query (50, 7, "Quit Example", "Are you sure you want to quit this example?", "Yes", "No"); - return n == 0; -} - -var login = new Label ("Login: ") { X = 3, Y = 2 }; -var password = new Label ("Password: ") { - X = Pos.Left (login), - Y = Pos.Top (login) + 1 -}; -var loginText = new TextField ("") { - X = Pos.Right (password), - Y = Pos.Top (login), - Width = 40 -}; -var passText = new TextField ("") { - Secret = true, - X = Pos.Left (loginText), - Y = Pos.Top (password), - Width = Dim.Width (loginText) -}; - -// Add the views to the main window, -win.Add ( - // Using Computed Layout: - login, password, loginText, passText, - - // Using Absolute Layout: - new CheckBox (3, 6, "Remember me"), - new RadioGroup (3, 8, new ustring [] { "_Personal", "_Company" }, 0), - new Button (3, 14, "Ok"), - new Button (10, 14, "Cancel"), - new Label (3, 18, "Press F9 or ESC plus 9 to activate the menubar") -); - -// Run blocks until the user quits the application -Application.Run (); - -// Always bracket Application.Init with .Shutdown. +// Before the application exits, reset Terminal.Gui for clean shutdown Application.Shutdown (); -``` -The example above shows adding views using both styles of layout supported by **Terminal.Gui**: **Absolute layout** and **[Computed layout](https://gui-cs.github.io/Terminal.Gui/articles/overview.html#layout)**. - -Alternatively, you can encapsulate the app behavior in a new `Window`-derived class, say `App.cs` containing the code above, and simplify your `Main` method to: - -```csharp -using Terminal.Gui; - -class Demo { - static void Main () +// Defines a top-level window with border and title +public class ExampleWindow : Window { + public TextField usernameText; + + public ExampleWindow () { - Application.Run (); - Application.Shutdown (); + Title = "Example App (Ctrl+Q to quit)"; + + // Create input components and labels + var usernameLabel = new Label () { + Text = "Username:" + }; + + usernameText = new TextField ("") { + // Position text field adjacent to the label + X = Pos.Right (usernameLabel) + 1, + + // Fill remaining horizontal space + Width = Dim.Fill (), + }; + + var passwordLabel = new Label () { + Text = "Password:", + X = Pos.Left (usernameLabel), + Y = Pos.Bottom (usernameLabel) + 1 + }; + + var passwordText = new TextField ("") { + Secret = true, + // align with the text box above + X = Pos.Left (usernameText), + Y = Pos.Top (passwordLabel), + Width = Dim.Fill (), + }; + + // Create login button + var btnLogin = new Button () { + Text = "Login", + Y = Pos.Bottom(passwordLabel) + 1, + // center the login button horizontally + X = Pos.Center (), + IsDefault = true, + }; + + // When login button is clicked display a message popup + btnLogin.Clicked += () => { + if (usernameText.Text == "admin" && passwordText.Text == "password") { + MessageBox.Query ("Logging In", "Login Successful", "Ok"); + Application.RequestStop (); + } else { + MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok"); + } + }; + + // Add the views to the Window + Add (usernameLabel, usernameText, passwordLabel, passwordText, btnLogin); } } ``` +When run the application looks as follows: + +![Simple Usage app](./docfx/images/Example.png) + +_Sample application running_ + ## Installing Use NuGet to install the `Terminal.Gui` NuGet package: https://www.nuget.org/packages/Terminal.Gui @@ -157,13 +156,15 @@ To install Terminal.Gui into a .NET Core project, use the `dotnet` CLI tool with dotnet add package Terminal.Gui ``` -See [CONTRIBUTING.md](CONTRIBUTING.md) for instructions for downloading and forking the source. +Or, you can use the [Terminal.Gui.Templates](https://github.com/gui-cs/Terminal.Gui.templates). -## Running and Building +## Building the Library and Running the Examples * Windows, Mac, and Linux - Build and run using the .NET SDK command line tools (`dotnet build` in the root directory). Run `UICatalog` with `dotnet run --project UICatalog`. * Windows - Open `Terminal.sln` with Visual Studio 2022. +See [CONTRIBUTING.md](CONTRIBUTING.md) for instructions for downloading and forking the source. + ## Contributing See [CONTRIBUTING.md](https://github.com/gui-cs/Terminal.Gui/blob/master/CONTRIBUTING.md). @@ -172,4 +173,4 @@ Debates on architecture and design can be found in Issues tagged with [design](h ## History -See [gui-cs](https://github.com/gui-cs/) for how this project came to be. \ No newline at end of file +See [gui-cs](https://github.com/gui-cs/) for how this project came to be. diff --git a/ReactiveExample/ReactiveExample.csproj b/ReactiveExample/ReactiveExample.csproj index afbdd701e..b0dbfe464 100644 --- a/ReactiveExample/ReactiveExample.csproj +++ b/ReactiveExample/ReactiveExample.csproj @@ -11,8 +11,8 @@ 2.0 - - + + diff --git a/UnitTests/ScenarioTests.cs b/Terminal.Gui UnitTests/ScenarioTests.cs similarity index 94% rename from UnitTests/ScenarioTests.cs rename to Terminal.Gui UnitTests/ScenarioTests.cs index 47e94b216..f5f1dc57b 100644 --- a/UnitTests/ScenarioTests.cs +++ b/Terminal.Gui UnitTests/ScenarioTests.cs @@ -11,7 +11,7 @@ using Xunit.Abstractions; // Alias Console to MockConsole so we don't accidentally use Console using Console = Terminal.Gui.FakeConsole; -namespace Terminal.Gui { +namespace UICatalog { public class ScenarioTests { readonly ITestOutputHelper output; @@ -49,28 +49,33 @@ namespace Terminal.Gui { [Fact] public void Run_All_Scenarios () { - List scenarioClasses = Scenario.GetDerivedClasses (); - Assert.NotEmpty (scenarioClasses); + List scenarios = Scenario.GetScenarios (); + Assert.NotEmpty (scenarios); - foreach (var scenarioClass in scenarioClasses) { + foreach (var scenario in scenarios) { - output.WriteLine ($"Running Scenario '{scenarioClass.Name}'"); + output.WriteLine ($"Running Scenario '{scenario}'"); Func closeCallback = (MainLoop loop) => { Application.RequestStop (); return false; }; - var scenario = (Scenario)Activator.CreateInstance (scenarioClass); Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); // Close after a short period of time - var token = Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (200), closeCallback); + var token = Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (100), closeCallback); - scenario.Init (Application.Top, Colors.Base); + scenario.Init (Colors.Base); scenario.Setup (); scenario.Run (); Application.Shutdown (); +#if DEBUG_IDISPOSABLE + foreach (var inst in Responder.Instances) { + Assert.True (inst.WasDisposed); + } + Responder.Instances.Clear (); +#endif } #if DEBUG_IDISPOSABLE foreach (var inst in Responder.Instances) { @@ -83,11 +88,11 @@ namespace Terminal.Gui { [Fact] public void Run_Generic () { - List scenarioClasses = Scenario.GetDerivedClasses (); - Assert.NotEmpty (scenarioClasses); + List scenarios = Scenario.GetScenarios (); + Assert.NotEmpty (scenarios); - var item = scenarioClasses.FindIndex (t => Scenario.ScenarioMetadata.GetName (t).Equals ("Generic", StringComparison.OrdinalIgnoreCase)); - var scenarioClass = scenarioClasses [item]; + var item = scenarios.FindIndex (s => s.GetName ().Equals ("Generic", StringComparison.OrdinalIgnoreCase)); + var generic = scenarios [item]; // Setup some fake keypresses // Passing empty string will cause just a ctrl-q to be fired int stackSize = CreateInput (""); @@ -116,13 +121,12 @@ namespace Terminal.Gui { Assert.Equal (Key.CtrlMask | Key.Q, args.KeyEvent.Key); }; - var scenario = (Scenario)Activator.CreateInstance (scenarioClass); - scenario.Init (Application.Top, Colors.Base); - scenario.Setup (); + generic.Init (Colors.Base); + generic.Setup (); // There is no need to call Application.Begin because Init already creates the Application.Top // If Application.RunState is used then the Application.RunLoop must also be used instead Application.Run. //var rs = Application.Begin (Application.Top); - scenario.Run (); + generic.Run (); //Application.End (rs); diff --git a/Terminal.Gui UnitTests/UnitTests.csproj b/Terminal.Gui UnitTests/UnitTests.csproj new file mode 100644 index 000000000..f01db4c0c --- /dev/null +++ b/Terminal.Gui UnitTests/UnitTests.csproj @@ -0,0 +1,57 @@ + + + net6.0 + false + + + + + 1.0 + 1.0 + 1.0 + 1.0 + + + TRACE + + + TRACE;DEBUG_IDISPOSABLE + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + True + + + [UICatalog]* + + + + + + + + + + False + + + \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index b5db7847b..d8d38392f 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -6,6 +6,7 @@ // using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; @@ -617,7 +618,7 @@ namespace Terminal.Gui { return keyModifiers; } - void ProcessInput (Action keyHandler, Action keyDownHandler, Action keyUpHandler, Action mouseHandler) + void ProcessInput () { int wch; var code = Curses.get_wch (out wch); @@ -787,6 +788,8 @@ namespace Terminal.Gui { } Action keyHandler; + Action keyDownHandler; + Action keyUpHandler; Action mouseHandler; public override void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action keyDownHandler, Action keyUpHandler, Action mouseHandler) @@ -794,12 +797,14 @@ namespace Terminal.Gui { // Note: Curses doesn't support keydown/up events and thus any passed keyDown/UpHandlers will never be called Curses.timeout (0); this.keyHandler = keyHandler; + this.keyDownHandler = keyDownHandler; + this.keyUpHandler = keyUpHandler; this.mouseHandler = mouseHandler; var mLoop = mainLoop.Driver as UnixMainLoop; mLoop.AddWatch (0, UnixMainLoop.Condition.PollIn, x => { - ProcessInput (keyHandler, keyDownHandler, keyUpHandler, mouseHandler); + ProcessInput (); return true; }); @@ -950,11 +955,13 @@ namespace Terminal.Gui { public static bool Is_WSL_Platform () { - if (new CursesClipboard ().IsSupported) { - return false; - } - var result = BashRunner.Run ("uname -a", runCurses: false); - if (result.Contains ("microsoft") && result.Contains ("WSL")) { + // xclip does not work on WSL, so we need to use the Windows clipboard vis Powershell + //if (new CursesClipboard ().IsSupported) { + // // If xclip is installed on Linux under WSL, this will return true. + // return false; + //} + var (exitCode, result) = ClipboardProcessRunner.Bash ("uname -a", waitForOutput: true); + if (exitCode == 0 && result.Contains ("microsoft") && result.Contains ("WSL")) { return true; } return false; @@ -1128,26 +1135,48 @@ namespace Terminal.Gui { return false; } - public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control) + public override void SendKeys (char keyChar, ConsoleKey consoleKey, bool shift, bool alt, bool control) { - Key k; + Key key; - if ((shift || alt || control) - && keyChar - (int)Key.Space >= (uint)Key.A && keyChar - (int)Key.Space <= (uint)Key.Z) { - k = (Key)(keyChar - (uint)Key.Space); + if (consoleKey == ConsoleKey.Packet) { + ConsoleModifiers mod = new ConsoleModifiers (); + if (shift) { + mod |= ConsoleModifiers.Shift; + } + if (alt) { + mod |= ConsoleModifiers.Alt; + } + if (control) { + mod |= ConsoleModifiers.Control; + } + var kchar = ConsoleKeyMapping.GetKeyCharFromConsoleKey (keyChar, mod, out uint ckey, out _); + key = ConsoleKeyMapping.MapConsoleKeyToKey ((ConsoleKey)ckey, out bool mappable); + if (mappable) { + key = (Key)kchar; + } } else { - k = (Key)keyChar; + key = (Key)keyChar; } + + KeyModifiers km = new KeyModifiers (); if (shift) { - k |= Key.ShiftMask; + if (keyChar == 0) { + key |= Key.ShiftMask; + } + km.Shift = shift; } if (alt) { - k |= Key.AltMask; + key |= Key.AltMask; + km.Alt = alt; } if (control) { - k |= Key.CtrlMask; + key |= Key.CtrlMask; + km.Ctrl = control; } - keyHandler (new KeyEvent (k, MapKeyModifiers (k))); + keyDownHandler (new KeyEvent (key, km)); + keyHandler (new KeyEvent (key, km)); + keyUpHandler (new KeyEvent (key, km)); } public override bool GetColors (int value, out Color foreground, out Color background) @@ -1233,134 +1262,79 @@ namespace Terminal.Gui { } } + /// + /// A clipboard implementation for Linux. + /// This implementation uses the xclip command to access the clipboard. + /// + /// + /// If xclip is not installed, this implementation will not work. + /// class CursesClipboard : ClipboardBase { public CursesClipboard () { IsSupported = CheckSupport (); } + string xclipPath = string.Empty; public override bool IsSupported { get; } bool CheckSupport () { try { - var result = BashRunner.Run ("which xclip", runCurses: false); - return result.FileExists (); + var (exitCode, result) = ClipboardProcessRunner.Bash ("which xclip", waitForOutput: true); + if (exitCode == 0 && result.FileExists ()) { + xclipPath = result; + return true; + } } catch (Exception) { // Permissions issue. - return false; } + return false; } protected override string GetClipboardDataImpl () { var tempFileName = System.IO.Path.GetTempFileName (); + var xclipargs = "-selection clipboard -o"; + try { - // BashRunner.Run ($"xsel -o --clipboard > {tempFileName}"); - BashRunner.Run ($"xclip -selection clipboard -o > {tempFileName}"); - return System.IO.File.ReadAllText (tempFileName); + var (exitCode, result) = ClipboardProcessRunner.Bash ($"{xclipPath} {xclipargs} > {tempFileName}", waitForOutput: false); + if (exitCode == 0) { + if (Application.Driver is CursesDriver) { + Curses.raw (); + Curses.noecho (); + } + return System.IO.File.ReadAllText (tempFileName); + } + } catch (Exception e) { + throw new NotSupportedException ($"\"{xclipPath} {xclipargs}\" failed.", e); } finally { System.IO.File.Delete (tempFileName); } + return string.Empty; } protected override void SetClipboardDataImpl (string text) { - // var tempFileName = System.IO.Path.GetTempFileName (); - // System.IO.File.WriteAllText (tempFileName, text); - // try { - // // BashRunner.Run ($"cat {tempFileName} | xsel -i --clipboard"); - // BashRunner.Run ($"cat {tempFileName} | xclip -selection clipboard"); - // } finally { - // System.IO.File.Delete (tempFileName); - // } - - BashRunner.Run ("xclip -selection clipboard -i", false, text); - } - } - - static class BashRunner { - public static string Run (string commandLine, bool output = true, string inputText = "", bool runCurses = true) - { - var arguments = $"-c \"{commandLine}\""; - - if (output) { - var errorBuilder = new System.Text.StringBuilder (); - var outputBuilder = new System.Text.StringBuilder (); - - using (var process = new System.Diagnostics.Process { - StartInfo = new System.Diagnostics.ProcessStartInfo { - FileName = "bash", - Arguments = arguments, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = false, - } - }) { - process.Start (); - process.OutputDataReceived += (sender, args) => { outputBuilder.AppendLine (args.Data); }; - process.BeginOutputReadLine (); - process.ErrorDataReceived += (sender, args) => { errorBuilder.AppendLine (args.Data); }; - process.BeginErrorReadLine (); - if (!process.DoubleWaitForExit ()) { - var timeoutError = $@"Process timed out. Command line: bash {arguments}. - Output: {outputBuilder} - Error: {errorBuilder}"; - throw new Exception (timeoutError); - } - if (process.ExitCode == 0) { - if (runCurses && Application.Driver is CursesDriver) { - Curses.raw (); - Curses.noecho (); - } - return outputBuilder.ToString (); - } - - var error = $@"Could not execute process. Command line: bash {arguments}. - Output: {outputBuilder} - Error: {errorBuilder}"; - throw new Exception (error); - } - } else { - using (var process = new System.Diagnostics.Process { - StartInfo = new System.Diagnostics.ProcessStartInfo { - FileName = "bash", - Arguments = arguments, - RedirectStandardInput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = false - } - }) { - process.Start (); - process.StandardInput.Write (inputText); - process.StandardInput.Close (); - process.WaitForExit (); - if (runCurses && Application.Driver is CursesDriver) { - Curses.raw (); - Curses.noecho (); - } - return inputText; - } - } - } - - public static bool DoubleWaitForExit (this System.Diagnostics.Process process) - { - var result = process.WaitForExit (500); - if (result) { - process.WaitForExit (); - } - return result; - } - - public static bool FileExists (this string value) - { - return !string.IsNullOrEmpty (value) && !value.Contains ("not found"); + var xclipargs = "-selection clipboard -i"; + try { + var (exitCode, _) = ClipboardProcessRunner.Bash ($"{xclipPath} {xclipargs}", text, waitForOutput: false); + if (exitCode == 0 && Application.Driver is CursesDriver) { + Curses.raw (); + Curses.noecho (); + } + } catch (Exception e) { + throw new NotSupportedException ($"\"{xclipPath} {xclipargs} < {text}\" failed", e); + } } } + /// + /// A clipboard implementation for MacOSX. + /// This implementation uses the Mac clipboard API (via P/Invoke) to copy/paste. + /// The existance of the Mac pbcopy and pbpaste commands + /// is used to determine if copy/paste is supported. + /// class MacOSXClipboard : ClipboardBase { IntPtr nsString = objc_getClass ("NSString"); IntPtr nsPasteboard = objc_getClass ("NSPasteboard"); @@ -1387,12 +1361,12 @@ namespace Terminal.Gui { bool CheckSupport () { - var result = BashRunner.Run ("which pbcopy"); - if (!result.FileExists ()) { + var (exitCode, result) = ClipboardProcessRunner.Bash ("which pbcopy", waitForOutput: true); + if (exitCode != 0 || !result.FileExists ()) { return false; } - result = BashRunner.Run ("which pbpaste"); - return result.FileExists (); + (exitCode, result) = ClipboardProcessRunner.Bash ("which pbpaste", waitForOutput: true); + return exitCode == 0 && result.FileExists (); } protected override string GetClipboardDataImpl () @@ -1435,94 +1409,77 @@ namespace Terminal.Gui { static extern IntPtr sel_registerName (string selectorName); } + /// + /// A clipboard implementation for Linux, when running under WSL. + /// This implementation uses the Windows clipboard to store the data, and uses Windows' + /// powershell.exe (launched via WSL interop services) to set/get the Windows + /// clipboard. + /// class WSLClipboard : ClipboardBase { + bool isSupported = false; public WSLClipboard () { - IsSupported = CheckSupport (); + isSupported = CheckSupport (); } - public override bool IsSupported { get; } + public override bool IsSupported { + get { + return isSupported = CheckSupport (); + } + } + + private static string powershellPath = string.Empty; bool CheckSupport () { - try { - var result = BashRunner.Run ("which powershell.exe"); - return result.FileExists (); - } catch (System.Exception) { - return false; - } + if (string.IsNullOrEmpty (powershellPath)) { + // Specify pwsh.exe (not pwsh) to ensure we get the Windows version (invoked via WSL) + var (exitCode, result) = ClipboardProcessRunner.Bash ("which pwsh.exe", waitForOutput: true); + if (exitCode > 0) { + (exitCode, result) = ClipboardProcessRunner.Bash ("which powershell.exe", waitForOutput: true); + } - //var result = BashRunner.Run ("which powershell.exe"); - //if (!result.FileExists ()) { - // return false; - //} - //result = BashRunner.Run ("which clip.exe"); - //return result.FileExists (); + if (exitCode == 0) { + powershellPath = result; + } + } + return !string.IsNullOrEmpty (powershellPath); } protected override string GetClipboardDataImpl () { - using (var powershell = new System.Diagnostics.Process { - StartInfo = new System.Diagnostics.ProcessStartInfo { - RedirectStandardOutput = true, - FileName = "powershell.exe", - Arguments = "-noprofile -command \"Get-Clipboard\"", - UseShellExecute = false, - CreateNoWindow = true - } - }) { - powershell.Start (); - var result = powershell.StandardOutput.ReadToEnd (); - powershell.StandardOutput.Close (); - if (!powershell.DoubleWaitForExit ()) { - var timeoutError = $@"Process timed out. Command line: bash {powershell.StartInfo.Arguments}. - Output: {powershell.StandardOutput.ReadToEnd ()} - Error: {powershell.StandardError.ReadToEnd ()}"; - throw new Exception (timeoutError); - } + if (!IsSupported) { + return string.Empty; + } + + var (exitCode, output) = ClipboardProcessRunner.Process (powershellPath, "-noprofile -command \"Get-Clipboard\""); + if (exitCode == 0) { if (Application.Driver is CursesDriver) { Curses.raw (); Curses.noecho (); } - if (result.EndsWith ("\r\n")) { - result = result.Substring (0, result.Length - 2); + + if (output.EndsWith ("\r\n")) { + output = output.Substring (0, output.Length - 2); } - return result; + return output; } + return string.Empty; } protected override void SetClipboardDataImpl (string text) { - using (var powershell = new System.Diagnostics.Process { - StartInfo = new System.Diagnostics.ProcessStartInfo { - FileName = "powershell.exe", - Arguments = $"-noprofile -command \"Set-Clipboard -Value \\\"{text}\\\"\"" - } - }) { - powershell.Start (); - if (!powershell.DoubleWaitForExit ()) { - var timeoutError = $@"Process timed out. Command line: bash {powershell.StartInfo.Arguments}. - Output: {powershell.StandardOutput.ReadToEnd ()} - Error: {powershell.StandardError.ReadToEnd ()}"; - throw new Exception (timeoutError); - } + if (!IsSupported) { + return; + } + + var (exitCode, output) = ClipboardProcessRunner.Process (powershellPath, $"-noprofile -command \"Set-Clipboard -Value \\\"{text}\\\"\""); + if (exitCode == 0) { if (Application.Driver is CursesDriver) { Curses.raw (); Curses.noecho (); } } - - //using (var clipExe = new System.Diagnostics.Process { - // StartInfo = new System.Diagnostics.ProcessStartInfo { - // FileName = "clip.exe", - // RedirectStandardInput = true - // } - //}) { - // clipExe.Start (); - // clipExe.StandardInput.Write (text); - // clipExe.StandardInput.Close (); - // clipExe.WaitForExit (); - //} } } } diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs index 64dbb3b18..977913fa2 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs @@ -38,6 +38,11 @@ namespace Terminal.Gui { /// can watch file descriptors using the AddWatch methods. /// internal class UnixMainLoop : IMainLoopDriver { + public UnixMainLoop (ConsoleDriver consoleDriver = null) + { + // UnixDriver doesn't use the consoleDriver parameter, but the WindowsDriver does. + } + public const int KEY_RESIZE = unchecked((int)0xffffffffffffffff); [StructLayout (LayoutKind.Sequential)] @@ -176,16 +181,15 @@ namespace Terminal.Gui { { UpdatePollMap (); - if (CheckTimers (wait, out var pollTimeout)) { - return true; - } + bool checkTimersResult = CheckTimers (wait, out var pollTimeout); var n = poll (pollmap, (uint)pollmap.Length, pollTimeout); if (n == KEY_RESIZE) { winChanged = true; } - return n >= KEY_RESIZE || CheckTimers (wait, out pollTimeout); + + return checkTimersResult || n >= KEY_RESIZE; } bool CheckTimers (bool wait, out int pollTimeout) diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs index f365fc2f1..e78baa96a 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs @@ -164,11 +164,13 @@ namespace Terminal.Gui { // // T:System.IO.IOException: // An I/O error occurred. + + static ConsoleColor _defaultBackgroundColor = ConsoleColor.Black; + /// /// /// public static ConsoleColor BackgroundColor { get; set; } = _defaultBackgroundColor; - static ConsoleColor _defaultBackgroundColor = ConsoleColor.Black; // // Summary: @@ -187,11 +189,13 @@ namespace Terminal.Gui { // // T:System.IO.IOException: // An I/O error occurred. + + static ConsoleColor _defaultForegroundColor = ConsoleColor.Gray; + /// /// /// public static ConsoleColor ForegroundColor { get; set; } = _defaultForegroundColor; - static ConsoleColor _defaultForegroundColor = ConsoleColor.Gray; // // Summary: // Gets or sets the height of the buffer area. @@ -541,6 +545,9 @@ namespace Terminal.Gui { // Exceptions: // T:System.IO.IOException: // An I/O error occurred. + + static char [,] _buffer = new char [WindowWidth, WindowHeight]; + /// /// /// @@ -550,8 +557,6 @@ namespace Terminal.Gui { SetCursorPosition (0, 0); } - static char [,] _buffer = new char [WindowWidth, WindowHeight]; - // // Summary: // Copies a specified source area of the screen buffer to a specified destination @@ -811,9 +816,9 @@ namespace Terminal.Gui { public static ConsoleKeyInfo ReadKey (bool intercept) { if (MockKeyPresses.Count > 0) { - return MockKeyPresses.Pop(); + return MockKeyPresses.Pop (); } else { - return new ConsoleKeyInfo ('\0', (ConsoleKey)'\0', false,false,false); + return new ConsoleKeyInfo ('\0', (ConsoleKey)'\0', false, false, false); } } @@ -1396,7 +1401,10 @@ namespace Terminal.Gui { /// public static void Write (char [] buffer) { - throw new NotImplementedException (); + _buffer [CursorLeft, CursorTop] = (char)0; + foreach (var ch in buffer) { + _buffer [CursorLeft, CursorTop] += ch; + } } // diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs index 8189207bc..77526629b 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -6,10 +6,12 @@ // using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using NStack; + // Alias Console to MockConsole so we don't accidentally use Console using Console = Terminal.Gui.FakeConsole; @@ -19,6 +21,27 @@ namespace Terminal.Gui { /// public class FakeDriver : ConsoleDriver { #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + + public class Behaviors { + + public bool UseFakeClipboard { get; internal set; } + public bool FakeClipboardAlwaysThrowsNotSupportedException { get; internal set; } + public bool FakeClipboardIsSupportedAlwaysFalse { get; internal set; } + + public Behaviors (bool useFakeClipboard = false, bool fakeClipboardAlwaysThrowsNotSupportedException = false, bool fakeClipboardIsSupportedAlwaysTrue = false) + { + UseFakeClipboard = useFakeClipboard; + FakeClipboardAlwaysThrowsNotSupportedException = fakeClipboardAlwaysThrowsNotSupportedException; + FakeClipboardIsSupportedAlwaysFalse = fakeClipboardIsSupportedAlwaysTrue; + + // double check usage is correct + Debug.Assert (useFakeClipboard == false && fakeClipboardAlwaysThrowsNotSupportedException == false); + Debug.Assert (useFakeClipboard == false && fakeClipboardIsSupportedAlwaysTrue == false); + } + } + + public static FakeDriver.Behaviors FakeBehaviors = new Behaviors (); + int cols, rows, left, top; public override int Cols => cols; public override int Rows => rows; @@ -26,7 +49,8 @@ namespace Terminal.Gui { public override int Left => 0; public override int Top => 0; public override bool HeightAsBuffer { get; set; } - public override IClipboard Clipboard { get; } + private IClipboard clipboard = null; + public override IClipboard Clipboard => clipboard; // The format is rows, columns and 3 values on the last column: Rune, Attribute and Dirty Flag int [,,] contents; @@ -59,15 +83,19 @@ namespace Terminal.Gui { public FakeDriver () { - if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { - Clipboard = new WindowsClipboard (); - } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { - Clipboard = new MacOSXClipboard (); + if (FakeBehaviors.UseFakeClipboard) { + clipboard = new FakeClipboard (FakeBehaviors.FakeClipboardAlwaysThrowsNotSupportedException, FakeBehaviors.FakeClipboardIsSupportedAlwaysFalse); } else { - if (CursesDriver.Is_WSL_Platform ()) { - Clipboard = new WSLClipboard (); + if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { + clipboard = new WindowsClipboard (); + } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { + clipboard = new MacOSXClipboard (); } else { - Clipboard = new CursesClipboard (); + if (CursesDriver.Is_WSL_Platform ()) { + clipboard = new WSLClipboard (); + } else { + clipboard = new CursesClipboard (); + } } } } @@ -104,12 +132,12 @@ namespace Terminal.Gui { needMove = false; } if (runeWidth < 2 && ccol > 0 - && Rune.ColumnWidth ((char)contents [crow, ccol - 1, 0]) > 1) { + && Rune.ColumnWidth ((Rune)contents [crow, ccol - 1, 0]) > 1) { contents [crow, ccol - 1, 0] = (int)(uint)' '; } else if (runeWidth < 2 && ccol <= Clip.Right - 1 - && Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) { + && Rune.ColumnWidth ((Rune)contents [crow, ccol, 0]) > 1) { contents [crow, ccol + 1, 0] = (int)(uint)' '; contents [crow, ccol + 1, 2] = 1; @@ -234,7 +262,12 @@ namespace Terminal.Gui { if (color != redrawColor) SetColor (color); - FakeConsole.Write ((char)contents [row, col, 0]); + Rune rune = contents [row, col, 0]; + if (Rune.DecodeSurrogatePair (rune, out char [] spair)) { + FakeConsole.Write (spair); + } else { + FakeConsole.Write ((char)rune); + } contents [row, col, 2] = 0; } } @@ -256,6 +289,22 @@ namespace Terminal.Gui { currentAttribute = c; } + public ConsoleKeyInfo FromVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) + { + if (consoleKeyInfo.Key != ConsoleKey.Packet) { + return consoleKeyInfo; + } + + var mod = consoleKeyInfo.Modifiers; + var shift = (mod & ConsoleModifiers.Shift) != 0; + var alt = (mod & ConsoleModifiers.Alt) != 0; + var control = (mod & ConsoleModifiers.Control) != 0; + + var keyChar = ConsoleKeyMapping.GetKeyCharFromConsoleKey (consoleKeyInfo.KeyChar, consoleKeyInfo.Modifiers, out uint virtualKey, out _); + + return new ConsoleKeyInfo ((char)keyChar, (ConsoleKey)virtualKey, shift, alt, control); + } + Key MapKey (ConsoleKeyInfo keyInfo) { switch (keyInfo.Key) { @@ -263,6 +312,8 @@ namespace Terminal.Gui { return MapKeyModifiers (keyInfo, Key.Esc); case ConsoleKey.Tab: return keyInfo.Modifiers == ConsoleModifiers.Shift ? Key.BackTab : Key.Tab; + case ConsoleKey.Clear: + return MapKeyModifiers (keyInfo, Key.Clear); case ConsoleKey.Home: return MapKeyModifiers (keyInfo, Key.Home); case ConsoleKey.End: @@ -289,6 +340,8 @@ namespace Terminal.Gui { return MapKeyModifiers (keyInfo, Key.DeleteChar); case ConsoleKey.Insert: return MapKeyModifiers (keyInfo, Key.InsertChar); + case ConsoleKey.PrintScreen: + return MapKeyModifiers (keyInfo, Key.PrintScreen); case ConsoleKey.Oem1: case ConsoleKey.Oem2: @@ -318,6 +371,9 @@ namespace Terminal.Gui { if (keyInfo.Modifiers == ConsoleModifiers.Alt) { return (Key)(((uint)Key.AltMask) | ((uint)Key.A + delta)); } + if (keyInfo.Modifiers == (ConsoleModifiers.Shift | ConsoleModifiers.Alt)) { + return MapKeyModifiers (keyInfo, (Key)((uint)Key.A + delta)); + } if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { if (keyInfo.KeyChar == 0) { return (Key)(((uint)Key.AltMask | (uint)Key.CtrlMask) | ((uint)Key.A + delta)); @@ -335,9 +391,14 @@ namespace Terminal.Gui { if (keyInfo.Modifiers == ConsoleModifiers.Control) { return (Key)(((uint)Key.CtrlMask) | ((uint)Key.D0 + delta)); } - if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30) { + if (keyInfo.Modifiers == (ConsoleModifiers.Shift | ConsoleModifiers.Alt)) { return MapKeyModifiers (keyInfo, (Key)((uint)Key.D0 + delta)); } + if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { + if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30) { + return MapKeyModifiers (keyInfo, (Key)((uint)Key.D0 + delta)); + } + } return (Key)((uint)keyInfo.KeyChar); } if (key >= ConsoleKey.F1 && key <= ConsoleKey.F12) { @@ -387,6 +448,9 @@ namespace Terminal.Gui { void ProcessInput (ConsoleKeyInfo consoleKey) { + if (consoleKey.Key == ConsoleKey.Packet) { + consoleKey = FromVKPacketToKConsoleKeyInfo (consoleKey); + } keyModifiers = new KeyModifiers (); if (consoleKey.Modifiers.HasFlag (ConsoleModifiers.Shift)) { keyModifiers.Shift = true; @@ -610,6 +674,41 @@ namespace Terminal.Gui { } #endregion + + public class FakeClipboard : ClipboardBase { + public Exception FakeException = null; + + string contents = string.Empty; + + bool isSupportedAlwaysFalse = false; + + public override bool IsSupported => !isSupportedAlwaysFalse; + + public FakeClipboard (bool fakeClipboardThrowsNotSupportedException = false, bool isSupportedAlwaysFalse = false) + { + this.isSupportedAlwaysFalse = isSupportedAlwaysFalse; + if (fakeClipboardThrowsNotSupportedException) { + FakeException = new NotSupportedException ("Fake clipboard exception"); + } + } + + protected override string GetClipboardDataImpl () + { + if (FakeException != null) { + throw FakeException; + } + return contents; + } + + protected override void SetClipboardDataImpl (string text) + { + if (FakeException != null) { + throw FakeException; + } + contents = text; + } + } + #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } } \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs index d59072972..efa1fcec6 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs @@ -15,7 +15,7 @@ namespace Terminal.Gui { AutoResetEvent waitForProbe = new AutoResetEvent (false); ConsoleKeyInfo? keyResult = null; MainLoop mainLoop; - Func consoleKeyReaderFn = null; + Func consoleKeyReaderFn = () => FakeConsole.ReadKey (true); /// /// Invoked when a Key is pressed. @@ -23,18 +23,12 @@ namespace Terminal.Gui { public Action KeyPressed; /// - /// Initializes the class. + /// Creates an instance of the FakeMainLoop. is not used. /// - /// - /// Passing a consoleKeyReaderfn is provided to support unit test scenarios. - /// - /// The method to be called to get a key from the console. - public FakeMainLoop (Func consoleKeyReaderFn = null) + /// + public FakeMainLoop (ConsoleDriver consoleDriver = null) { - if (consoleKeyReaderFn == null) { - throw new ArgumentNullException ("key reader function must be provided."); - } - this.consoleKeyReaderFn = consoleKeyReaderFn; + // consoleDriver is not needed/used in FakeConsole } void WindowsKeyReader () diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index c6c65caa7..37b43244b 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -533,6 +533,7 @@ namespace Terminal.Gui { int foundPoint = 0; string value = ""; var kChar = GetKeyCharArray (cki); + //System.Diagnostics.Debug.WriteLine ($"kChar: {new string (kChar)}"); for (int i = 0; i < kChar.Length; i++) { var c = kChar [i]; if (c == '<') { @@ -560,6 +561,8 @@ namespace Terminal.Gui { // isButtonPressed = false; //} + //System.Diagnostics.Debug.WriteLine ($"buttonCode: {buttonCode}"); + switch (buttonCode) { case 0: case 8: @@ -1480,7 +1483,13 @@ namespace Terminal.Gui { output.Append (WriteAttributes (attr)); } outputWidth++; - output.Append ((char)contents [row, col, 0]); + var rune = contents [row, col, 0]; + char [] spair; + if (Rune.DecodeSurrogatePair((uint) rune, out spair)) { + output.Append (spair); + } else { + output.Append ((char)rune); + } contents [row, col, 2] = 0; } } @@ -1610,6 +1619,22 @@ namespace Terminal.Gui { currentAttribute = c; } + public ConsoleKeyInfo FromVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) + { + if (consoleKeyInfo.Key != ConsoleKey.Packet) { + return consoleKeyInfo; + } + + var mod = consoleKeyInfo.Modifiers; + var shift = (mod & ConsoleModifiers.Shift) != 0; + var alt = (mod & ConsoleModifiers.Alt) != 0; + var control = (mod & ConsoleModifiers.Control) != 0; + + var keyChar = ConsoleKeyMapping.GetKeyCharFromConsoleKey (consoleKeyInfo.KeyChar, consoleKeyInfo.Modifiers, out uint virtualKey, out _); + + return new ConsoleKeyInfo ((char)keyChar, (ConsoleKey)virtualKey, shift, alt, control); + } + Key MapKey (ConsoleKeyInfo keyInfo) { MapKeyModifiers (keyInfo, (Key)keyInfo.Key); @@ -1687,7 +1712,7 @@ namespace Terminal.Gui { return (Key)(((uint)Key.CtrlMask) | ((uint)Key.D0 + delta)); } if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { - if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30) { + if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30 || keyInfo.KeyChar == ((uint)Key.D0 + delta)) { return MapKeyModifiers (keyInfo, (Key)((uint)Key.D0 + delta)); } } @@ -1754,14 +1779,23 @@ namespace Terminal.Gui { { switch (inputEvent.EventType) { case NetEvents.EventType.Key: + ConsoleKeyInfo consoleKeyInfo = inputEvent.ConsoleKeyInfo; + if (consoleKeyInfo.Key == ConsoleKey.Packet) { + consoleKeyInfo = FromVKPacketToKConsoleKeyInfo (consoleKeyInfo); + } keyModifiers = new KeyModifiers (); - var map = MapKey (inputEvent.ConsoleKeyInfo); + var map = MapKey (consoleKeyInfo); if (map == (Key)0xffffffff) { return; } - keyDownHandler (new KeyEvent (map, keyModifiers)); - keyHandler (new KeyEvent (map, keyModifiers)); - keyUpHandler (new KeyEvent (map, keyModifiers)); + if (map == Key.Null) { + keyDownHandler (new KeyEvent (map, keyModifiers)); + keyUpHandler (new KeyEvent (map, keyModifiers)); + } else { + keyDownHandler (new KeyEvent (map, keyModifiers)); + keyHandler (new KeyEvent (map, keyModifiers)); + keyUpHandler (new KeyEvent (map, keyModifiers)); + } break; case NetEvents.EventType.Mouse: mouseHandler (ToDriverMouse (inputEvent.MouseEvent)); @@ -1804,6 +1838,8 @@ namespace Terminal.Gui { MouseEvent ToDriverMouse (NetEvents.MouseEvent me) { + //System.Diagnostics.Debug.WriteLine ($"X: {me.Position.X}; Y: {me.Position.Y}; ButtonState: {me.ButtonState}"); + MouseFlags mouseFlag = 0; if ((me.ButtonState & NetEvents.MouseButtonState.Button1Pressed) != 0) { @@ -1935,14 +1971,8 @@ namespace Terminal.Gui { public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control) { NetEvents.InputResult input = new NetEvents.InputResult (); - ConsoleKey ck; - if (char.IsLetter (keyChar)) { - ck = key; - } else { - ck = (ConsoleKey)'\0'; - } input.EventType = NetEvents.EventType.Key; - input.ConsoleKeyInfo = new ConsoleKeyInfo (keyChar, ck, shift, alt, control); + input.ConsoleKeyInfo = new ConsoleKeyInfo (keyChar, key, shift, alt, control); try { ProcessInput (input); diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index e25027050..28013fe71 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -251,7 +251,7 @@ namespace Terminal.Gui { throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ()); } var winRect = new SmallRect (0, 0, (short)(newCols - 1), (short)Math.Max (newRows - 1, 0)); - if (!SetConsoleWindowInfo (ScreenBuffer, true, ref winRect)) { + if (!SetConsoleWindowInfo (OutputHandle, true, ref winRect)) { //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ()); return new Size (cols, rows); } @@ -261,7 +261,7 @@ namespace Terminal.Gui { void SetConsoleOutputWindow (CONSOLE_SCREEN_BUFFER_INFOEX csbi) { - if (ScreenBuffer != IntPtr.Zero && !SetConsoleScreenBufferInfoEx (OutputHandle, ref csbi)) { + if (ScreenBuffer != IntPtr.Zero && !SetConsoleScreenBufferInfoEx (ScreenBuffer, ref csbi)) { throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ()); } } @@ -534,12 +534,14 @@ namespace Terminal.Gui { public ConsoleKeyInfo consoleKeyInfo; public bool CapsLock; public bool NumLock; + public bool Scrolllock; - public ConsoleKeyInfoEx (ConsoleKeyInfo consoleKeyInfo, bool capslock, bool numlock) + public ConsoleKeyInfoEx (ConsoleKeyInfo consoleKeyInfo, bool capslock, bool numlock, bool scrolllock) { this.consoleKeyInfo = consoleKeyInfo; CapsLock = capslock; NumLock = numlock; + Scrolllock = scrolllock; } } @@ -771,7 +773,7 @@ namespace Terminal.Gui { w += 3; } var newSize = WinConsole.SetConsoleWindow ( - (short)Math.Max (w, 16), (short)Math.Max (e.Height, 1)); + (short)Math.Max (w, 16), (short)Math.Max (e.Height, 0)); left = 0; top = 0; cols = newSize.Width; @@ -786,7 +788,26 @@ namespace Terminal.Gui { { switch (inputEvent.EventType) { case WindowsConsole.EventType.Key: + var fromPacketKey = inputEvent.KeyEvent.wVirtualKeyCode == (uint)ConsoleKey.Packet; + if (fromPacketKey) { + inputEvent.KeyEvent = FromVKPacketToKeyEventRecord (inputEvent.KeyEvent); + } var map = MapKey (ToConsoleKeyInfoEx (inputEvent.KeyEvent)); + //var ke = inputEvent.KeyEvent; + //System.Diagnostics.Debug.WriteLine ($"fromPacketKey: {fromPacketKey}"); + //if (ke.UnicodeChar == '\0') { + // System.Diagnostics.Debug.WriteLine ("UnicodeChar: 0'\\0'"); + //} else if (ke.UnicodeChar == 13) { + // System.Diagnostics.Debug.WriteLine ("UnicodeChar: 13'\\n'"); + //} else { + // System.Diagnostics.Debug.WriteLine ($"UnicodeChar: {(uint)ke.UnicodeChar}'{ke.UnicodeChar}'"); + //} + //System.Diagnostics.Debug.WriteLine ($"bKeyDown: {ke.bKeyDown}"); + //System.Diagnostics.Debug.WriteLine ($"dwControlKeyState: {ke.dwControlKeyState}"); + //System.Diagnostics.Debug.WriteLine ($"wRepeatCount: {ke.wRepeatCount}"); + //System.Diagnostics.Debug.WriteLine ($"wVirtualKeyCode: {ke.wVirtualKeyCode}"); + //System.Diagnostics.Debug.WriteLine ($"wVirtualScanCode: {ke.wVirtualScanCode}"); + if (map == (Key)0xffffffff) { KeyEvent key = new KeyEvent (); @@ -854,6 +875,9 @@ namespace Terminal.Gui { keyUpHandler (key); } else { if (inputEvent.KeyEvent.bKeyDown) { + // May occurs using SendKeys + if (keyModifiers == null) + keyModifiers = new KeyModifiers (); // Key Down - Fire KeyDown Event and KeyStroke (ProcessKey) Event keyDownHandler (new KeyEvent (map, keyModifiers)); keyHandler (new KeyEvent (map, keyModifiers)); @@ -861,7 +885,7 @@ namespace Terminal.Gui { keyUpHandler (new KeyEvent (map, keyModifiers)); } } - if (!inputEvent.KeyEvent.bKeyDown) { + if (!inputEvent.KeyEvent.bKeyDown && inputEvent.KeyEvent.dwControlKeyState == 0) { keyModifiers = null; } break; @@ -1242,7 +1266,38 @@ namespace Terminal.Gui { keyModifiers.Scrolllock = scrolllock; var ConsoleKeyInfo = new ConsoleKeyInfo (keyEvent.UnicodeChar, (ConsoleKey)keyEvent.wVirtualKeyCode, shift, alt, control); - return new WindowsConsole.ConsoleKeyInfoEx (ConsoleKeyInfo, capslock, numlock); + + return new WindowsConsole.ConsoleKeyInfoEx (ConsoleKeyInfo, capslock, numlock, scrolllock); + } + + public WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsole.KeyEventRecord keyEvent) + { + if (keyEvent.wVirtualKeyCode != (uint)ConsoleKey.Packet) { + return keyEvent; + } + + var mod = new ConsoleModifiers (); + if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed)) { + mod |= ConsoleModifiers.Shift; + } + if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed) || + keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed)) { + mod |= ConsoleModifiers.Alt; + } + if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed) || + keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed)) { + mod |= ConsoleModifiers.Control; + } + var keyChar = ConsoleKeyMapping.GetKeyCharFromConsoleKey (keyEvent.UnicodeChar, mod, out uint virtualKey, out uint scanCode); + + return new WindowsConsole.KeyEventRecord { + UnicodeChar = (char)keyChar, + bKeyDown = keyEvent.bKeyDown, + dwControlKeyState = keyEvent.dwControlKeyState, + wRepeatCount = keyEvent.wRepeatCount, + wVirtualKeyCode = (ushort)virtualKey, + wVirtualScanCode = (ushort)scanCode + }; } public Key MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx) @@ -1253,6 +1308,8 @@ namespace Terminal.Gui { return MapKeyModifiers (keyInfo, Key.Esc); case ConsoleKey.Tab: return keyInfo.Modifiers == ConsoleModifiers.Shift ? Key.BackTab : Key.Tab; + case ConsoleKey.Clear: + return MapKeyModifiers (keyInfo, Key.Clear); case ConsoleKey.Home: return MapKeyModifiers (keyInfo, Key.Home); case ConsoleKey.End: @@ -1279,6 +1336,8 @@ namespace Terminal.Gui { return MapKeyModifiers (keyInfo, Key.DeleteChar); case ConsoleKey.Insert: return MapKeyModifiers (keyInfo, Key.InsertChar); + case ConsoleKey.PrintScreen: + return MapKeyModifiers (keyInfo, Key.PrintScreen); case ConsoleKey.NumPad0: return keyInfoEx.NumLock ? Key.D0 : Key.InsertChar; @@ -1331,6 +1390,9 @@ namespace Terminal.Gui { if (keyInfo.Modifiers == ConsoleModifiers.Alt) { return (Key)(((uint)Key.AltMask) | ((uint)Key.A + delta)); } + if (keyInfo.Modifiers == (ConsoleModifiers.Shift | ConsoleModifiers.Alt)) { + return MapKeyModifiers (keyInfo, (Key)((uint)Key.A + delta)); + } if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { if (keyInfo.KeyChar == 0 || (keyInfo.KeyChar != 0 && keyInfo.KeyChar >= 1 && keyInfo.KeyChar <= 26)) { return MapKeyModifiers (keyInfo, (Key)((uint)Key.A + delta)); @@ -1347,8 +1409,11 @@ namespace Terminal.Gui { if (keyInfo.Modifiers == ConsoleModifiers.Control) { return (Key)(((uint)Key.CtrlMask) | ((uint)Key.D0 + delta)); } + if (keyInfo.Modifiers == (ConsoleModifiers.Shift | ConsoleModifiers.Alt)) { + return MapKeyModifiers (keyInfo, (Key)((uint)Key.D0 + delta)); + } if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { - if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30) { + if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30 || keyInfo.KeyChar == ((uint)Key.D0 + delta)) { return MapKeyModifiers (keyInfo, (Key)((uint)Key.D0 + delta)); } } @@ -1369,7 +1434,7 @@ namespace Terminal.Gui { return (Key)(0xffffffff); } - Key MapKeyModifiers (ConsoleKeyInfo keyInfo, Key key) + private Key MapKeyModifiers (ConsoleKeyInfo keyInfo, Key key) { Key keyMod = new Key (); if ((keyInfo.Modifiers & ConsoleModifiers.Shift) != 0) @@ -1386,16 +1451,20 @@ namespace Terminal.Gui { { TerminalResized = terminalResized; - var winSize = WinConsole.GetConsoleOutputWindow (out Point pos); - cols = winSize.Width; - rows = winSize.Height; + try { + var winSize = WinConsole.GetConsoleOutputWindow (out Point pos); + cols = winSize.Width; + rows = winSize.Height; - WindowsConsole.SmallRect.MakeEmpty (ref damageRegion); + WindowsConsole.SmallRect.MakeEmpty (ref damageRegion); - ResizeScreen (); - UpdateOffScreen (); + ResizeScreen (); + UpdateOffScreen (); - CreateColors (); + CreateColors (); + } catch (Win32Exception e) { + throw new InvalidOperationException ("The Windows Console output window is not available.", e); + } } public override void ResizeScreen () @@ -1435,11 +1504,16 @@ namespace Terminal.Gui { crow = row; } + int GetOutputBufferPosition () + { + return crow * Cols + ccol; + } + public override void AddRune (Rune rune) { rune = MakePrintable (rune); var runeWidth = Rune.ColumnWidth (rune); - var position = crow * Cols + ccol; + var position = GetOutputBufferPosition (); var validClip = IsValidContent (ccol, crow, Clip); if (validClip) { @@ -1453,7 +1527,7 @@ namespace Terminal.Gui { } else if (runeWidth < 2 && ccol <= Clip.Right - 1 && Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) { - var prevPosition = crow * Cols + ccol + 1; + var prevPosition = GetOutputBufferPosition () + 1; OutputBuffer [prevPosition].Char.UnicodeChar = (char)' '; contents [crow, ccol + 1, 0] = (int)(uint)' '; @@ -1474,7 +1548,7 @@ namespace Terminal.Gui { ccol++; if (runeWidth > 1) { if (validClip && ccol < Clip.Right) { - position = crow * Cols + ccol; + position = GetOutputBufferPosition (); OutputBuffer [position].Attributes = (ushort)currentAttribute; OutputBuffer [position].Char.UnicodeChar = (char)0x00; contents [crow, ccol, 0] = (int)(uint)0x00; @@ -1660,9 +1734,7 @@ namespace Terminal.Gui { } keyEvent.UnicodeChar = keyChar; - if ((shift || alt || control) - && (key >= ConsoleKey.A && key <= ConsoleKey.Z - || key >= ConsoleKey.D0 && key <= ConsoleKey.D9)) { + if ((uint)key < 255) { keyEvent.wVirtualKeyCode = (ushort)key; } else { keyEvent.wVirtualKeyCode = '\0'; diff --git a/Terminal.Gui/Core/Application.cs b/Terminal.Gui/Core/Application.cs index f0764905c..b5f6e5984 100644 --- a/Terminal.Gui/Core/Application.cs +++ b/Terminal.Gui/Core/Application.cs @@ -39,6 +39,7 @@ namespace Terminal.Gui { /// }; /// Application.Top.Add(win); /// Application.Run(); + /// Application.Shutdown(); /// /// /// @@ -222,19 +223,28 @@ namespace Terminal.Gui { public static bool ExitRunLoopAfterFirstIteration { get; set; } = false; /// - /// Notify that a new token was created, - /// used if is true. + /// Notify that a new was created ( was called). The token is created in + /// and this event will be fired before that function exits. /// + /// + /// If is callers to + /// must also subscribe to + /// and manually dispose of the token when the application is done. + /// public static event Action NotifyNewRunState; /// - /// Notify that a existent token is stopping, - /// used if is true. + /// Notify that a existent is stopping ( was called). /// + /// + /// If is callers to + /// must also subscribe to + /// and manually dispose of the token when the application is done. + /// public static event Action NotifyStopRunState; /// - /// This event is raised on each iteration of the + /// This event is raised on each iteration of the . /// /// /// See also @@ -299,36 +309,59 @@ namespace Terminal.Gui { /// public static bool UseSystemConsole; + // For Unit testing - ignores UseSystemConsole + internal static bool ForceFakeConsole; + /// /// Initializes a new instance of Application. /// - /// /// /// Call this method once per instance (or after has been called). /// /// - /// Loads the right for the platform. + /// This function loads the right for the platform, + /// Creates a . and assigns it to /// /// - /// 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. /// - /// - public static void Init (ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) => Init (() => Toplevel.Create (), driver, mainLoopDriver); + /// + /// The function + /// combines and + /// into a single call. An applciation cam use + /// without explicitly calling . + /// + /// + /// The to use. If not specified the default driver for the + /// platform will be used (see , , and ). + /// + /// Specifies the to use. + /// Must not be if is not . + /// + public static void Init (ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) => InternalInit (() => Toplevel.Create (), driver, mainLoopDriver); internal static bool _initialized = false; internal static int _mainThreadId = -1; - /// - /// Initializes the Terminal.Gui application - /// - static void Init (Func topLevelFactory, ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) + // INTERNAL function for initializing an app with a Toplevel factory object, driver, and mainloop. + // + // Called from: + // + // Init() - When the user wants to use the default Toplevel. calledViaRunT will be false, causing all state to be reset. + // Run() - When the user wants to use a custom Toplevel. calledViaRunT will be true, enabling Run() to be called without calling Init first. + // Unit Tests - To initialize the app with a custom Toplevel, using the FakeDriver. calledViaRunT will be false, causing all state to be reset. + // + // calledViaRunT: If false (default) all state will be reset. If true the state will not be reset. + internal static void InternalInit (Func topLevelFactory, ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null, bool calledViaRunT = false) { if (_initialized && driver == null) return; if (_initialized) { - throw new InvalidOperationException ("Init must be bracketed by Shutdown"); + throw new InvalidOperationException ("Init has already been called and must be bracketed by Shutdown."); } + // Note in this case, we don't verify the type of the Toplevel created by new T(). // Used only for start debugging on Unix. //#if DEBUG // while (!System.Diagnostics.Debugger.IsAttached) { @@ -337,36 +370,69 @@ namespace Terminal.Gui { // System.Diagnostics.Debugger.Break (); //#endif - // Reset all class variables (Application is a singleton). - ResetState (); + if (!calledViaRunT) { + // Reset all class variables (Application is a singleton). + ResetState (); + } - // This supports Unit Tests and the passing of a mock driver/loopdriver + // For UnitTests if (driver != null) { - if (mainLoopDriver == null) { - throw new ArgumentNullException ("mainLoopDriver cannot be null if driver is provided."); - } + //if (mainLoopDriver == null) { + // throw new ArgumentNullException ("InternalInit mainLoopDriver cannot be null if driver is provided."); + //} + //if (!(driver is FakeDriver)) { + // throw new InvalidOperationException ("InternalInit can only be called with FakeDriver."); + //} Driver = driver; - Driver.Init (TerminalResized); - MainLoop = new MainLoop (mainLoopDriver); - SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop)); } if (Driver == null) { var p = Environment.OSVersion.Platform; - if (UseSystemConsole) { + if (ForceFakeConsole) { + // For Unit Testing only + Driver = new FakeDriver (); + } else if (UseSystemConsole) { Driver = new NetDriver (); - mainLoopDriver = new NetMainLoop (Driver); } else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) { Driver = new WindowsDriver (); - mainLoopDriver = new WindowsMainLoop (Driver); } else { - mainLoopDriver = new UnixMainLoop (); Driver = new CursesDriver (); } - Driver.Init (TerminalResized); - MainLoop = new MainLoop (mainLoopDriver); - SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop)); + if (Driver == null) { + throw new InvalidOperationException ("Init could not determine the ConsoleDriver to use."); + } } + + if (mainLoopDriver == null) { + // TODO: Move this logic into ConsoleDriver + if (Driver is FakeDriver) { + mainLoopDriver = new FakeMainLoop (Driver); + } else if (Driver is NetDriver) { + mainLoopDriver = new NetMainLoop (Driver); + } else if (Driver is WindowsDriver) { + mainLoopDriver = new WindowsMainLoop (Driver); + } else if (Driver is CursesDriver) { + mainLoopDriver = new UnixMainLoop (Driver); + } + if (mainLoopDriver == null) { + throw new InvalidOperationException ("Init could not determine the MainLoopDriver to use."); + } + } + + MainLoop = new MainLoop (mainLoopDriver); + + try { + Driver.Init (TerminalResized); + } catch (InvalidOperationException ex) { + // This is a case where the driver is unable to initialize the console. + // This can happen if the console is already in use by another process or + // if running in unit tests. + // In this case, we want to throw a more specific exception. + throw new InvalidOperationException ("Unable to initialize the console. This can happen if the console is already in use by another process or in unit tests.", ex); + } + + SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop)); + Top = topLevelFactory (); Current = Top; supportedCultures = GetSupportedCultures (); @@ -375,7 +441,7 @@ namespace Terminal.Gui { } /// - /// Captures the execution state for the provided view. + /// Captures the execution state for the provided view. /// public class RunState : IDisposable { /// @@ -391,31 +457,61 @@ namespace Terminal.Gui { /// public Toplevel Toplevel { get; internal set; } +#if DEBUG_IDISPOSABLE /// - /// Releases alTop = l resource used by the object. + /// For debug purposes to verify objects are being disposed properly /// - /// Call when you are finished using the . The + public bool WasDisposed = false; + /// + /// For debug purposes to verify objects are being disposed properly + /// + public int DisposedCount = 0; + /// + /// For debug purposes + /// + public static List Instances = new List (); + /// + /// For debug purposes + /// + public RunState () + { + Instances.Add (this); + } +#endif + + /// + /// 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. + /// was occupying. + /// public void Dispose () { Dispose (true); GC.SuppressFinalize (this); +#if DEBUG_IDISPOSABLE + WasDisposed = true; +#endif } /// - /// Dispose the specified disposing. + /// Releases all resource used by the object. /// - /// The dispose. - /// If set to true disposing. + /// If set to we are disposing and should dispose held objects. protected virtual void Dispose (bool disposing) { if (Toplevel != null && disposing) { - End (Toplevel); - Toplevel.Dispose (); - Toplevel = null; + throw new InvalidOperationException ("You must clean up (Dispose) the Toplevel before calling Application.RunState.Dispose"); + // BUGBUG: It's insidious that we call EndFirstTopLevel here so I moved it to End. + //EndFirstTopLevel (Toplevel); + //Toplevel.Dispose (); + //Toplevel = null; } } } @@ -670,10 +766,14 @@ namespace Terminal.Gui { me.View = view; } RootMouseEvent?.Invoke (me); + + if (me.Handled) { + return; + } + if (mouseGrabView != null) { if (view == null) { - UngrabMouse (); - return; + view = mouseGrabView; } var newxy = mouseGrabView.ScreenToView (me.X, me.Y); @@ -688,7 +788,7 @@ namespace Terminal.Gui { if (OutsideFrame (new Point (nme.X, nme.Y), mouseGrabView.Frame)) { lastMouseOwnerView?.OnMouseLeave (me); } - // System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}"); + //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}"); if (mouseGrabView?.OnMouseEvent (nme) == true) { return; } @@ -797,8 +897,8 @@ namespace Terminal.Gui { /// /// Building block API: Prepares the provided for execution. /// - /// The runstate handle that needs to be passed to the method upon completion. - /// Toplevel to prepare execution for. + /// The handle that needs to be passed to the method upon completion. + /// The to prepare execution for. /// /// This method prepares the provided toplevel for running with the focus, /// it adds this to the list of toplevels, sets up the mainloop to process the @@ -811,13 +911,12 @@ namespace Terminal.Gui { { if (toplevel == null) { throw new ArgumentNullException (nameof (toplevel)); - } else if (toplevel.IsMdiContainer && MdiTop != null) { + } else if (toplevel.IsMdiContainer && MdiTop != toplevel && MdiTop != null) { throw new InvalidOperationException ("Only one Mdi Container is allowed."); } var rs = new RunState (toplevel); - Init (); if (toplevel is ISupportInitializeNotification initializableNotification && !initializableNotification.IsInitialized) { initializableNotification.BeginInit (); @@ -828,6 +927,13 @@ namespace Terminal.Gui { } lock (toplevels) { + // If Top was already initialized with Init, and Begin has never been called + // Top was not added to the toplevels Stack. It will thus never get disposed. + // Clean it up here: + if (Top != null && toplevel != Top && !toplevels.Contains (Top)) { + Top.Dispose (); + Top = null; + } if (string.IsNullOrEmpty (toplevel.Id.ToString ())) { var count = 1; var id = (toplevels.Count + count).ToString (); @@ -849,7 +955,8 @@ namespace Terminal.Gui { throw new ArgumentException ("There are duplicates toplevels Id's"); } } - if (toplevel.IsMdiContainer) { + // Fix $520 - Set Top = toplevel if Top == null + if (Top == null || toplevel.IsMdiContainer) { Top = toplevel; } @@ -888,13 +995,14 @@ namespace Terminal.Gui { Driver.Refresh (); } + NotifyNewRunState?.Invoke (rs); return rs; } /// - /// Building block API: completes the execution of a that was started with . + /// Building block API: completes the execution of a that was started with . /// - /// The runstate returned by the method. + /// The returned by the method. public static void End (RunState runState) { if (runState == null) @@ -905,12 +1013,52 @@ namespace Terminal.Gui { } else { runState.Toplevel.OnUnloaded (); } + + // End the RunState.Toplevel + // First, take it off the toplevel Stack + if (toplevels.Count > 0) { + if (toplevels.Peek () != runState.Toplevel) { + // If there the top of the stack is not the RunState.Toplevel then + // this call to End is not balanced with the call to Begin that started the RunState + throw new ArgumentException ("End must be balanced with calls to Begin"); + } + toplevels.Pop (); + } + + // Notify that it is closing + runState.Toplevel?.OnClosed (runState.Toplevel); + + // If there is a MdiTop that is not the RunState.Toplevel then runstate.TopLevel + // is a child of MidTop and we should notify the MdiTop that it is closing + if (MdiTop != null && !(runState.Toplevel).Modal && runState.Toplevel != MdiTop) { + MdiTop.OnChildClosed (runState.Toplevel); + } + + // Set Current and Top to the next TopLevel on the stack + if (toplevels.Count == 0) { + Current = null; + } else { + Current = toplevels.Peek (); + if (toplevels.Count == 1 && Current == MdiTop) { + MdiTop.OnAllChildClosed (); + } else { + SetCurrentAsTop (); + } + Refresh (); + } + + runState.Toplevel?.Dispose (); + runState.Toplevel = null; runState.Dispose (); } /// - /// Shutdown an application initialized with + /// 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. + /// public static void Shutdown () { ResetState (); @@ -925,15 +1073,17 @@ namespace Terminal.Gui { // Shutdown is the bookend for Init. As such it needs to clean up all resources // Init created. Apps that do any threading will need to code defensively for this. // e.g. see Issue #537 - // TODO: Some of this state is actually related to Begin/End (not Init/Shutdown) and should be moved to `RunState` (#520) foreach (var t in toplevels) { t.Running = false; t.Dispose (); } toplevels.Clear (); Current = null; + Top?.Dispose (); Top = null; + // BUGBUG: MdiTop is not cleared here, but it should be? + MainLoop = null; Driver?.End (); Driver = null; @@ -985,40 +1135,17 @@ namespace Terminal.Gui { Driver.Refresh (); } - internal static void End (View view) - { - if (toplevels.Peek () != view) - throw new ArgumentException ("The view that you end with must be balanced"); - toplevels.Pop (); - (view as Toplevel)?.OnClosed ((Toplevel)view); - - if (MdiTop != null && !((Toplevel)view).Modal && view != MdiTop) { - MdiTop.OnChildClosed (view as Toplevel); - } - - if (toplevels.Count == 0) { - Current = null; - } else { - Current = toplevels.Peek (); - if (toplevels.Count == 1 && Current == MdiTop) { - MdiTop.OnAllChildClosed (); - } else { - SetCurrentAsTop (); - } - Refresh (); - } - } /// - /// Building block API: Runs the main loop for the created dialog + /// Building block API: Runs the for the created . /// /// - /// Use the wait parameter to control whether this is a - /// blocking or non-blocking call. + /// Use the parameter to control whether this is a blocking or non-blocking call. /// - /// The state returned by the Begin method. - /// By default this is true which will execute the runloop waiting for events, if you pass false, you can use this method to run a single iteration of the events. + /// The state returned by the method. + /// By default this is which will execute the runloop waiting for events, + /// if set to , a single iteration will execute. public static void RunLoop (RunState state, bool wait = true) { if (state == null) @@ -1028,18 +1155,21 @@ namespace Terminal.Gui { bool firstIteration = true; for (state.Toplevel.Running = true; state.Toplevel.Running;) { - if (ExitRunLoopAfterFirstIteration && !firstIteration) + if (ExitRunLoopAfterFirstIteration && !firstIteration) { return; + } RunMainLoopIteration (ref state, wait, ref firstIteration); } } /// - /// Run one iteration of the MainLoop. + /// Run one iteration of the . /// - /// The state returned by the Begin method. - /// If will execute the runloop waiting for events. - /// If it's the first run loop iteration. + /// The state returned by . + /// If will execute the runloop waiting for events. If + /// will return after a single iteration. + /// Set to if this is the first run loop iteration. Upon return, + /// it will be set to if at least one iteration happened. public static void RunMainLoopIteration (ref RunState state, bool wait, ref bool firstIteration) { if (MainLoop.EventsPending (wait)) { @@ -1140,30 +1270,57 @@ namespace Terminal.Gui { } /// - /// Runs the application by calling with the value of + /// Runs the application by calling with the value of . /// + /// + /// See for more details. + /// public static void Run (Func errorHandler = null) { Run (Top, errorHandler); } /// - /// Runs the application by calling with a new instance of the specified -derived class + /// Runs the application by calling + /// with a new instance of the specified -derived class. + /// + /// Calling first is not needed as this function will initialze the application. + /// + /// + /// must be called when the application is closing (typically after Run> has + /// returned) to ensure resources are cleaned up and terminal settings restored. + /// /// - public static void Run (Func errorHandler = null) where T : Toplevel, new() + /// + /// See for more details. + /// + /// + /// The to use. If not specified the default driver for the + /// platform will be used (, , or ). + /// This parameteter must be if has already been called. + /// + /// Specifies the to use. + public static void Run (Func errorHandler = null, ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) where T : Toplevel, new() { - if (_initialized && Driver != null) { - var top = new T (); - var type = top.GetType ().BaseType; - while (type != typeof (Toplevel) && type != typeof (object)) { - type = type.BaseType; + if (_initialized) { + if (Driver != null) { + // Init() has been called and we have a driver, so just run the app. + var top = new T (); + var type = top.GetType ().BaseType; + while (type != typeof (Toplevel) && type != typeof (object)) { + type = type.BaseType; + } + if (type != typeof (Toplevel)) { + throw new ArgumentException ($"{top.GetType ().Name} must be derived from TopLevel"); + } + Run (top, errorHandler); + } else { + // This codepath should be impossible because Init(null, null) will select the platform default driver + throw new InvalidOperationException ("Init() completed without a driver being set (this should be impossible); Run() cannot be called."); } - if (type != typeof (Toplevel)) { - throw new ArgumentException ($"{top.GetType ().Name} must be derived from TopLevel"); - } - Run (top, errorHandler); } else { - Init (() => new T ()); + // Init() has NOT been called. + InternalInit (() => new T (), driver, mainLoopDriver, calledViaRunT: true); Run (Top, errorHandler); } } @@ -1187,16 +1344,19 @@ namespace Terminal.Gui { /// /// Alternatively, to have a program control the main loop and /// process events manually, call to set things up manually and then - /// repeatedly call with the wait parameter set to false. By doing this + /// repeatedly call with the wait parameter set to false. By doing this /// the method will only process any pending events, timers, idle handlers and /// then return control immediately. /// /// - /// When is null the exception is rethrown, when it returns true the application is resumed and when false method exits gracefully. + /// RELEASE builds only: When is any exeptions will be rethrown. + /// Otheriwse, if will be called. If + /// returns the will resume; otherwise + /// this method will exit. /// /// /// The to run modally. - /// Handler for any unhandled exceptions (resumes when returns true, rethrows when null). + /// RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true, rethrows when null). public static void Run (Toplevel view, Func errorHandler = null) { var resume = true; @@ -1206,13 +1366,12 @@ namespace Terminal.Gui { #endif resume = false; var runToken = Begin (view); + // If ExitRunLoopAfterFirstIteration is true then the user must dispose of the runToken + // by using NotifyStopRunState event. RunLoop (runToken); - if (!ExitRunLoopAfterFirstIteration) + if (!ExitRunLoopAfterFirstIteration) { End (runToken); - else - // If ExitRunLoopAfterFirstIteration is true then the user must deal his disposing when it ends - // by using NotifyStopRunState event. - NotifyNewRunState?.Invoke (runToken); + } #if !DEBUG } catch (Exception error) @@ -1305,8 +1464,9 @@ namespace Terminal.Gui { static void OnNotifyStopRunState (Toplevel top) { - if (ExitRunLoopAfterFirstIteration) + if (ExitRunLoopAfterFirstIteration) { NotifyStopRunState?.Invoke (top); + } } /// diff --git a/Terminal.Gui/Core/Clipboard/Clipboard.cs b/Terminal.Gui/Core/Clipboard/Clipboard.cs index 914dd0153..4aed183da 100644 --- a/Terminal.Gui/Core/Clipboard/Clipboard.cs +++ b/Terminal.Gui/Core/Clipboard/Clipboard.cs @@ -3,13 +3,32 @@ using System; namespace Terminal.Gui { /// - /// Provides cut, copy, and paste support for the clipboard with OS interaction. + /// Provides cut, copy, and paste support for the OS clipboard. /// + /// + /// + /// 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 the xclip command line tool. If xclip is not installed, + /// the clipboard will not work. + /// + /// + /// On Linux, when running under Windows Subsystem for Linux (WSL), + /// the class launches Windows' powershell.exe via WSL interop and uses the + /// "Set-Clipboard" and "Get-Clipboard" Powershell CmdLets. + /// + /// + /// On the Mac, the class uses the MacO OS X pbcopy and pbpaste command line tools + /// and the Mac clipboard APIs vai P/Invoke. + /// + /// public static class Clipboard { static ustring contents; /// - /// Get or sets the operation system clipboard, otherwise the contents field. + /// Gets (copies from) or sets (pastes to) the contents of the OS clipboard. /// public static ustring Contents { get { @@ -25,10 +44,15 @@ namespace Terminal.Gui { } set { try { - if (IsSupported && value != null) { + if (IsSupported) { + if (value == null) { + value = string.Empty; + } Application.Driver.Clipboard.SetClipboardData (value.ToString ()); } contents = value; + } catch (NotSupportedException e) { + throw e; } catch (Exception) { contents = value; } @@ -38,32 +62,35 @@ namespace Terminal.Gui { /// /// Returns true if the environmental dependencies are in place to interact with the OS clipboard. /// + /// + /// public static bool IsSupported { get => Application.Driver.Clipboard.IsSupported; } /// - /// Gets the operation system clipboard if possible. + /// Copies the contents of the OS clipboard to if possible. /// - /// Clipboard contents read - /// true if it was possible to read the OS clipboard. + /// The contents of the OS clipboard if successful, if not. + /// the OS clipboard was retrieved, otherwise. public static bool TryGetClipboardData (out string result) { - if (Application.Driver.Clipboard.TryGetClipboardData (out result)) { + if (IsSupported && Application.Driver.Clipboard.TryGetClipboardData (out result)) { if (contents != result) { contents = result; } return true; } + result = string.Empty; return false; } /// - /// Sets the operation system clipboard if possible. + /// Pastes the to the OS clipboard if possible. /// - /// - /// True if the clipboard content was set successfully. + /// The text to paste to the OS clipboard. + /// the OS clipboard was set, otherwise. public static bool TrySetClipboardData (string text) { - if (Application.Driver.Clipboard.TrySetClipboardData (text)) { + if (IsSupported && Application.Driver.Clipboard.TrySetClipboardData (text)) { contents = text; return true; } diff --git a/Terminal.Gui/Core/Clipboard/ClipboardBase.cs b/Terminal.Gui/Core/Clipboard/ClipboardBase.cs index db61af80f..ee2e437d4 100644 --- a/Terminal.Gui/Core/Clipboard/ClipboardBase.cs +++ b/Terminal.Gui/Core/Clipboard/ClipboardBase.cs @@ -15,48 +15,52 @@ namespace Terminal.Gui { public abstract bool IsSupported { get; } /// - /// Get the operation system clipboard. + /// Returns the contents of the OS clipboard if possible. /// - /// Thrown if it was not possible to read the clipboard contents + /// The contents of the OS clipboard if successful. + /// Thrown if it was not possible to copy from the OS clipboard. public string GetClipboardData () { try { return GetClipboardDataImpl (); - } catch (Exception ex) { - throw new NotSupportedException ("Failed to read clipboard.", ex); + } catch (NotSupportedException ex) { + throw new NotSupportedException ("Failed to copy from the OS clipboard.", ex); } } /// - /// Get the operation system clipboard. + /// Returns the contents of the OS clipboard if possible. Implemented by -specific subclasses. /// + /// The contents of the OS clipboard if successful. + /// Thrown if it was not possible to copy from the OS clipboard. protected abstract string GetClipboardDataImpl (); /// - /// Sets the operation system clipboard. + /// Pastes the to the OS clipboard if possible. /// - /// - /// Thrown if it was not possible to set the clipboard contents + /// The text to paste to the OS clipboard. + /// Thrown if it was not possible to paste to the OS clipboard. public void SetClipboardData (string text) { try { SetClipboardDataImpl (text); - } catch (Exception ex) { - throw new NotSupportedException ("Failed to write to clipboard.", ex); + } catch (NotSupportedException ex) { + throw new NotSupportedException ("Failed to paste to the OS clipboard.", ex); } } /// - /// Sets the operation system clipboard. + /// Pastes the to the OS clipboard if possible. Implemented by -specific subclasses. /// - /// + /// The text to paste to the OS clipboard. + /// Thrown if it was not possible to paste to the OS clipboard. protected abstract void SetClipboardDataImpl (string text); /// - /// Gets the operation system clipboard if possible. + /// Copies the contents of the OS clipboard to if possible. /// - /// Clipboard contents read - /// true if it was possible to read the OS clipboard. + /// The contents of the OS clipboard if successful, if not. + /// the OS clipboard was retrieved, otherwise. public bool TryGetClipboardData (out string result) { // Don't even try to read because environment is not set up. @@ -71,17 +75,18 @@ namespace Terminal.Gui { result = GetClipboardDataImpl (); } return true; - } catch (Exception) { + } catch (NotSupportedException ex) { + System.Diagnostics.Debug.WriteLine ($"TryGetClipboardData: {ex.Message}"); result = null; return false; } } /// - /// Sets the operation system clipboard if possible. + /// Pastes the to the OS clipboard if possible. /// - /// - /// True if the clipboard content was set successfully + /// The text to paste to the OS clipboard. + /// the OS clipboard was set, otherwise. public bool TrySetClipboardData (string text) { // Don't even try to set because environment is not set up @@ -92,7 +97,8 @@ namespace Terminal.Gui { try { SetClipboardDataImpl (text); return true; - } catch (Exception) { + } catch (NotSupportedException ex) { + System.Diagnostics.Debug.WriteLine ($"TrySetClipboardData: {ex.Message}"); return false; } } diff --git a/Terminal.Gui/Core/CollectionNavigator.cs b/Terminal.Gui/Core/CollectionNavigator.cs new file mode 100644 index 000000000..6637daf20 --- /dev/null +++ b/Terminal.Gui/Core/CollectionNavigator.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Terminal.Gui { + /// + /// 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. + /// + /// 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. + /// + /// + /// If the user pauses keystrokes for a short time (see ), the search string is cleared. + /// + /// + public class CollectionNavigator { + /// + /// Constructs a new CollectionNavigator. + /// + public CollectionNavigator () { } + + /// + /// Constructs a new CollectionNavigator for the given collection. + /// + /// + public CollectionNavigator (IEnumerable collection) => Collection = collection; + + DateTime lastKeystroke = DateTime.Now; + /// + /// 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. + /// + public int TypingDelay { get; set; } = 500; + + /// + /// The compararer function to use when searching the collection. + /// + public StringComparer Comparer { get; set; } = StringComparer.InvariantCultureIgnoreCase; + + /// + /// The collection of objects to search. is used to search the collection. + /// + public IEnumerable Collection { get; set; } + + /// + /// Event arguments for the event. + /// + public class KeystrokeNavigatorEventArgs { + /// + /// he current . + /// + public string SearchString { get; } + + /// + /// Initializes a new instance of + /// + /// The current . + public KeystrokeNavigatorEventArgs (string searchString) + { + SearchString = searchString; + } + } + + /// + /// This event is invoked when changes. Useful for debugging. + /// + public event Action SearchStringChanged; + + private string _searchString = ""; + /// + /// Gets the current search string. This includes the set of keystrokes that have been pressed + /// since the last unsuccessful match or after ) milliseconds. Useful for debugging. + /// + public string SearchString { + get => _searchString; + private set { + _searchString = value; + OnSearchStringChanged (new KeystrokeNavigatorEventArgs (value)); + } + } + + /// + /// Invoked when the changes. Useful for debugging. Invokes the event. + /// + /// + public virtual void OnSearchStringChanged (KeystrokeNavigatorEventArgs e) + { + SearchStringChanged?.Invoke (e); + } + + /// + /// Gets the index of the next item in the collection that matches the current plus the provided character (typically + /// from a key press). + /// + /// 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 collection matched. + public int GetNextMatchingItem (int currentIndex, char keyStruck) + { + AssertCollectionIsNotNull (); + if (!char.IsControl (keyStruck)) { + + // maybe user pressed 'd' and now presses 'd' again. + // a candidate search is things that begin with "dd" + // but if we find none then we must fallback on cycling + // d instead and discard the candidate state + string candidateState = ""; + + // is it a second or third (etc) keystroke within a short time + if (SearchString.Length > 0 && DateTime.Now - lastKeystroke < TimeSpan.FromMilliseconds (TypingDelay)) { + // "dd" is a candidate + candidateState = SearchString + keyStruck; + } else { + // its a fresh keystroke after some time + // or its first ever key press + SearchString = new string (keyStruck, 1); + } + + var 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); + + if (idxCandidate != -1) { + // found "dd" so candidate searchstring is accepted + lastKeystroke = DateTime.Now; + SearchString = candidateState; + return idxCandidate; + } + + //// nothing matches "dd" so discard it as a candidate + //// and just cycle "d" instead + lastKeystroke = DateTime.Now; + idxCandidate = GetNextMatchingItem (currentIndex, candidateState); + + // 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) { + // 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) { + // clear history and treat as a fresh letter + ClearSearchString (); + + // match on the fresh letter alone + SearchString = new string (keyStruck, 1); + idxCandidate = GetNextMatchingItem (currentIndex, SearchString); + return idxCandidate == -1 ? currentIndex : idxCandidate; + } + + // Found another "d" or just leave index as it was + return idxCandidate; + + } else { + // clear state because keypress was a control char + ClearSearchString (); + + // control char indicates no selection + return -1; + } + } + + /// + /// 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. + /// Set to to stop the search on the first match + /// if there are multiple matches for . + /// e.g. "ca" + 'r' should stay on "car" and not jump to "cart". If (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) + { + if (string.IsNullOrEmpty (search)) { + return -1; + } + AssertCollectionIsNotNull (); + + // find indexes of items that start with the search text + int [] matchingIndexes = Collection.Select ((item, idx) => (item, idx)) + .Where (k => k.item?.ToString ().StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false) + .Select (k => k.idx) + .ToArray (); + + // if there are items beginning with search + if (matchingIndexes.Length > 0) { + // is one of them currently selected? + var currentlySelected = Array.IndexOf (matchingIndexes, currentIndex); + + if (currentlySelected == -1) { + // we are not currently selecting any item beginning with the search + // so jump to first item in list that begins with the letter + return matchingIndexes [0]; + } else { + + // the current index is part of the matching collection + if (minimizeMovement) { + // if we would rather not jump around (e.g. user is typing lots of text to get this match) + return matchingIndexes [currentlySelected]; + } + + // cycle to next (circular) + return matchingIndexes [(currentlySelected + 1) % matchingIndexes.Length]; + } + } + + // nothing starts with the search + return -1; + } + + private void AssertCollectionIsNotNull () + { + if (Collection == null) { + throw new InvalidOperationException ("Collection is null"); + } + } + + private void ClearSearchString () + { + SearchString = ""; + lastKeystroke = DateTime.Now; + } + + /// + /// Returns true if is a searchable key + /// (e.g. letters, numbers etc) that is valid to pass to to this + /// class for search filtering. + /// + /// + /// + public static bool IsCompatibleKey (KeyEvent kb) + { + return !kb.IsAlt && !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock; + } + } +} diff --git a/Terminal.Gui/Core/Command.cs b/Terminal.Gui/Core/Command.cs index 42f0d0f1e..9d106f664 100644 --- a/Terminal.Gui/Core/Command.cs +++ b/Terminal.Gui/Core/Command.cs @@ -10,54 +10,54 @@ namespace Terminal.Gui { public enum Command { /// - /// Moves the caret down one line. + /// Moves down one item (cell, line, etc...). /// LineDown, /// - /// Extends the selection down one line. + /// Extends the selection down one (cell, line, etc...). /// LineDownExtend, /// - /// Moves the caret down to the last child node of the branch that holds the current selection + /// Moves down to the last child node of the branch that holds the current selection. /// LineDownToLastBranch, /// - /// Scrolls down one line (without changing the selection). + /// Scrolls down one (cell, line, etc...) (without changing the selection). /// ScrollDown, // -------------------------------------------------------------------- /// - /// Moves the caret up one line. + /// Moves up one (cell, line, etc...). /// LineUp, /// - /// Extends the selection up one line. + /// Extends the selection up one item (cell, line, etc...). /// LineUpExtend, /// - /// Moves the caret up to the first child node of the branch that holds the current selection + /// Moves up to the first child node of the branch that holds the current selection. /// LineUpToFirstBranch, /// - /// Scrolls up one line (without changing the selection). + /// Scrolls up one item (cell, line, etc...) (without changing the selection). /// ScrollUp, /// - /// Moves the selection left one by the minimum increment supported by the view e.g. single character, cell, item etc. + /// Moves the selection left one by the minimum increment supported by the e.g. single character, cell, item etc. /// Left, /// - /// Scrolls one character to the left + /// Scrolls one item (cell, character, etc...) to the left /// ScrollLeft, @@ -72,7 +72,7 @@ namespace Terminal.Gui { Right, /// - /// Scrolls one character to the right. + /// Scrolls one item (cell, character, etc...) to the right. /// ScrollRight, @@ -102,12 +102,12 @@ namespace Terminal.Gui { WordRightExtend, /// - /// Deletes and copies to the clipboard the characters from the current position to the end of the line. + /// Cuts to the clipboard the characters from the current position to the end of the line. /// CutToEndLine, /// - /// Deletes and copies to the clipboard the characters from the current position to the start of the line. + /// Cuts to the clipboard the characters from the current position to the start of the line. /// CutToStartLine, @@ -140,47 +140,47 @@ namespace Terminal.Gui { DisableOverwrite, /// - /// Move the page down. + /// Move one page down. /// PageDown, /// - /// Move the page down increase selection area to cover revealed objects/characters. + /// Move one page page extending the selection to cover revealed objects/characters. /// PageDownExtend, /// - /// Move the page up. + /// Move one page up. /// PageUp, /// - /// Move the page up increase selection area to cover revealed objects/characters. + /// Move one page up extending the selection to cover revealed objects/characters. /// PageUpExtend, /// - /// Moves to top begin. + /// Moves to the top/home. /// TopHome, /// - /// Extends the selection to the top begin. + /// Extends the selection to the top/home. /// TopHomeExtend, /// - /// Moves to bottom end. + /// Moves to the bottom/end. /// BottomEnd, /// - /// Extends the selection to the bottom end. + /// Extends the selection to the bottom/end. /// BottomEndExtend, /// - /// Open selected item. + /// Open the selected item. /// OpenSelectedItem, @@ -190,43 +190,43 @@ namespace Terminal.Gui { ToggleChecked, /// - /// Accepts the current state (e.g. selection, button press etc) + /// Accepts the current state (e.g. selection, button press etc). /// Accept, /// - /// Toggles the Expanded or collapsed state of a a list or item (with subitems) + /// Toggles the Expanded or collapsed state of a a list or item (with subitems). /// ToggleExpandCollapse, /// - /// Expands a list or item (with subitems) + /// Expands a list or item (with subitems). /// Expand, /// - /// Recursively Expands all child items and their child items (if any) + /// Recursively Expands all child items and their child items (if any). /// ExpandAll, /// - /// Collapses a list or item (with subitems) + /// Collapses a list or item (with subitems). /// Collapse, /// - /// Recursively collapses a list items of their children (if any) + /// Recursively collapses a list items of their children (if any). /// CollapseAll, /// - /// Cancels any current temporary states on the control e.g. expanding - /// a combo list + /// Cancels an action or any temporary states on the control e.g. expanding + /// a combo list. /// Cancel, /// - /// Unix emulation + /// Unix emulation. /// UnixEmulation, @@ -241,12 +241,12 @@ namespace Terminal.Gui { DeleteCharLeft, /// - /// Selects all objects in the control. + /// Selects all objects. /// SelectAll, /// - /// Deletes all objects in the control. + /// Deletes all objects. /// DeleteAll, @@ -336,7 +336,7 @@ namespace Terminal.Gui { Paste, /// - /// Quit a toplevel. + /// Quit a . /// QuitToplevel, @@ -356,37 +356,37 @@ namespace Terminal.Gui { PreviousView, /// - /// Moves focus to the next view or toplevel (case of Mdi). + /// Moves focus to the next view or toplevel (case of MDI). /// NextViewOrTop, /// - /// Moves focus to the next previous or toplevel (case of Mdi). + /// Moves focus to the next previous or toplevel (case of MDI). /// PreviousViewOrTop, /// - /// Refresh the application. + /// Refresh. /// Refresh, /// - /// Toggles the extended selection. + /// Toggles the selection. /// ToggleExtend, /// - /// Inserts a new line. + /// Inserts a new item. /// NewLine, /// - /// Inserts a tab. + /// Tabs to the next item. /// Tab, /// - /// Inserts a shift tab. + /// Tabs back to the previous item. /// BackTab } diff --git a/Terminal.Gui/Core/ConsoleDriver.cs b/Terminal.Gui/Core/ConsoleDriver.cs index 873d8398b..42d32ebfa 100644 --- a/Terminal.Gui/Core/ConsoleDriver.cs +++ b/Terminal.Gui/Core/ConsoleDriver.cs @@ -8,8 +8,11 @@ using NStack; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Unix.Terminal; namespace Terminal.Gui { /// @@ -209,153 +212,27 @@ namespace Terminal.Gui { /// /// The default color for text, when the view is not focused. /// - public Attribute Normal { get { return _normal; } set { _normal = SetAttribute (value); } } + public Attribute Normal { get { return _normal; } set { _normal = value; } } /// /// The color for text when the view has the focus. /// - public Attribute Focus { get { return _focus; } set { _focus = SetAttribute (value); } } + public Attribute Focus { get { return _focus; } set { _focus = value; } } /// /// The color for the hotkey when a view is not focused /// - public Attribute HotNormal { get { return _hotNormal; } set { _hotNormal = SetAttribute (value); } } + public Attribute HotNormal { get { return _hotNormal; } set { _hotNormal = value; } } /// /// The color for the hotkey when the view is focused. /// - public Attribute HotFocus { get { return _hotFocus; } set { _hotFocus = SetAttribute (value); } } + public Attribute HotFocus { get { return _hotFocus; } set { _hotFocus = value; } } /// /// The default color for text, when the view is disabled. /// - public Attribute Disabled { get { return _disabled; } set { _disabled = SetAttribute (value); } } - - bool preparingScheme = false; - - Attribute SetAttribute (Attribute attribute, [CallerMemberName] string callerMemberName = null) - { - if (!Application._initialized && !preparingScheme) - return attribute; - - if (preparingScheme) - return attribute; - - preparingScheme = true; - switch (caller) { - case "TopLevel": - switch (callerMemberName) { - case "Normal": - HotNormal = Application.Driver.MakeAttribute (HotNormal.Foreground, attribute.Background); - break; - case "Focus": - HotFocus = Application.Driver.MakeAttribute (HotFocus.Foreground, attribute.Background); - break; - case "HotNormal": - HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus.Background); - break; - case "HotFocus": - HotNormal = Application.Driver.MakeAttribute (attribute.Foreground, HotNormal.Background); - if (Focus.Foreground != attribute.Background) - Focus = Application.Driver.MakeAttribute (Focus.Foreground, attribute.Background); - break; - } - break; - - case "Base": - switch (callerMemberName) { - case "Normal": - HotNormal = Application.Driver.MakeAttribute (HotNormal.Foreground, attribute.Background); - break; - case "Focus": - HotFocus = Application.Driver.MakeAttribute (HotFocus.Foreground, attribute.Background); - break; - case "HotNormal": - HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus.Background); - Normal = Application.Driver.MakeAttribute (Normal.Foreground, attribute.Background); - break; - case "HotFocus": - HotNormal = Application.Driver.MakeAttribute (attribute.Foreground, HotNormal.Background); - if (Focus.Foreground != attribute.Background) - Focus = Application.Driver.MakeAttribute (Focus.Foreground, attribute.Background); - break; - } - break; - - case "Menu": - switch (callerMemberName) { - case "Normal": - if (Focus.Background != attribute.Background) - Focus = Application.Driver.MakeAttribute (attribute.Foreground, Focus.Background); - HotNormal = Application.Driver.MakeAttribute (HotNormal.Foreground, attribute.Background); - Disabled = Application.Driver.MakeAttribute (Disabled.Foreground, attribute.Background); - break; - case "Focus": - Normal = Application.Driver.MakeAttribute (attribute.Foreground, Normal.Background); - HotFocus = Application.Driver.MakeAttribute (HotFocus.Foreground, attribute.Background); - break; - case "HotNormal": - if (Focus.Background != attribute.Background) - HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus.Background); - Normal = Application.Driver.MakeAttribute (Normal.Foreground, attribute.Background); - Disabled = Application.Driver.MakeAttribute (Disabled.Foreground, attribute.Background); - break; - case "HotFocus": - HotNormal = Application.Driver.MakeAttribute (attribute.Foreground, HotNormal.Background); - if (Focus.Foreground != attribute.Background) - Focus = Application.Driver.MakeAttribute (Focus.Foreground, attribute.Background); - break; - case "Disabled": - if (Focus.Background != attribute.Background) - HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus.Background); - Normal = Application.Driver.MakeAttribute (Normal.Foreground, attribute.Background); - HotNormal = Application.Driver.MakeAttribute (HotNormal.Foreground, attribute.Background); - break; - } - break; - - case "Dialog": - switch (callerMemberName) { - case "Normal": - if (Focus.Background != attribute.Background) - Focus = Application.Driver.MakeAttribute (attribute.Foreground, Focus.Background); - HotNormal = Application.Driver.MakeAttribute (HotNormal.Foreground, attribute.Background); - break; - case "Focus": - Normal = Application.Driver.MakeAttribute (attribute.Foreground, Normal.Background); - HotFocus = Application.Driver.MakeAttribute (HotFocus.Foreground, attribute.Background); - break; - case "HotNormal": - if (Focus.Background != attribute.Background) - HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus.Background); - if (Normal.Foreground != attribute.Background) - Normal = Application.Driver.MakeAttribute (Normal.Foreground, attribute.Background); - break; - case "HotFocus": - HotNormal = Application.Driver.MakeAttribute (attribute.Foreground, HotNormal.Background); - if (Focus.Foreground != attribute.Background) - Focus = Application.Driver.MakeAttribute (Focus.Foreground, attribute.Background); - break; - } - break; - - case "Error": - switch (callerMemberName) { - case "Normal": - HotNormal = Application.Driver.MakeAttribute (HotNormal.Foreground, attribute.Background); - HotFocus = Application.Driver.MakeAttribute (HotFocus.Foreground, attribute.Background); - break; - case "HotNormal": - case "HotFocus": - HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, attribute.Background); - Normal = Application.Driver.MakeAttribute (Normal.Foreground, attribute.Background); - break; - } - break; - } - preparingScheme = false; - return attribute; - } + public Attribute Disabled { get { return _disabled; } set { _disabled = value; } } /// /// Compares two objects for equality. @@ -681,11 +558,13 @@ namespace Terminal.Gui { /// Column to move the cursor to. /// Row to move the cursor to. public abstract void Move (int col, int row); + /// - /// Adds the specified rune to the display at the current cursor position + /// Adds the specified rune to the display at the current cursor position. /// /// Rune to add. public abstract void AddRune (Rune rune); + /// /// Ensures a Rune is not a control character and can be displayed by translating characters below 0x20 /// to equivalent, printable, Unicode chars. @@ -694,21 +573,13 @@ namespace Terminal.Gui { /// public static Rune MakePrintable (Rune c) { - var controlChars = gethexaformat (c, 4); - if (controlChars <= 0x1F || (controlChars >= 0X7F && controlChars <= 0x9F)) { + if (c <= 0x1F || (c >= 0X7F && c <= 0x9F)) { // ASCII (C0) control characters. // C1 control characters (https://www.aivosto.com/articles/control-characters.html#c1) - return new Rune (controlChars + 0x2400); - } else { - return c; + return new Rune (c + 0x2400); } - } - static uint gethexaformat (uint rune, int length) - { - var hex = rune.ToString ($"x{length}"); - var hexstr = hex.Substring (hex.Length - length, length); - return (uint)int.Parse (hexstr, System.Globalization.NumberStyles.HexNumber); + return c; } /// @@ -722,10 +593,11 @@ namespace Terminal.Gui { col >= 0 && row >= 0 && col < Cols && row < Rows && clip.Contains (col, row); /// - /// Adds the specified + /// Adds the to the display at the cursor position. /// /// String. public abstract void AddStr (ustring str); + /// /// Prepare the driver and set the key and mouse events handlers. /// @@ -1390,8 +1262,78 @@ namespace Terminal.Gui { Colors.Error.Normal = MakeColor (Color.Red, Color.White); Colors.Error.Focus = MakeColor (Color.Black, Color.BrightRed); Colors.Error.HotNormal = MakeColor (Color.Black, Color.White); - Colors.Error.HotFocus = MakeColor (Color.BrightRed, Color.Gray); + Colors.Error.HotFocus = MakeColor (Color.White, Color.BrightRed); Colors.Error.Disabled = MakeColor (Color.DarkGray, Color.White); } } + + /// + /// Helper class for console drivers to invoke shell commands to interact with the clipboard. + /// Used primarily by CursesDriver, but also used in Unit tests which is why it is in + /// ConsoleDriver.cs. + /// + internal static class ClipboardProcessRunner { + public static (int exitCode, string result) Bash (string commandLine, string inputText = "", bool waitForOutput = false) + { + var arguments = $"-c \"{commandLine}\""; + var (exitCode, result) = Process ("bash", arguments, inputText, waitForOutput); + + return (exitCode, result.TrimEnd ()); + } + + public static (int exitCode, string result) Process (string cmd, string arguments, string input = null, bool waitForOutput = true) + { + var output = string.Empty; + + using (Process process = new Process { + StartInfo = new ProcessStartInfo { + FileName = cmd, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }) { + var eventHandled = new TaskCompletionSource (); + process.Start (); + if (!string.IsNullOrEmpty (input)) { + process.StandardInput.Write (input); + process.StandardInput.Close (); + } + + if (!process.WaitForExit (5000)) { + var timeoutError = $@"Process timed out. Command line: {process.StartInfo.FileName} {process.StartInfo.Arguments}."; + throw new TimeoutException (timeoutError); + } + + if (waitForOutput && process.StandardOutput.Peek () != -1) { + output = process.StandardOutput.ReadToEnd (); + } + + if (process.ExitCode > 0) { + output = $@"Process failed to run. Command line: {cmd} {arguments}. + Output: {output} + Error: {process.StandardError.ReadToEnd ()}"; + } + + return (process.ExitCode, output); + } + } + + public static bool DoubleWaitForExit (this System.Diagnostics.Process process) + { + var result = process.WaitForExit (500); + if (result) { + process.WaitForExit (); + } + return result; + } + + public static bool FileExists (this string value) + { + return !string.IsNullOrEmpty (value) && !value.Contains ("not found"); + } + } } diff --git a/Terminal.Gui/Core/ConsoleKeyMapping.cs b/Terminal.Gui/Core/ConsoleKeyMapping.cs new file mode 100644 index 000000000..d7bc3d584 --- /dev/null +++ b/Terminal.Gui/Core/ConsoleKeyMapping.cs @@ -0,0 +1,521 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Terminal.Gui { + /// + /// Helper class to handle the scan code and virtual key from a . + /// + public static class ConsoleKeyMapping { + private class ScanCodeMapping : IEquatable { + public uint ScanCode; + public uint VirtualKey; + public ConsoleModifiers Modifiers; + public uint UnicodeChar; + + public ScanCodeMapping (uint scanCode, uint virtualKey, ConsoleModifiers modifiers, uint unicodeChar) + { + ScanCode = scanCode; + VirtualKey = virtualKey; + Modifiers = modifiers; + UnicodeChar = unicodeChar; + } + + public bool Equals (ScanCodeMapping other) + { + return (this.ScanCode.Equals (other.ScanCode) && + this.VirtualKey.Equals (other.VirtualKey) && + this.Modifiers.Equals (other.Modifiers) && + this.UnicodeChar.Equals (other.UnicodeChar)); + } + } + + private static ConsoleModifiers GetModifiers (uint unicodeChar, ConsoleModifiers modifiers, bool isConsoleKey) + { + if (modifiers.HasFlag (ConsoleModifiers.Shift) && + !modifiers.HasFlag (ConsoleModifiers.Alt) && + !modifiers.HasFlag (ConsoleModifiers.Control)) { + + return ConsoleModifiers.Shift; + } else if (modifiers == (ConsoleModifiers.Alt | ConsoleModifiers.Control)) { + return modifiers; + } else if ((!isConsoleKey || (isConsoleKey && (modifiers.HasFlag (ConsoleModifiers.Shift) || + modifiers.HasFlag (ConsoleModifiers.Alt) || modifiers.HasFlag (ConsoleModifiers.Control)))) && + unicodeChar >= 65 && unicodeChar <= 90) { + + return ConsoleModifiers.Shift; + } + return 0; + } + + private static ScanCodeMapping GetScanCode (string propName, uint keyValue, ConsoleModifiers modifiers) + { + switch (propName) { + case "UnicodeChar": + var sCode = scanCodes.FirstOrDefault ((e) => e.UnicodeChar == keyValue && e.Modifiers == modifiers); + if (sCode == null && modifiers == (ConsoleModifiers.Alt | ConsoleModifiers.Control)) { + return scanCodes.FirstOrDefault ((e) => e.UnicodeChar == keyValue && e.Modifiers == 0); + } + return sCode; + case "VirtualKey": + sCode = scanCodes.FirstOrDefault ((e) => e.VirtualKey == keyValue && e.Modifiers == modifiers); + if (sCode == null && modifiers == (ConsoleModifiers.Alt | ConsoleModifiers.Control)) { + return scanCodes.FirstOrDefault ((e) => e.VirtualKey == keyValue && e.Modifiers == 0); + } + return sCode; + } + + return null; + } + + /// + /// Get the from a . + /// + /// The key value. + /// The modifiers keys. + /// The resulting scan code. + /// The resulting output character. + /// The or the . + public static uint GetConsoleKeyFromKey (uint keyValue, ConsoleModifiers modifiers, out uint scanCode, out uint outputChar) + { + scanCode = 0; + outputChar = keyValue; + if (keyValue == 0) { + return 0; + } + + uint consoleKey = MapKeyToConsoleKey (keyValue, out bool mappable); + if (mappable) { + var mod = GetModifiers (keyValue, modifiers, false); + var scode = GetScanCode ("UnicodeChar", keyValue, mod); + if (scode != null) { + consoleKey = scode.VirtualKey; + scanCode = scode.ScanCode; + outputChar = scode.UnicodeChar; + } else { + consoleKey = consoleKey < 0xff ? (uint)(consoleKey & 0xff | 0xff << 8) : consoleKey; + } + } else { + var mod = GetModifiers (keyValue, modifiers, false); + var scode = GetScanCode ("VirtualKey", consoleKey, mod); + if (scode != null) { + consoleKey = scode.VirtualKey; + scanCode = scode.ScanCode; + outputChar = scode.UnicodeChar; + } + } + + return consoleKey; + } + + /// + /// Get the output character from the . + /// + /// The unicode character. + /// The modifiers keys. + /// The resulting console key. + /// The resulting scan code. + /// The output character or the . + public static uint GetKeyCharFromConsoleKey (uint unicodeChar, ConsoleModifiers modifiers, out uint consoleKey, out uint scanCode) + { + uint decodedChar = unicodeChar >> 8 == 0xff ? unicodeChar & 0xff : unicodeChar; + uint keyChar = decodedChar; + consoleKey = 0; + var mod = GetModifiers (decodedChar, modifiers, true); + scanCode = 0; + var scode = unicodeChar != 0 && unicodeChar >> 8 != 0xff ? GetScanCode ("VirtualKey", decodedChar, mod) : null; + if (scode != null) { + consoleKey = scode.VirtualKey; + keyChar = scode.UnicodeChar; + scanCode = scode.ScanCode; + } + if (scode == null) { + scode = unicodeChar != 0 ? GetScanCode ("UnicodeChar", decodedChar, mod) : null; + if (scode != null) { + consoleKey = scode.VirtualKey; + keyChar = scode.UnicodeChar; + scanCode = scode.ScanCode; + } + } + if (decodedChar != 0 && scanCode == 0 && char.IsLetter ((char)decodedChar)) { + string stFormD = ((char)decodedChar).ToString ().Normalize (System.Text.NormalizationForm.FormD); + for (int i = 0; i < stFormD.Length; i++) { + UnicodeCategory uc = CharUnicodeInfo.GetUnicodeCategory (stFormD [i]); + if (uc != UnicodeCategory.NonSpacingMark && uc != UnicodeCategory.OtherLetter) { + consoleKey = char.ToUpper (stFormD [i]); + scode = GetScanCode ("VirtualKey", char.ToUpper (stFormD [i]), 0); + if (scode != null) { + scanCode = scode.ScanCode; + } + } + } + } + + return keyChar; + } + + /// + /// Maps a to a . + /// + /// The key value. + /// If is mapped to a valid character, otherwise . + /// The or the . + public static uint MapKeyToConsoleKey (uint keyValue, out bool isMappable) + { + isMappable = false; + + switch ((Key)keyValue) { + case Key.Delete: + return (uint)ConsoleKey.Delete; + case Key.CursorUp: + return (uint)ConsoleKey.UpArrow; + case Key.CursorDown: + return (uint)ConsoleKey.DownArrow; + case Key.CursorLeft: + return (uint)ConsoleKey.LeftArrow; + case Key.CursorRight: + return (uint)ConsoleKey.RightArrow; + case Key.PageUp: + return (uint)ConsoleKey.PageUp; + case Key.PageDown: + return (uint)ConsoleKey.PageDown; + case Key.Home: + return (uint)ConsoleKey.Home; + case Key.End: + return (uint)ConsoleKey.End; + case Key.InsertChar: + return (uint)ConsoleKey.Insert; + case Key.DeleteChar: + return (uint)ConsoleKey.Delete; + case Key.F1: + return (uint)ConsoleKey.F1; + case Key.F2: + return (uint)ConsoleKey.F2; + case Key.F3: + return (uint)ConsoleKey.F3; + case Key.F4: + return (uint)ConsoleKey.F4; + case Key.F5: + return (uint)ConsoleKey.F5; + case Key.F6: + return (uint)ConsoleKey.F6; + case Key.F7: + return (uint)ConsoleKey.F7; + case Key.F8: + return (uint)ConsoleKey.F8; + case Key.F9: + return (uint)ConsoleKey.F9; + case Key.F10: + return (uint)ConsoleKey.F10; + case Key.F11: + return (uint)ConsoleKey.F11; + case Key.F12: + return (uint)ConsoleKey.F12; + case Key.F13: + return (uint)ConsoleKey.F13; + case Key.F14: + return (uint)ConsoleKey.F14; + case Key.F15: + return (uint)ConsoleKey.F15; + case Key.F16: + return (uint)ConsoleKey.F16; + case Key.F17: + return (uint)ConsoleKey.F17; + case Key.F18: + return (uint)ConsoleKey.F18; + case Key.F19: + return (uint)ConsoleKey.F19; + case Key.F20: + return (uint)ConsoleKey.F20; + case Key.F21: + return (uint)ConsoleKey.F21; + case Key.F22: + return (uint)ConsoleKey.F22; + case Key.F23: + return (uint)ConsoleKey.F23; + case Key.F24: + return (uint)ConsoleKey.F24; + case Key.BackTab: + return (uint)ConsoleKey.Tab; + case Key.Unknown: + isMappable = true; + return 0; + } + isMappable = true; + + return keyValue; + } + + /// + /// Maps a to a . + /// + /// The console key. + /// If is mapped to a valid character, otherwise . + /// The or the . + public static Key MapConsoleKeyToKey (ConsoleKey consoleKey, out bool isMappable) + { + isMappable = false; + + switch (consoleKey) { + case ConsoleKey.Delete: + return Key.Delete; + case ConsoleKey.UpArrow: + return Key.CursorUp; + case ConsoleKey.DownArrow: + return Key.CursorDown; + case ConsoleKey.LeftArrow: + return Key.CursorLeft; + case ConsoleKey.RightArrow: + return Key.CursorRight; + case ConsoleKey.PageUp: + return Key.PageUp; + case ConsoleKey.PageDown: + return Key.PageDown; + case ConsoleKey.Home: + return Key.Home; + case ConsoleKey.End: + return Key.End; + case ConsoleKey.Insert: + return Key.InsertChar; + case ConsoleKey.F1: + return Key.F1; + case ConsoleKey.F2: + return Key.F2; + case ConsoleKey.F3: + return Key.F3; + case ConsoleKey.F4: + return Key.F4; + case ConsoleKey.F5: + return Key.F5; + case ConsoleKey.F6: + return Key.F6; + case ConsoleKey.F7: + return Key.F7; + case ConsoleKey.F8: + return Key.F8; + case ConsoleKey.F9: + return Key.F9; + case ConsoleKey.F10: + return Key.F10; + case ConsoleKey.F11: + return Key.F11; + case ConsoleKey.F12: + return Key.F12; + case ConsoleKey.F13: + return Key.F13; + case ConsoleKey.F14: + return Key.F14; + case ConsoleKey.F15: + return Key.F15; + case ConsoleKey.F16: + return Key.F16; + case ConsoleKey.F17: + return Key.F17; + case ConsoleKey.F18: + return Key.F18; + case ConsoleKey.F19: + return Key.F19; + case ConsoleKey.F20: + return Key.F20; + case ConsoleKey.F21: + return Key.F21; + case ConsoleKey.F22: + return Key.F22; + case ConsoleKey.F23: + return Key.F23; + case ConsoleKey.F24: + return Key.F24; + case ConsoleKey.Tab: + return Key.BackTab; + } + isMappable = true; + + return (Key)consoleKey; + } + + private static HashSet scanCodes = new HashSet { + new ScanCodeMapping (1,27,0,27), // Escape + new ScanCodeMapping (1,27,ConsoleModifiers.Shift,27), + new ScanCodeMapping (2,49,0,49), // D1 + new ScanCodeMapping (2,49,ConsoleModifiers.Shift,33), + new ScanCodeMapping (3,50,0,50), // D2 + new ScanCodeMapping (3,50,ConsoleModifiers.Shift,34), + new ScanCodeMapping (3,50,ConsoleModifiers.Alt | ConsoleModifiers.Control,64), + new ScanCodeMapping (4,51,0,51), // D3 + new ScanCodeMapping (4,51,ConsoleModifiers.Shift,35), + new ScanCodeMapping (4,51,ConsoleModifiers.Alt | ConsoleModifiers.Control,163), + new ScanCodeMapping (5,52,0,52), // D4 + new ScanCodeMapping (5,52,ConsoleModifiers.Shift,36), + new ScanCodeMapping (5,52,ConsoleModifiers.Alt | ConsoleModifiers.Control,167), + new ScanCodeMapping (6,53,0,53), // D5 + new ScanCodeMapping (6,53,ConsoleModifiers.Shift,37), + new ScanCodeMapping (6,53,ConsoleModifiers.Alt | ConsoleModifiers.Control,8364), + new ScanCodeMapping (7,54,0,54), // D6 + new ScanCodeMapping (7,54,ConsoleModifiers.Shift,38), + new ScanCodeMapping (8,55,0,55), // D7 + new ScanCodeMapping (8,55,ConsoleModifiers.Shift,47), + new ScanCodeMapping (8,55,ConsoleModifiers.Alt | ConsoleModifiers.Control,123), + new ScanCodeMapping (9,56,0,56), // D8 + new ScanCodeMapping (9,56,ConsoleModifiers.Shift,40), + new ScanCodeMapping (9,56,ConsoleModifiers.Alt | ConsoleModifiers.Control,91), + new ScanCodeMapping (10,57,0,57), // D9 + new ScanCodeMapping (10,57,ConsoleModifiers.Shift,41), + new ScanCodeMapping (10,57,ConsoleModifiers.Alt | ConsoleModifiers.Control,93), + new ScanCodeMapping (11,48,0,48), // D0 + new ScanCodeMapping (11,48,ConsoleModifiers.Shift,61), + new ScanCodeMapping (11,48,ConsoleModifiers.Alt | ConsoleModifiers.Control,125), + new ScanCodeMapping (12,219,0,39), // Oem4 + new ScanCodeMapping (12,219,ConsoleModifiers.Shift,63), + new ScanCodeMapping (13,221,0,171), // Oem6 + new ScanCodeMapping (13,221,ConsoleModifiers.Shift,187), + new ScanCodeMapping (14,8,0,8), // Backspace + new ScanCodeMapping (14,8,ConsoleModifiers.Shift,8), + new ScanCodeMapping (15,9,0,9), // Tab + new ScanCodeMapping (15,9,ConsoleModifiers.Shift,15), + new ScanCodeMapping (16,81,0,113), // Q + new ScanCodeMapping (16,81,ConsoleModifiers.Shift,81), + new ScanCodeMapping (17,87,0,119), // W + new ScanCodeMapping (17,87,ConsoleModifiers.Shift,87), + new ScanCodeMapping (18,69,0,101), // E + new ScanCodeMapping (18,69,ConsoleModifiers.Shift,69), + new ScanCodeMapping (19,82,0,114), // R + new ScanCodeMapping (19,82,ConsoleModifiers.Shift,82), + new ScanCodeMapping (20,84,0,116), // T + new ScanCodeMapping (20,84,ConsoleModifiers.Shift,84), + new ScanCodeMapping (21,89,0,121), // Y + new ScanCodeMapping (21,89,ConsoleModifiers.Shift,89), + new ScanCodeMapping (22,85,0,117), // U + new ScanCodeMapping (22,85,ConsoleModifiers.Shift,85), + new ScanCodeMapping (23,73,0,105), // I + new ScanCodeMapping (23,73,ConsoleModifiers.Shift,73), + new ScanCodeMapping (24,79,0,111), // O + new ScanCodeMapping (24,79,ConsoleModifiers.Shift,79), + new ScanCodeMapping (25,80,0,112), // P + new ScanCodeMapping (25,80,ConsoleModifiers.Shift,80), + new ScanCodeMapping (26,187,0,43), // OemPlus + new ScanCodeMapping (26,187,ConsoleModifiers.Shift,42), + new ScanCodeMapping (26,187,ConsoleModifiers.Alt | ConsoleModifiers.Control,168), + new ScanCodeMapping (27,186,0,180), // Oem1 + new ScanCodeMapping (27,186,ConsoleModifiers.Shift,96), + new ScanCodeMapping (28,13,0,13), // Enter + new ScanCodeMapping (28,13,ConsoleModifiers.Shift,13), + new ScanCodeMapping (29,17,0,0), // Control + new ScanCodeMapping (29,17,ConsoleModifiers.Shift,0), + new ScanCodeMapping (30,65,0,97), // A + new ScanCodeMapping (30,65,ConsoleModifiers.Shift,65), + new ScanCodeMapping (31,83,0,115), // S + new ScanCodeMapping (31,83,ConsoleModifiers.Shift,83), + new ScanCodeMapping (32,68,0,100), // D + new ScanCodeMapping (32,68,ConsoleModifiers.Shift,68), + new ScanCodeMapping (33,70,0,102), // F + new ScanCodeMapping (33,70,ConsoleModifiers.Shift,70), + new ScanCodeMapping (34,71,0,103), // G + new ScanCodeMapping (34,71,ConsoleModifiers.Shift,71), + new ScanCodeMapping (35,72,0,104), // H + new ScanCodeMapping (35,72,ConsoleModifiers.Shift,72), + new ScanCodeMapping (36,74,0,106), // J + new ScanCodeMapping (36,74,ConsoleModifiers.Shift,74), + new ScanCodeMapping (37,75,0,107), // K + new ScanCodeMapping (37,75,ConsoleModifiers.Shift,75), + new ScanCodeMapping (38,76,0,108), // L + new ScanCodeMapping (38,76,ConsoleModifiers.Shift,76), + new ScanCodeMapping (39,192,0,231), // Oem3 + new ScanCodeMapping (39,192,ConsoleModifiers.Shift,199), + new ScanCodeMapping (40,222,0,186), // Oem7 + new ScanCodeMapping (40,222,ConsoleModifiers.Shift,170), + new ScanCodeMapping (41,220,0,92), // Oem5 + new ScanCodeMapping (41,220,ConsoleModifiers.Shift,124), + new ScanCodeMapping (42,16,0,0), // LShift + new ScanCodeMapping (42,16,ConsoleModifiers.Shift,0), + new ScanCodeMapping (43,191,0,126), // Oem2 + new ScanCodeMapping (43,191,ConsoleModifiers.Shift,94), + new ScanCodeMapping (44,90,0,122), // Z + new ScanCodeMapping (44,90,ConsoleModifiers.Shift,90), + new ScanCodeMapping (45,88,0,120), // X + new ScanCodeMapping (45,88,ConsoleModifiers.Shift,88), + new ScanCodeMapping (46,67,0,99), // C + new ScanCodeMapping (46,67,ConsoleModifiers.Shift,67), + new ScanCodeMapping (47,86,0,118), // V + new ScanCodeMapping (47,86,ConsoleModifiers.Shift,86), + new ScanCodeMapping (48,66,0,98), // B + new ScanCodeMapping (48,66,ConsoleModifiers.Shift,66), + new ScanCodeMapping (49,78,0,110), // N + new ScanCodeMapping (49,78,ConsoleModifiers.Shift,78), + new ScanCodeMapping (50,77,0,109), // M + new ScanCodeMapping (50,77,ConsoleModifiers.Shift,77), + new ScanCodeMapping (51,188,0,44), // OemComma + new ScanCodeMapping (51,188,ConsoleModifiers.Shift,59), + new ScanCodeMapping (52,190,0,46), // OemPeriod + new ScanCodeMapping (52,190,ConsoleModifiers.Shift,58), + new ScanCodeMapping (53,189,0,45), // OemMinus + new ScanCodeMapping (53,189,ConsoleModifiers.Shift,95), + new ScanCodeMapping (54,16,0,0), // RShift + new ScanCodeMapping (54,16,ConsoleModifiers.Shift,0), + new ScanCodeMapping (55,44,0,0), // PrintScreen + new ScanCodeMapping (55,44,ConsoleModifiers.Shift,0), + new ScanCodeMapping (56,18,0,0), // Alt + new ScanCodeMapping (56,18,ConsoleModifiers.Shift,0), + new ScanCodeMapping (57,32,0,32), // Spacebar + new ScanCodeMapping (57,32,ConsoleModifiers.Shift,32), + new ScanCodeMapping (58,20,0,0), // Caps + new ScanCodeMapping (58,20,ConsoleModifiers.Shift,0), + new ScanCodeMapping (59,112,0,0), // F1 + new ScanCodeMapping (59,112,ConsoleModifiers.Shift,0), + new ScanCodeMapping (60,113,0,0), // F2 + new ScanCodeMapping (60,113,ConsoleModifiers.Shift,0), + new ScanCodeMapping (61,114,0,0), // F3 + new ScanCodeMapping (61,114,ConsoleModifiers.Shift,0), + new ScanCodeMapping (62,115,0,0), // F4 + new ScanCodeMapping (62,115,ConsoleModifiers.Shift,0), + new ScanCodeMapping (63,116,0,0), // F5 + new ScanCodeMapping (63,116,ConsoleModifiers.Shift,0), + new ScanCodeMapping (64,117,0,0), // F6 + new ScanCodeMapping (64,117,ConsoleModifiers.Shift,0), + new ScanCodeMapping (65,118,0,0), // F7 + new ScanCodeMapping (65,118,ConsoleModifiers.Shift,0), + new ScanCodeMapping (66,119,0,0), // F8 + new ScanCodeMapping (66,119,ConsoleModifiers.Shift,0), + new ScanCodeMapping (67,120,0,0), // F9 + new ScanCodeMapping (67,120,ConsoleModifiers.Shift,0), + new ScanCodeMapping (68,121,0,0), // F10 + new ScanCodeMapping (68,121,ConsoleModifiers.Shift,0), + new ScanCodeMapping (69,144,0,0), // Num + new ScanCodeMapping (69,144,ConsoleModifiers.Shift,0), + new ScanCodeMapping (70,145,0,0), // Scroll + new ScanCodeMapping (70,145,ConsoleModifiers.Shift,0), + new ScanCodeMapping (71,36,0,0), // Home + new ScanCodeMapping (71,36,ConsoleModifiers.Shift,0), + new ScanCodeMapping (72,38,0,0), // UpArrow + new ScanCodeMapping (72,38,ConsoleModifiers.Shift,0), + new ScanCodeMapping (73,33,0,0), // PageUp + new ScanCodeMapping (73,33,ConsoleModifiers.Shift,0), + new ScanCodeMapping (74,109,0,45), // Subtract + new ScanCodeMapping (74,109,ConsoleModifiers.Shift,45), + new ScanCodeMapping (75,37,0,0), // LeftArrow + new ScanCodeMapping (75,37,ConsoleModifiers.Shift,0), + new ScanCodeMapping (76,12,0,0), // Center + new ScanCodeMapping (76,12,ConsoleModifiers.Shift,0), + new ScanCodeMapping (77,39,0,0), // RightArrow + new ScanCodeMapping (77,39,ConsoleModifiers.Shift,0), + new ScanCodeMapping (78,107,0,43), // Add + new ScanCodeMapping (78,107,ConsoleModifiers.Shift,43), + new ScanCodeMapping (79,35,0,0), // End + new ScanCodeMapping (79,35,ConsoleModifiers.Shift,0), + new ScanCodeMapping (80,40,0,0), // DownArrow + new ScanCodeMapping (80,40,ConsoleModifiers.Shift,0), + new ScanCodeMapping (81,34,0,0), // PageDown + new ScanCodeMapping (81,34,ConsoleModifiers.Shift,0), + new ScanCodeMapping (82,45,0,0), // Insert + new ScanCodeMapping (82,45,ConsoleModifiers.Shift,0), + new ScanCodeMapping (83,46,0,0), // Delete + new ScanCodeMapping (83,46,ConsoleModifiers.Shift,0), + new ScanCodeMapping (86,226,0,60), // OEM 102 + new ScanCodeMapping (86,226,ConsoleModifiers.Shift,62), + new ScanCodeMapping (87,122,0,0), // F11 + new ScanCodeMapping (87,122,ConsoleModifiers.Shift,0), + new ScanCodeMapping (88,123,0,0), // F12 + new ScanCodeMapping (88,123,ConsoleModifiers.Shift,0) + }; + } +} diff --git a/Terminal.Gui/Core/Event.cs b/Terminal.Gui/Core/Event.cs index 181724b1c..4203faa86 100644 --- a/Terminal.Gui/Core/Event.cs +++ b/Terminal.Gui/Core/Event.cs @@ -77,11 +77,26 @@ namespace Terminal.Gui { /// Null = '\0', + /// + /// Backspace key. + /// + Backspace = 8, + + /// + /// The key code for the user pressing the tab key (forwards tab key). + /// + Tab = 9, + /// /// The key code for the user pressing the return key. /// Enter = '\n', + /// + /// The key code for the user pressing the clear key. + /// + Clear = 12, + /// /// The key code for the user pressing the escape key /// @@ -363,15 +378,10 @@ namespace Terminal.Gui { /// CtrlMask = 0x40000000, - /// - /// Backspace key. - /// - Backspace = 0x100000, - /// /// Cursor up key /// - CursorUp, + CursorUp = 0x100000, /// /// Cursor down key. /// @@ -393,22 +403,34 @@ namespace Terminal.Gui { /// PageDown, /// - /// Home key + /// Home key. /// Home, /// - /// End key + /// End key. /// End, + /// - /// Delete character key - /// - DeleteChar, - /// - /// Insert character key + /// Insert character key. /// InsertChar, + /// + /// Delete character key. + /// + DeleteChar, + + /// + /// Shift-tab key (backwards tab key). + /// + BackTab, + + /// + /// Print screen character key. + /// + PrintScreen, + /// /// F1 key. /// @@ -457,15 +479,54 @@ namespace Terminal.Gui { /// F12 key. /// F12, - /// - /// The key code for the user pressing the tab key (forwards tab key). + /// F13 key. /// - Tab, + F13, /// - /// Shift-tab key (backwards tab key). + /// F14 key. /// - BackTab, + F14, + /// + /// F15 key. + /// + F15, + /// + /// F16 key. + /// + F16, + /// + /// F17 key. + /// + F17, + /// + /// F18 key. + /// + F18, + /// + /// F19 key. + /// + F19, + /// + /// F20 key. + /// + F20, + /// + /// F21 key. + /// + F21, + /// + /// F22 key. + /// + F22, + /// + /// F23 key. + /// + F23, + /// + /// F24 key. + /// + F24, /// /// A key with an unknown mapping was raised. @@ -480,7 +541,7 @@ namespace Terminal.Gui { KeyModifiers keyModifiers; /// - /// Symb olid definition for the key. + /// Symbolic definition for the key. /// public Key Key; @@ -573,7 +634,7 @@ namespace Terminal.Gui { msg += "Scrolllock-"; } - msg += $"{(((uint)this.KeyValue & (uint)Key.CharMask) > 27 ? $"{(char)this.KeyValue}" : $"{key}")}"; + msg += $"{((Key)KeyValue != Key.Unknown && ((uint)this.KeyValue & (uint)Key.CharMask) > 27 ? $"{(char)this.KeyValue}" : $"{key}")}"; return msg; } @@ -706,38 +767,48 @@ namespace Terminal.Gui { } /// - /// Describes a mouse event + /// Low-level construct that conveys the details of mouse events, such + /// as coordinates and button state, from ConsoleDrivers up to and + /// Views. /// - public struct MouseEvent { + /// The class includes the + /// Action which takes a MouseEvent argument. + public class MouseEvent { /// /// The X (column) location for the mouse event. /// - public int X; + public int X { get; set; } /// /// The Y (column) location for the mouse event. /// - public int Y; + public int Y { get; set; } /// /// Flags indicating the kind of mouse event that is being posted. /// - public MouseFlags Flags; + public MouseFlags Flags { get; set; } /// /// The offset X (column) location for the mouse event. /// - public int OfX; + public int OfX { get; set; } /// /// The offset Y (column) location for the mouse event. /// - public int OfY; + public int OfY { get; set; } /// /// The current view at the location for the mouse event. /// - public View View; + public View View { get; set; } + + /// + /// Indicates if the current mouse event has already been processed and the driver should stop notifying any other event subscriber. + /// Its important to set this value to true specially when updating any View's layout from inside the subscriber method. + /// + public bool Handled { get; set; } /// /// Returns a that represents the current . diff --git a/Terminal.Gui/Core/MainLoop.cs b/Terminal.Gui/Core/MainLoop.cs index 43789e228..4ec3824f6 100644 --- a/Terminal.Gui/Core/MainLoop.cs +++ b/Terminal.Gui/Core/MainLoop.cs @@ -94,15 +94,16 @@ namespace Terminal.Gui { public IMainLoopDriver Driver { get; } /// - /// Invoked when a new timeout is added to be used on the case - /// if is true, + /// Invoked when a new timeout is added. To be used in the case + /// when is . /// public event Action TimeoutAdded; /// /// Creates a new Mainloop. /// - /// Should match the (one of the implementations UnixMainLoop, NetMainLoop or WindowsMainLoop). + /// Should match the + /// (one of the implementations FakeMainLoop, UnixMainLoop, NetMainLoop or WindowsMainLoop). public MainLoop (IMainLoopDriver driver) { Driver = driver; @@ -306,9 +307,12 @@ namespace Terminal.Gui { Driver.MainIteration (); + bool runIdle = false; lock (idleHandlersLock) { - if (idleHandlers.Count > 0) - RunIdle (); + runIdle = idleHandlers.Count > 0; + } + if (runIdle) { + RunIdle (); } } diff --git a/Terminal.Gui/Core/PosDim.cs b/Terminal.Gui/Core/PosDim.cs index 7ce92f9f1..1169fa049 100644 --- a/Terminal.Gui/Core/PosDim.cs +++ b/Terminal.Gui/Core/PosDim.cs @@ -593,7 +593,7 @@ namespace Terminal.Gui { internal class DimCombine : Dim { internal Dim left, right; - bool add; + internal bool add; public DimCombine (bool add, Dim left, Dim right) { this.left = left; diff --git a/Terminal.Gui/Core/Responder.cs b/Terminal.Gui/Core/Responder.cs index 37de82145..7b92f8a04 100644 --- a/Terminal.Gui/Core/Responder.cs +++ b/Terminal.Gui/Core/Responder.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Reflection; namespace Terminal.Gui { /// @@ -236,6 +237,25 @@ namespace Terminal.Gui { /// public virtual void OnVisibleChanged () { } + /// + /// Utilty function to determine is overridden in the . + /// + /// The view. + /// The method name. + /// if it's overridden, otherwise. + internal static bool IsOverridden (Responder subclass, string method) + { + MethodInfo m = subclass.GetType ().GetMethod (method, + BindingFlags.Instance + | BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.DeclaredOnly); + if (m == null) { + return false; + } + return m.GetBaseDefinition ().DeclaringType != m.DeclaringType; + } + /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// diff --git a/Terminal.Gui/Core/TextFormatter.cs b/Terminal.Gui/Core/TextFormatter.cs index 06b0323d5..50fc9f5ae 100644 --- a/Terminal.Gui/Core/TextFormatter.cs +++ b/Terminal.Gui/Core/TextFormatter.cs @@ -293,12 +293,6 @@ namespace Terminal.Gui { } } - /// - /// Specifies the mask to apply to the hotkey to tag it as the hotkey. The default value of 0x100000 causes - /// the underlying Rune to be identified as a "private use" Unicode character. - /// HotKeyTagMask - public uint HotKeyTagMask { get; set; } = 0x100000; - /// /// Gets the cursor position from . If the is defined, the cursor will be positioned over it. /// @@ -317,8 +311,9 @@ namespace Terminal.Gui { get { // With this check, we protect against subclasses with overrides of Text if (ustring.IsNullOrEmpty (Text) || Size.IsEmpty) { - lines = new List (); - lines.Add (ustring.Empty); + lines = new List { + ustring.Empty + }; NeedsFormat = false; return lines; } @@ -716,7 +711,7 @@ namespace Terminal.Gui { } static char [] whitespace = new char [] { ' ', '\t' }; - private int hotKeyPos; + private int hotKeyPos = -1; /// /// Reformats text into lines, applying text alignment and optionally wrapping text to new lines on word boundaries. @@ -1113,14 +1108,13 @@ namespace Terminal.Gui { /// The text with the hotkey tagged. /// /// The returned string will not render correctly without first un-doing the tag. To undo the tag, search for - /// Runes with a bitmask of otKeyTagMask and remove that bitmask. /// public ustring ReplaceHotKeyWithTag (ustring text, int hotPos) { // Set the high bit var runes = text.ToRuneList (); if (Rune.IsLetterOrNumber (runes [hotPos])) { - runes [hotPos] = new Rune ((uint)runes [hotPos] | HotKeyTagMask); + runes [hotPos] = new Rune ((uint)runes [hotPos]); } return ustring.Make (runes); } @@ -1182,10 +1176,22 @@ namespace Terminal.Gui { } var isVertical = IsVerticalDirection (textDirection); + var savedClip = Application.Driver?.Clip; + var maxBounds = bounds; + if (Application.Driver != null) { + Application.Driver.Clip = maxBounds = containerBounds == default + ? bounds + : new Rect (Math.Max (containerBounds.X, bounds.X), + Math.Max (containerBounds.Y, bounds.Y), + Math.Max (Math.Min (containerBounds.Width, containerBounds.Right - bounds.Left), 0), + Math.Max (Math.Min (containerBounds.Height, containerBounds.Bottom - bounds.Top), 0)); + } for (int line = 0; line < linesFormated.Count; line++) { if ((isVertical && line > bounds.Width) || (!isVertical && line > bounds.Height)) continue; + if ((isVertical && line > maxBounds.Left + maxBounds.Width - bounds.X) || (!isVertical && line > maxBounds.Top + maxBounds.Height - bounds.Y)) + break; var runes = lines [line].ToRunes (); @@ -1206,11 +1212,11 @@ namespace Terminal.Gui { if (isVertical) { var runesWidth = GetSumMaxCharWidth (Lines, line); x = bounds.Right - runesWidth; - CursorPosition = bounds.Width - runesWidth + hotKeyPos; + CursorPosition = bounds.Width - runesWidth + (hotKeyPos > -1 ? hotKeyPos : 0); } else { var runesWidth = GetTextWidth (ustring.Make (runes)); x = bounds.Right - runesWidth; - CursorPosition = bounds.Width - runesWidth + hotKeyPos; + CursorPosition = bounds.Width - runesWidth + (hotKeyPos > -1 ? hotKeyPos : 0); } } else if (textAlignment == TextAlignment.Left || textAlignment == TextAlignment.Justified) { if (isVertical) { @@ -1219,16 +1225,16 @@ namespace Terminal.Gui { } else { x = bounds.Left; } - CursorPosition = hotKeyPos; + CursorPosition = hotKeyPos > -1 ? hotKeyPos : 0; } else if (textAlignment == TextAlignment.Centered) { if (isVertical) { var runesWidth = GetSumMaxCharWidth (Lines, line); x = bounds.Left + line + ((bounds.Width - runesWidth) / 2); - CursorPosition = (bounds.Width - runesWidth) / 2 + hotKeyPos; + CursorPosition = (bounds.Width - runesWidth) / 2 + (hotKeyPos > -1 ? hotKeyPos : 0); } else { var runesWidth = GetTextWidth (ustring.Make (runes)); x = bounds.Left + (bounds.Width - runesWidth) / 2; - CursorPosition = (bounds.Width - runesWidth) / 2 + hotKeyPos; + CursorPosition = (bounds.Width - runesWidth) / 2 + (hotKeyPos > -1 ? hotKeyPos : 0); } } else { throw new ArgumentOutOfRangeException (); @@ -1262,15 +1268,6 @@ namespace Terminal.Gui { var start = isVertical ? bounds.Top : bounds.Left; var size = isVertical ? bounds.Height : bounds.Width; var current = start; - var savedClip = Application.Driver?.Clip; - if (Application.Driver != null) { - Application.Driver.Clip = containerBounds == default - ? bounds - : new Rect (Math.Max (containerBounds.X, bounds.X), - Math.Max (containerBounds.Y, bounds.Y), - Math.Max (Math.Min (containerBounds.Width, containerBounds.Right - bounds.Left), 0), - Math.Max (Math.Min (containerBounds.Height, containerBounds.Bottom - bounds.Top), 0)); - } for (var idx = (isVertical ? start - y : start - x); current < start + size; idx++) { if (!fillRemaining && idx < 0) { @@ -1279,6 +1276,9 @@ namespace Terminal.Gui { } else if (!fillRemaining && idx > runes.Length - 1) { break; } + if ((!isVertical && idx > maxBounds.Left + maxBounds.Width - bounds.X) || (isVertical && idx > maxBounds.Top + maxBounds.Height - bounds.Y)) + break; + var rune = (Rune)' '; if (isVertical) { Application.Driver?.Move (x, current); @@ -1291,13 +1291,13 @@ namespace Terminal.Gui { rune = runes [idx]; } } - if ((rune & HotKeyTagMask) == HotKeyTagMask) { + if (HotKeyPos > -1 && idx == HotKeyPos) { if ((isVertical && textVerticalAlignment == VerticalTextAlignment.Justified) || - (!isVertical && textAlignment == TextAlignment.Justified)) { + (!isVertical && textAlignment == TextAlignment.Justified)) { CursorPosition = idx - start; } Application.Driver?.SetAttribute (hotColor); - Application.Driver?.AddRune ((Rune)((uint)rune & ~HotKeyTagMask)); + Application.Driver?.AddRune (rune); Application.Driver?.SetAttribute (normalColor); } else { Application.Driver?.AddRune (rune); @@ -1313,9 +1313,9 @@ namespace Terminal.Gui { break; } } - if (Application.Driver != null) - Application.Driver.Clip = (Rect)savedClip; } + if (Application.Driver != null) + Application.Driver.Clip = (Rect)savedClip; } } } diff --git a/Terminal.Gui/Core/Toplevel.cs b/Terminal.Gui/Core/Toplevel.cs index a922f729d..fddabb692 100644 --- a/Terminal.Gui/Core/Toplevel.cs +++ b/Terminal.Gui/Core/Toplevel.cs @@ -44,7 +44,7 @@ namespace Terminal.Gui { public bool Running { get; set; } /// - /// Invoked when the Toplevel has begin loaded. + /// Invoked when the Toplevel has begun to be loaded. /// A Loaded event handler is a good place to finalize initialization before calling /// . /// @@ -77,13 +77,13 @@ namespace Terminal.Gui { /// /// Invoked when a child of the Toplevel is closed by - /// . + /// . /// public event Action ChildClosed; /// /// Invoked when the last child of the Toplevel is closed from - /// by . + /// by . /// public event Action AllChildClosed; @@ -94,7 +94,7 @@ namespace Terminal.Gui { public event Action Closing; /// - /// Invoked when the Toplevel's is closed by . + /// Invoked when the Toplevel's is closed by . /// public event Action Closed; @@ -613,11 +613,9 @@ namespace Terminal.Gui { } nx = Math.Max (x, 0); nx = nx + top.Frame.Width > l ? Math.Max (l - top.Frame.Width, 0) : nx; - var canChange = SetWidth (top.Frame.Width, out int rWidth); - if (canChange && rWidth < 0 && nx >= top.Frame.X) { - nx = Math.Max (top.Frame.Right - 2, 0); - } else if (rWidth < 0 && nx >= top.Frame.X) { - nx = Math.Min (nx + 1, top.Frame.Right - 2); + var mfLength = top.Border?.DrawMarginFrame == true ? 2 : 1; + if (nx + mfLength > top.Frame.X + top.Frame.Width) { + nx = Math.Max (top.Frame.Right - mfLength, 0); } //System.Diagnostics.Debug.WriteLine ($"nx:{nx}, rWidth:{rWidth}"); bool m, s; @@ -656,11 +654,8 @@ namespace Terminal.Gui { } ny = Math.Min (ny, l); ny = ny + top.Frame.Height >= l ? Math.Max (l - top.Frame.Height, m ? 1 : 0) : ny; - canChange = SetHeight (top.Frame.Height, out int rHeight); - if (canChange && rHeight < 0 && ny >= top.Frame.Y) { - ny = Math.Max (top.Frame.Bottom - 2, 0); - } else if (rHeight < 0 && ny >= top.Frame.Y) { - ny = Math.Min (ny + 1, top.Frame.Bottom - 2); + if (ny + mfLength > top.Frame.Y + top.Frame.Height) { + ny = Math.Max (top.Frame.Bottom - mfLength, 0); } //System.Diagnostics.Debug.WriteLine ($"ny:{ny}, rHeight:{rHeight}"); @@ -701,7 +696,7 @@ namespace Terminal.Gui { } if (sb != null && ny + top.Frame.Height != superView.Frame.Height - (sb.Visible ? 1 : 0) - && top.Height is Dim.DimFill) { + && top.Height is Dim.DimFill && -top.Height.Anchor (0) < 1) { top.Height = Dim.Fill (sb.Visible ? 1 : 0); layoutSubviews = true; @@ -775,6 +770,8 @@ namespace Terminal.Gui { return true; } + //System.Diagnostics.Debug.WriteLine ($"dragPosition before: {dragPosition.HasValue}"); + int nx, ny; if (!dragPosition.HasValue && (mouseEvent.Flags == MouseFlags.Button1Pressed || mouseEvent.Flags == MouseFlags.Button2Pressed @@ -809,32 +806,27 @@ namespace Terminal.Gui { SuperView.SetNeedsDisplay (); } EnsureVisibleBounds (this, mouseEvent.X + (SuperView == null ? mouseEvent.OfX - start.X : Frame.X - start.X), - mouseEvent.Y + (SuperView == null ? mouseEvent.OfY : Frame.Y), + mouseEvent.Y + (SuperView == null ? mouseEvent.OfY - start.Y : Frame.Y - start.Y), out nx, out ny, out _, out _); dragPosition = new Point (nx, ny); - LayoutSubviews (); - Frame = new Rect (nx, ny, Frame.Width, Frame.Height); - if (X == null || X is Pos.PosAbsolute) { - X = nx; - } - if (Y == null || Y is Pos.PosAbsolute) { - Y = ny; - } - //System.Diagnostics.Debug.WriteLine ($"nx:{nx},ny:{ny}"); + X = nx; + Y = ny; + //System.Diagnostics.Debug.WriteLine ($"Drag: nx:{nx},ny:{ny}"); SetNeedsDisplay (); return true; } } - if (mouseEvent.Flags == MouseFlags.Button1Released && dragPosition.HasValue) { + if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && dragPosition.HasValue) { Application.UngrabMouse (); Driver.UncookMouse (); dragPosition = null; } - //System.Diagnostics.Debug.WriteLine (mouseEvent.ToString ()); + //System.Diagnostics.Debug.WriteLine ($"dragPosition after: {dragPosition.HasValue}"); + //System.Diagnostics.Debug.WriteLine ($"Toplevel: {mouseEvent}"); return false; } diff --git a/Terminal.Gui/Core/Trees/Branch.cs b/Terminal.Gui/Core/Trees/Branch.cs index 35a81965a..ce699af6b 100644 --- a/Terminal.Gui/Core/Trees/Branch.cs +++ b/Terminal.Gui/Core/Trees/Branch.cs @@ -5,23 +5,23 @@ using System.Linq; namespace Terminal.Gui.Trees { class Branch where T : class { /// - /// True if the branch is expanded to reveal child branches + /// True if the branch is expanded to reveal child branches. /// public bool IsExpanded { get; set; } /// - /// The users object that is being displayed by this branch of the tree + /// The users object that is being displayed by this branch of the tree. /// public T Model { get; private set; } /// - /// The depth of the current branch. Depth of 0 indicates root level branches + /// The depth of the current branch. Depth of 0 indicates root level branches. /// public int Depth { get; private set; } = 0; /// /// The children of the current branch. This is null until the first call to - /// to avoid enumerating the entire underlying hierarchy + /// to avoid enumerating the entire underlying hierarchy. /// public Dictionary> ChildBranches { get; set; } @@ -34,12 +34,12 @@ namespace Terminal.Gui.Trees { /// /// Declares a new branch of in which the users object - /// is presented + /// is presented. /// - /// The UI control in which the branch resides + /// The UI control in which the branch resides. /// Pass null for root level branches, otherwise - /// pass the parent - /// The user's object that should be displayed + /// pass the parent. + /// The user's object that should be displayed. public Branch (TreeView tree, Branch parentBranchIfAny, T model) { this.tree = tree; @@ -53,7 +53,7 @@ namespace Terminal.Gui.Trees { /// - /// Fetch the children of this branch. This method populates + /// Fetch the children of this branch. This method populates . /// public virtual void FetchChildren () { @@ -80,7 +80,7 @@ namespace Terminal.Gui.Trees { } /// - /// Renders the current on the specified line + /// Renders the current on the specified line . /// /// /// @@ -89,10 +89,10 @@ namespace Terminal.Gui.Trees { public virtual void Draw (ConsoleDriver driver, ColorScheme colorScheme, int y, int availableWidth) { // true if the current line of the tree is the selected one and control has focus - bool isSelected = tree.IsSelected (Model) && tree.HasFocus; - Attribute lineColor = isSelected ? colorScheme.Focus : colorScheme.Normal; + bool isSelected = tree.IsSelected (Model); - driver.SetAttribute (lineColor); + Attribute textColor = isSelected ? (tree.HasFocus ? colorScheme.Focus : colorScheme.HotNormal) : colorScheme.Normal; + Attribute symbolColor = tree.Style.HighlightModelTextOnly ? colorScheme.Normal : textColor; // Everything on line before the expansion run and branch text Rune [] prefix = GetLinePrefix (driver).ToArray (); @@ -104,7 +104,8 @@ namespace Terminal.Gui.Trees { // if we have scrolled to the right then bits of the prefix will have dispeared off the screen int toSkip = tree.ScrollOffsetHorizontal; - // Draw the line prefix (all paralell lanes or whitespace and an expand/collapse/leaf symbol) + driver.SetAttribute (symbolColor); + // Draw the line prefix (all parallel lanes or whitespace and an expand/collapse/leaf symbol) foreach (Rune r in prefix) { if (toSkip > 0) { @@ -117,12 +118,16 @@ namespace Terminal.Gui.Trees { // pick color for expanded symbol if (tree.Style.ColorExpandSymbol || tree.Style.InvertExpandSymbolColors) { - Attribute color; + Attribute color = symbolColor; if (tree.Style.ColorExpandSymbol) { - color = isSelected ? tree.ColorScheme.HotFocus : tree.ColorScheme.HotNormal; + if (isSelected) { + color = tree.Style.HighlightModelTextOnly ? colorScheme.HotNormal : (tree.HasFocus ? tree.ColorScheme.HotFocus : tree.ColorScheme.HotNormal); + } else { + color = tree.ColorScheme.HotNormal; + } } else { - color = lineColor; + color = symbolColor; } if (tree.Style.InvertExpandSymbolColors) { @@ -162,16 +167,14 @@ namespace Terminal.Gui.Trees { // default behaviour is for model to use the color scheme // of the tree view - var modelColor = lineColor; + var modelColor = textColor; // if custom color delegate invoke it - if(tree.ColorGetter != null) - { - var modelScheme = tree.ColorGetter(Model); + if (tree.ColorGetter != null) { + var modelScheme = tree.ColorGetter (Model); // if custom color scheme is defined for this Model - if(modelScheme != null) - { + if (modelScheme != null) { // use it modelColor = isSelected ? modelScheme.Focus : modelScheme.Normal; } @@ -179,24 +182,23 @@ namespace Terminal.Gui.Trees { driver.SetAttribute (modelColor); driver.AddStr (lineBody); - driver.SetAttribute (lineColor); if (availableWidth > 0) { + driver.SetAttribute (symbolColor); driver.AddStr (new string (' ', availableWidth)); } - driver.SetAttribute (colorScheme.Normal); } /// /// Gets all characters to render prior to the current branches line. This includes indentation - /// whitespace and any tree branches (if enabled) + /// whitespace and any tree branches (if enabled). /// /// /// private IEnumerable GetLinePrefix (ConsoleDriver driver) { - // If not showing line branches or this is a root object + // If not showing line branches or this is a root object. if (!tree.Style.ShowBranchLines) { for (int i = 0; i < Depth; i++) { yield return new Rune (' '); @@ -224,7 +226,7 @@ namespace Terminal.Gui.Trees { } /// - /// Returns all parents starting with the immediate parent and ending at the root + /// Returns all parents starting with the immediate parent and ending at the root. /// /// private IEnumerable> GetParentBranches () @@ -240,7 +242,7 @@ namespace Terminal.Gui.Trees { /// /// Returns an appropriate symbol for displaying next to the string representation of /// the object to indicate whether it or - /// not (or it is a leaf) + /// not (or it is a leaf). /// /// /// @@ -261,7 +263,7 @@ namespace Terminal.Gui.Trees { /// /// Returns true if the current branch can be expanded according to - /// the or cached children already fetched + /// the or cached children already fetched. /// /// public bool CanExpand () @@ -283,7 +285,7 @@ namespace Terminal.Gui.Trees { } /// - /// Expands the current branch if possible + /// Expands the current branch if possible. /// public void Expand () { @@ -297,7 +299,7 @@ namespace Terminal.Gui.Trees { } /// - /// Marks the branch as collapsed ( false) + /// Marks the branch as collapsed ( false). /// public void Collapse () { @@ -305,10 +307,10 @@ namespace Terminal.Gui.Trees { } /// - /// Refreshes cached knowledge in this branch e.g. what children an object has + /// Refreshes cached knowledge in this branch e.g. what children an object has. /// /// True to also refresh all - /// branches (starting with the root) + /// branches (starting with the root). public void Refresh (bool startAtTop) { // if we must go up and refresh from the top down @@ -351,7 +353,7 @@ namespace Terminal.Gui.Trees { } /// - /// Calls on the current branch and all expanded children + /// Calls on the current branch and all expanded children. /// internal void Rebuild () { @@ -375,7 +377,7 @@ namespace Terminal.Gui.Trees { /// /// Returns true if this branch has parents and it is the last node of it's parents - /// branches (or last root of the tree) + /// branches (or last root of the tree). /// /// private bool IsLast () @@ -389,7 +391,7 @@ namespace Terminal.Gui.Trees { /// /// Returns true if the given x offset on the branch line is the +/- symbol. Returns - /// false if not showing expansion symbols or leaf node etc + /// false if not showing expansion symbols or leaf node etc. /// /// /// @@ -415,7 +417,7 @@ namespace Terminal.Gui.Trees { } /// - /// Expands the current branch and all children branches + /// Expands the current branch and all children branches. /// internal void ExpandAll () { @@ -430,7 +432,7 @@ namespace Terminal.Gui.Trees { /// /// Collapses the current branch and all children branches (even though those branches are - /// no longer visible they retain collapse/expansion state) + /// no longer visible they retain collapse/expansion state). /// internal void CollapseAll () { diff --git a/Terminal.Gui/Core/Trees/TreeStyle.cs b/Terminal.Gui/Core/Trees/TreeStyle.cs index f6cc30e4c..744ed6974 100644 --- a/Terminal.Gui/Core/Trees/TreeStyle.cs +++ b/Terminal.Gui/Core/Trees/TreeStyle.cs @@ -2,46 +2,51 @@ namespace Terminal.Gui.Trees { /// - /// Defines rendering options that affect how the tree is displayed + /// Defines rendering options that affect how the tree is displayed. /// public class TreeStyle { /// - /// True to render vertical lines under expanded nodes to show which node belongs to which - /// parent. False to use only whitespace + /// to render vertical lines under expanded nodes to show which node belongs to which + /// parent. to use only whitespace. /// /// public bool ShowBranchLines { get; set; } = true; /// - /// Symbol to use for branch nodes that can be expanded to indicate this to the user. - /// Defaults to '+'. Set to null to hide + /// Symbol to use for branch nodes that can be expanded to indicate this to the user. + /// Defaults to '+'. Set to null to hide. /// public Rune? ExpandableSymbol { get; set; } = '+'; /// /// Symbol to use for branch nodes that can be collapsed (are currently expanded). - /// Defaults to '-'. Set to null to hide + /// Defaults to '-'. Set to null to hide. /// public Rune? CollapseableSymbol { get; set; } = '-'; /// - /// Set to true to highlight expand/collapse symbols in hot key color + /// Set to to highlight expand/collapse symbols in hot key color. /// public bool ColorExpandSymbol { get; set; } /// - /// Invert console colours used to render the expand symbol + /// Invert console colours used to render the expand symbol. /// public bool InvertExpandSymbolColors { get; set; } /// - /// True to leave the last row of the control free for overwritting (e.g. by a scrollbar) - /// When True scrolling will be triggered on the second last row of the control rather than + /// to leave the last row of the control free for overwritting (e.g. by a scrollbar) + /// When scrolling will be triggered on the second last row of the control rather than. /// the last. /// /// public bool LeaveLastRow { get; set; } + /// + /// Set to to cause the selected item to be rendered with only the text + /// to be highlighted. If (the default), the entire row will be highlighted. + /// + public bool HighlightModelTextOnly { get; set; } = false; } } \ No newline at end of file diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index ea9d80885..c7104fcdb 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -1,28 +1,15 @@ -// -// Authors: -// Miguel de Icaza (miguel@gnome.org) -// -// Pending: -// - Check for NeedDisplay on the hierarchy and repaint -// - Layout support -// - "Colors" type or "Attributes" type? -// - What to surface as "BackgroundCOlor" when clearing a window, an attribute or colors? -// -// Optimizations -// - Add rendering limitation to the exposed area -using System; -using System.Collections; +using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics; using System.Linq; +using System.Reflection; using NStack; namespace Terminal.Gui { /// - /// Determines the LayoutStyle for a view, if Absolute, during LayoutSubviews, the - /// value from the Frame will be used, if the value is Computed, then the Frame - /// will be updated from the X, Y Pos objects and the Width and Height Dim objects. + /// Determines the LayoutStyle for a , if Absolute, during , the + /// value from the will be used, if the value is Computed, then + /// will be updated from the X, Y objects and the Width and Height objects. /// public enum LayoutStyle { /// @@ -38,17 +25,19 @@ namespace Terminal.Gui { } /// - /// View is the base class for all views on the screen and represents a visible element that can render itself and contains zero or more nested views. + /// View is the base class for all views on the screen and represents a visible element that can render itself and + /// contains zero or more nested views. /// /// /// - /// The View defines the base functionality for user interface elements in Terminal.Gui. Views + /// The View defines the base functionality for user interface elements in Terminal.Gui. Views /// can contain one or more subviews, can respond to user input and render themselves on the screen. /// /// - /// Views supports two layout styles: Absolute or Computed. The choice as to which layout style is used by the View + /// Views supports two layout styles: or . + /// The choice as to which layout style is used by the View /// is determined when the View is initialized. To create a View using Absolute layout, call a constructor that takes a - /// Rect parameter to specify the absolute position and size (the View.)/. To create a View + /// Rect parameter to specify the absolute position and size (the View.). To create a View /// using Computed layout use a constructor that does not take a Rect parameter and set the X, Y, Width and Height /// properties on the view. Both approaches use coordinates that are relative to the container they are being added to. /// @@ -61,9 +50,9 @@ namespace Terminal.Gui { /// properties are Dim and Pos objects that dynamically update the position of a view. /// The X and Y properties are of type /// and you can use either absolute positions, percentages or anchor - /// points. The Width and Height properties are of type + /// points. The Width and Height properties are of type /// and can use absolute position, - /// percentages and anchors. These are useful as they will take + /// percentages and anchors. These are useful as they will take /// care of repositioning views when view's frames are resized or /// if the terminal size changes. /// @@ -73,17 +62,17 @@ namespace Terminal.Gui { /// property. /// /// - /// Subviews (child views) can be added to a View by calling the method. + /// Subviews (child views) can be added to a View by calling the method. /// The container of a View can be accessed with the property. /// /// - /// To flag a region of the View's to be redrawn call . To flag the entire view - /// for redraw call . + /// To flag a region of the View's to be redrawn call . + /// To flag the entire view for redraw call . /// /// /// Views have a property that defines the default colors that subviews - /// should use for rendering. This ensures that the views fit in the context where - /// they are being used, and allows for themes to be plugged in. For example, the + /// should use for rendering. This ensures that the views fit in the context where + /// they are being used, and allows for themes to be plugged in. For example, the /// default colors for windows and toplevels uses a blue background, while it uses /// a white background for dialog boxes and a red background for errors. /// @@ -99,14 +88,14 @@ namespace Terminal.Gui { /// /// /// Views that are focusable should implement the to make sure that - /// the cursor is placed in a location that makes sense. Unix terminals do not have + /// the cursor is placed in a location that makes sense. Unix terminals do not have /// a way of hiding the cursor, so it can be distracting to have the cursor left at - /// the last focused view. So views should make sure that they place the cursor + /// the last focused view. So views should make sure that they place the cursor /// in a visually sensible place. /// /// /// The method is invoked when the size or layout of a view has - /// changed. The default processing system will keep the size and dimensions + /// changed. The default processing system will keep the size and dimensions /// for views that use the , and will recompute the /// frames for the vies that use . /// @@ -248,9 +237,9 @@ namespace Terminal.Gui { /// Points to the current driver in use by the view, it is a convenience property /// for simplifying the development of new views. /// - public static ConsoleDriver Driver { get { return Application.Driver; } } + public static ConsoleDriver Driver => Application.Driver; - static IList empty = new List (0).AsReadOnly (); + static readonly IList empty = new List (0).AsReadOnly (); // This is null, and allocated on demand. List subviews; @@ -259,7 +248,7 @@ namespace Terminal.Gui { /// This returns a list of the subviews contained by this view. /// /// The subviews. - public IList Subviews => subviews == null ? empty : subviews.AsReadOnly (); + public IList Subviews => subviews?.AsReadOnly () ?? empty; // Internally, we use InternalSubviews rather than subviews, as we do not expect us // to make the same mistakes our users make when they poke at the Subviews. @@ -278,7 +267,7 @@ namespace Terminal.Gui { /// This returns a tab index list of the subviews contained by this view. /// /// The tabIndexes. - public IList TabIndexes => tabIndexes == null ? empty : tabIndexes.AsReadOnly (); + public IList TabIndexes => tabIndexes?.AsReadOnly () ?? empty; int tabIndex = -1; @@ -309,7 +298,7 @@ namespace Terminal.Gui { int GetTabIndex (int idx) { - int i = 0; + var i = 0; foreach (var v in SuperView.tabIndexes) { if (v.tabIndex == -1 || v == this) { continue; @@ -321,7 +310,7 @@ namespace Terminal.Gui { void SetTabIndex () { - int i = 0; + var i = 0; foreach (var v in SuperView.tabIndexes) { if (v.tabIndex == -1) { continue; @@ -334,10 +323,11 @@ namespace Terminal.Gui { bool tabStop = true; /// - /// This only be true if the is also true and the focus can be avoided by setting this to false + /// This only be if the is also + /// and the focus can be avoided by setting this to /// public bool TabStop { - get { return tabStop; } + get => tabStop; set { if (tabStop == value) { return; @@ -358,12 +348,16 @@ namespace Terminal.Gui { } if (base.CanFocus != value) { base.CanFocus = value; - if (!value && tabIndex > -1) { + + switch (value) { + case false when tabIndex > -1: TabIndex = -1; + break; + case true when SuperView?.CanFocus == false && addingView: + SuperView.CanFocus = true; + break; } - if (value && SuperView?.CanFocus == false && addingView) { - SuperView.CanFocus = value; - } + if (value && tabIndex == -1) { TabIndex = SuperView != null ? SuperView.tabIndexes.IndexOf (this) : -1; } @@ -375,7 +369,7 @@ namespace Terminal.Gui { if (!value && HasFocus) { SetHasFocus (false, this); SuperView?.EnsureFocus (); - if (SuperView != null && SuperView?.Focused == null) { + if (SuperView != null && SuperView.Focused == null) { SuperView.FocusNext (); if (SuperView.Focused == null) { Application.Current.FocusNext (); @@ -389,7 +383,7 @@ namespace Terminal.Gui { if (!value) { view.oldCanFocus = view.CanFocus; view.oldTabIndex = view.tabIndex; - view.CanFocus = value; + view.CanFocus = false; view.tabIndex = -1; } else { if (addingView) { @@ -423,22 +417,18 @@ namespace Terminal.Gui { /// /// Returns a value indicating if this View is currently on Top (Active) /// - public bool IsCurrentTop { - get { - return Application.Current == this; - } - } + public bool IsCurrentTop => Application.Current == this; /// /// Gets or sets a value indicating whether this wants mouse position reports. /// - /// true if want mouse position reports; otherwise, false. - public virtual bool WantMousePositionReports { get; set; } = false; + /// if want mouse position reports; otherwise, . + public virtual bool WantMousePositionReports { get; set; } /// /// Gets or sets a value indicating whether this want continuous button pressed event. /// - public virtual bool WantContinuousButtonPressed { get; set; } = false; + public virtual bool WantContinuousButtonPressed { get; set; } /// /// Gets or sets the frame for the view. The frame is relative to the view's container (). @@ -446,7 +436,7 @@ namespace Terminal.Gui { /// The frame. /// /// - /// Change the Frame when using the layout style to move or resize views. + /// Change the Frame when using the layout style to move or resize views. /// /// /// Altering the Frame of a view will trigger the redrawing of the @@ -480,8 +470,10 @@ namespace Terminal.Gui { LayoutStyle layoutStyle; /// - /// Controls how the View's is computed during the LayoutSubviews method, if the style is set to , - /// LayoutSubviews does not change the . If the style is the is updated using + /// Controls how the View's is computed during the LayoutSubviews method, if the style is set to + /// , + /// LayoutSubviews does not change the . If the style is + /// the is updated using /// the , , , and properties. /// /// The layout style. @@ -511,19 +503,17 @@ namespace Terminal.Gui { /// public Rect Bounds { get => new Rect (Point.Empty, Frame.Size); - set { - Frame = new Rect (frame.Location, value.Size); - } + set => Frame = new Rect (frame.Location, value.Size); } Pos x, y; /// - /// Gets or sets the X position for the view (the column). Only used the is . + /// Gets or sets the X position for the view (the column). Only used if the is . /// /// The X Position. /// - /// If is changing this property has no effect and its value is indeterminate. + /// If is changing this property has no effect and its value is indeterminate. /// public Pos X { get => x; @@ -539,11 +529,11 @@ namespace Terminal.Gui { } /// - /// Gets or sets the Y position for the view (the row). Only used the is . + /// Gets or sets the Y position for the view (the row). Only used if the is . /// /// The y position (line). /// - /// If is changing this property has no effect and its value is indeterminate. + /// If is changing this property has no effect and its value is indeterminate. /// public Pos Y { get => y; @@ -560,11 +550,11 @@ namespace Terminal.Gui { Dim width, height; /// - /// Gets or sets the width of the view. Only used the is . + /// Gets or sets the width of the view. Only used the is . /// /// The width. /// - /// If is changing this property has no effect and its value is indeterminate. + /// If is changing this property has no effect and its value is indeterminate. /// public Dim Width { get => width; @@ -587,10 +577,10 @@ namespace Terminal.Gui { } /// - /// Gets or sets the height of the view. Only used the is . + /// Gets or sets the height of the view. Only used the is . /// /// The height. - /// If is changing this property has no effect and its value is indeterminate. + /// If is changing this property has no effect and its value is indeterminate. public Dim Height { get => height; set { @@ -612,18 +602,18 @@ namespace Terminal.Gui { } /// - /// Forces validation with layout + /// Forces validation with layout /// to avoid breaking the and settings. /// public bool ForceValidatePosDim { get; set; } - bool ValidatePosDim (object oldvalue, object newValue) + bool ValidatePosDim (object oldValue, object newValue) { - if (!IsInitialized || layoutStyle == LayoutStyle.Absolute || oldvalue == null || oldvalue.GetType () == newValue.GetType () || this is Toplevel) { + if (!IsInitialized || layoutStyle == LayoutStyle.Absolute || oldValue == null || oldValue.GetType () == newValue.GetType () || this is Toplevel) { return true; } if (layoutStyle == LayoutStyle.Computed) { - if (oldvalue.GetType () != newValue.GetType () && !(newValue is Pos.PosAbsolute || newValue is Dim.DimAbsolute)) { + if (oldValue.GetType () != newValue.GetType () && !(newValue is Pos.PosAbsolute || newValue is Dim.DimAbsolute)) { return true; } } @@ -634,7 +624,7 @@ namespace Terminal.Gui { /// Verifies if the minimum width or height can be sets in the view. /// /// The size. - /// if the size can be set, otherwise. + /// if the size can be set, otherwise. public bool GetMinWidthHeight (out Size size) { size = Size.Empty; @@ -663,7 +653,7 @@ namespace Terminal.Gui { /// /// Sets the minimum width or height if the view can be resized. /// - /// if the size can be set, otherwise. + /// if the size can be set, otherwise. public bool SetMinWidthHeight () { if (GetMinWidthHeight (out Size size)) { @@ -686,13 +676,13 @@ namespace Terminal.Gui { public View SuperView => container; /// - /// Initializes a new instance of a class with the absolute - /// dimensions specified in the frame parameter. + /// Initializes a new instance of a class with the absolute + /// dimensions specified in the parameter. /// /// The region covered by this view. /// - /// This constructor initialize a View with a of . Use to - /// initialize a View with of + /// This constructor initialize a View with a of . + /// Use to initialize a View with of /// public View (Rect frame) { @@ -700,50 +690,50 @@ namespace Terminal.Gui { } /// - /// Initializes a new instance of using layout. + /// Initializes a new instance of using layout. /// /// /// /// Use , , , and properties to dynamically control the size and location of the view. - /// The will be created using - /// coordinates. The initial size ( will be + /// The will be created using + /// coordinates. The initial size () will be /// adjusted to fit the contents of , including newlines ('\n') for multiple lines. /// /// - /// If Height is greater than one, word wrapping is provided. + /// If is greater than one, word wrapping is provided. /// /// - /// This constructor initialize a View with a of . + /// This constructor initialize a View with a of . /// Use , , , and properties to dynamically control the size and location of the view. /// /// public View () : this (text: string.Empty, direction: TextDirection.LeftRight_TopBottom) { } /// - /// Initializes a new instance of using layout. + /// Initializes a new instance of using layout. /// /// /// /// The will be created at the given - /// coordinates with the given string. The size ( will be + /// coordinates with the given string. The size () will be /// adjusted to fit the contents of , including newlines ('\n') for multiple lines. /// /// /// No line wrapping is provided. /// /// - /// column to locate the Label. - /// row to locate the Label. + /// column to locate the View. + /// row to locate the View. /// text to initialize the property with. public View (int x, int y, ustring text) : this (TextFormatter.CalcRect (x, y, text), text) { } /// - /// Initializes a new instance of using layout. + /// Initializes a new instance of using layout. /// /// /// /// The will be created at the given - /// coordinates with the given string. The initial size ( will be + /// coordinates with the given string. The initial size () will be /// adjusted to fit the contents of , including newlines ('\n') for multiple lines. /// /// @@ -759,16 +749,16 @@ namespace Terminal.Gui { } /// - /// Initializes a new instance of using layout. + /// Initializes a new instance of using layout. /// /// /// - /// The will be created using - /// coordinates with the given string. The initial size ( will be + /// The will be created using + /// coordinates with the given string. The initial size () will be /// adjusted to fit the contents of , including newlines ('\n') for multiple lines. /// /// - /// If Height is greater than one, word wrapping is provided. + /// If is greater than one, word wrapping is provided. /// /// /// text to initialize the property with. @@ -795,12 +785,8 @@ namespace Terminal.Gui { TabStop = false; LayoutStyle = layoutStyle; // BUGBUG: CalcRect doesn't account for line wrapping - Rect r; - if (rect.IsEmpty) { - r = TextFormatter.CalcRect (0, 0, text, direction); - } else { - r = rect; - } + + var r = rect.IsEmpty ? TextFormatter.CalcRect (0, 0, text, direction) : rect; Frame = r; Text = text; @@ -809,7 +795,7 @@ namespace Terminal.Gui { } /// - /// Can be overridden if the has + /// Can be overridden if the has /// different format than the default. /// protected virtual void UpdateTextFormatterText () @@ -823,18 +809,18 @@ namespace Terminal.Gui { /// protected virtual void ProcessResizeView () { - var _x = x is Pos.PosAbsolute ? x.Anchor (0) : frame.X; - var _y = y is Pos.PosAbsolute ? y.Anchor (0) : frame.Y; + var actX = x is Pos.PosAbsolute ? x.Anchor (0) : frame.X; + var actY = y is Pos.PosAbsolute ? y.Anchor (0) : frame.Y; if (AutoSize) { var s = GetAutoSize (); var w = width is Dim.DimAbsolute && width.Anchor (0) > s.Width ? width.Anchor (0) : s.Width; var h = height is Dim.DimAbsolute && height.Anchor (0) > s.Height ? height.Anchor (0) : s.Height; - frame = new Rect (new Point (_x, _y), new Size (w, h)); + frame = new Rect (new Point (actX, actY), new Size (w, h)); } else { var w = width is Dim.DimAbsolute ? width.Anchor (0) : frame.Width; var h = height is Dim.DimAbsolute ? height.Anchor (0) : frame.Height; - frame = new Rect (new Point (_x, _y), new Size (w, h)); + frame = new Rect (new Point (actX, actY), new Size (w, h)); SetMinWidthHeight (); } TextFormatter.Size = GetBoundsTextFormatterSize (); @@ -842,7 +828,7 @@ namespace Terminal.Gui { SetNeedsDisplay (); } - private void TextFormatter_HotKeyChanged (Key obj) + void TextFormatter_HotKeyChanged (Key obj) { HotKeyChanged?.Invoke (obj); } @@ -894,10 +880,11 @@ namespace Terminal.Gui { var h = Math.Max (NeedDisplay.Height, region.Height); NeedDisplay = new Rect (x, y, w, h); } - if (container != null) - container.SetChildNeedsDisplay (); + container?.SetChildNeedsDisplay (); + if (subviews == null) return; + foreach (var view in subviews) if (view.Frame.IntersectsWith (region)) { var childRegion = Rect.Intersect (view.Frame, region); @@ -919,13 +906,14 @@ namespace Terminal.Gui { container.SetChildNeedsDisplay (); } - internal bool addingView = false; + internal bool addingView; /// /// Adds a subview (child) to this view. /// /// - /// The Views that have been added to this view can be retrieved via the property. See also + /// The Views that have been added to this view can be retrieved via the property. + /// See also /// public virtual void Add (View view) { @@ -965,7 +953,8 @@ namespace Terminal.Gui { /// /// Array of one or more views (can be optional parameter). /// - /// The Views that have been added to this view can be retrieved via the property. See also + /// The Views that have been added to this view can be retrieved via the property. + /// See also /// public void Add (params View [] views) { @@ -998,13 +987,13 @@ namespace Terminal.Gui { if (view == null || subviews == null) return; - SetNeedsLayout (); - SetNeedsDisplay (); var touched = view.Frame; subviews.Remove (view); tabIndexes.Remove (view); view.container = null; view.tabIndex = -1; + SetNeedsLayout (); + SetNeedsDisplay (); if (subviews.Count < 1) { CanFocus = false; } @@ -1104,11 +1093,18 @@ namespace Terminal.Gui { /// public void Clear () { - var h = Frame.Height; - var w = Frame.Width; - for (int line = 0; line < h; line++) { + Rect containerBounds = GetContainerBounds (); + Rect viewBounds = Bounds; + if (!containerBounds.IsEmpty) { + viewBounds.Width = Math.Min (viewBounds.Width, containerBounds.Width); + viewBounds.Height = Math.Min (viewBounds.Height, containerBounds.Height); + } + + var h = viewBounds.Height; + var w = viewBounds.Width; + for (var line = 0; line < h; line++) { Move (0, line); - for (int col = 0; col < w; col++) + for (var col = 0; col < w; col++) Driver.AddRune (' '); } } @@ -1123,9 +1119,9 @@ namespace Terminal.Gui { { var h = regionScreen.Height; var w = regionScreen.Width; - for (int line = regionScreen.Y; line < regionScreen.Y + h; line++) { + for (var line = regionScreen.Y; line < regionScreen.Y + h; line++) { Driver.Move (regionScreen.X, line); - for (int col = 0; col < w; col++) + for (var col = 0; col < w; col++) Driver.AddRune (' '); } } @@ -1137,17 +1133,18 @@ namespace Terminal.Gui { /// View-relative row. /// Absolute column; screen-relative. /// Absolute row; screen-relative. - /// Whether to clip the result of the ViewToScreen method, if set to true, the rcol, rrow values are clamped to the screen (terminal) dimensions (0..TerminalDim-1). + /// Whether to clip the result of the ViewToScreen method, if set to , the rcol, rrow values are clamped to the screen (terminal) dimensions (0..TerminalDim-1). internal void ViewToScreen (int col, int row, out int rcol, out int rrow, bool clipped = true) { // Computes the real row, col relative to the screen. rrow = row + frame.Y; rcol = col + frame.X; - var ccontainer = container; - while (ccontainer != null) { - rrow += ccontainer.frame.Y; - rcol += ccontainer.frame.X; - ccontainer = ccontainer.container; + + var curContainer = container; + while (curContainer != null) { + rrow += curContainer.frame.Y; + rcol += curContainer.frame.X; + curContainer = curContainer.container; } // The following ensures that the cursor is always in the screen boundaries. @@ -1222,7 +1219,7 @@ namespace Terminal.Gui { /// /// View-relative region for the frame to be drawn. /// The padding to add around the outside of the drawn frame. - /// If set to true it fill will the contents. + /// If set to it fill will the contents. public void DrawFrame (Rect region, int padding = 0, bool fill = false) { var scrRect = ViewToScreen (region); @@ -1259,7 +1256,7 @@ namespace Terminal.Gui { /// Utility function to draw strings that contains a hotkey using a and the "focused" state. /// /// String to display, the underscore before a letter flags the next letter as the hotkey. - /// If set to true this uses the focused colors from the color scheme, otherwise the regular ones. + /// If set to this uses the focused colors from the color scheme, otherwise the regular ones. /// The color scheme to use. public void DrawHotString (ustring text, bool focused, ColorScheme scheme) { @@ -1276,15 +1273,15 @@ namespace Terminal.Gui { /// Col. /// Row. /// Whether to clip the result of the ViewToScreen method, - /// if set to true, the col, row values are clamped to the screen (terminal) dimensions (0..TerminalDim-1). + /// if set to , the col, row values are clamped to the screen (terminal) dimensions (0..TerminalDim-1). public void Move (int col, int row, bool clipped = true) { if (Driver.Rows == 0) { return; } - ViewToScreen (col, row, out var rcol, out var rrow, clipped); - Driver.Move (rcol, rrow); + ViewToScreen (col, row, out var rCol, out var rRow, clipped); + Driver.Move (rCol, rRow); } /// @@ -1313,12 +1310,9 @@ namespace Terminal.Gui { } bool hasFocus; + /// - public override bool HasFocus { - get { - return hasFocus; - } - } + public override bool HasFocus => hasFocus; void SetHasFocus (bool value, View view, bool force = false) { @@ -1334,8 +1328,9 @@ namespace Terminal.Gui { // Remove focus down the chain of subviews if focus is removed if (!value && focused != null) { - focused.OnLeave (view); - focused.SetHasFocus (false, view); + var f = focused; + f.OnLeave (view); + f.SetHasFocus (false, view); focused = null; } } @@ -1361,7 +1356,7 @@ namespace Terminal.Gui { } /// - /// Method invoked when a subview is being added to this view. + /// Method invoked when a subview is being added to this view. /// /// The subview being added. public virtual void OnAdded (View view) @@ -1388,7 +1383,7 @@ namespace Terminal.Gui { /// public override bool OnEnter (View view) { - FocusEventArgs args = new FocusEventArgs (view); + var args = new FocusEventArgs (view); Enter?.Invoke (args); if (args.Handled) return true; @@ -1401,7 +1396,7 @@ namespace Terminal.Gui { /// public override bool OnLeave (View view) { - FocusEventArgs args = new FocusEventArgs (view); + var args = new FocusEventArgs (view); Leave?.Invoke (args); if (args.Handled) return true; @@ -1420,7 +1415,7 @@ namespace Terminal.Gui { /// /// Returns the most focused view in the chain of subviews (the leaf view that has the focus). /// - /// The most focused. + /// The most focused View. public View MostFocused { get { if (Focused == null) @@ -1491,7 +1486,7 @@ namespace Terminal.Gui { /// /// /// Overrides of must ensure they do not set Driver.Clip to a clip region - /// larger than the region parameter. + /// larger than the parameter, as this will cause the driver to clip the entire region. /// /// public virtual void Redraw (Rect bounds) @@ -1502,26 +1497,28 @@ namespace Terminal.Gui { var clipRect = new Rect (Point.Empty, frame.Size); - //if (ColorScheme != null && !(this is Toplevel)) { if (ColorScheme != null) { Driver.SetAttribute (HasFocus ? ColorScheme.Focus : ColorScheme.Normal); } if (Border != null) { Border.DrawContent (this); + } else if (ustring.IsNullOrEmpty (TextFormatter.Text) && + (GetType ().IsNestedPublic) && !IsOverridden (this, "Redraw") && + (!NeedDisplay.IsEmpty || ChildNeedsDisplay || LayoutNeeded)) { + + Clear (); + SetChildNeedsDisplay (); } - if (!ustring.IsNullOrEmpty (TextFormatter.Text) || (this is Label && !AutoSize)) { + if (!ustring.IsNullOrEmpty (TextFormatter.Text)) { Clear (); + SetChildNeedsDisplay (); // Draw any Text if (TextFormatter != null) { TextFormatter.NeedsFormat = true; } - var containerBounds = SuperView == null ? default : SuperView.ViewToScreen (SuperView.Bounds); - containerBounds.X = Math.Max (containerBounds.X, Driver.Clip.X); - containerBounds.Y = Math.Max (containerBounds.Y, Driver.Clip.Y); - containerBounds.Width = Math.Min (containerBounds.Width, Driver.Clip.Width); - containerBounds.Height = Math.Min (containerBounds.Height, Driver.Clip.Height); + Rect containerBounds = GetContainerBounds (); TextFormatter?.Draw (ViewToScreen (Bounds), HasFocus ? ColorScheme.Focus : GetNormalColor (), HasFocus ? ColorScheme.HotFocus : Enabled ? ColorScheme.HotNormal : ColorScheme.Disabled, containerBounds); @@ -1564,6 +1561,17 @@ namespace Terminal.Gui { ClearNeedsDisplay (); } + Rect GetContainerBounds () + { + var containerBounds = SuperView == null ? default : SuperView.ViewToScreen (SuperView.Bounds); + var driverClip = Driver == null ? Rect.Empty : Driver.Clip; + containerBounds.X = Math.Max (containerBounds.X, driverClip.X); + containerBounds.Y = Math.Max (containerBounds.Y, driverClip.Y); + containerBounds.Width = Math.Min (containerBounds.Width, driverClip.Width); + containerBounds.Height = Math.Min (containerBounds.Height, driverClip.Height); + return containerBounds; + } + /// /// Event invoked when the content area of the View is to be drawn. /// @@ -1695,7 +1703,7 @@ namespace Terminal.Gui { return false; } - KeyEventEventArgs args = new KeyEventEventArgs (keyEvent); + var args = new KeyEventEventArgs (keyEvent); KeyPress?.Invoke (args); if (args.Handled) return true; @@ -1704,10 +1712,8 @@ namespace Terminal.Gui { if (args.Handled) return true; } - if (Focused?.Enabled == true && Focused?.ProcessKey (keyEvent) == true) - return true; - return false; + return Focused?.Enabled == true && Focused?.ProcessKey (keyEvent) == true; } /// @@ -1737,7 +1743,7 @@ namespace Terminal.Gui { // if ever see a true then that's what we will return if (thisReturn ?? false) { - toReturn = thisReturn.Value; + toReturn = true; } } } @@ -1753,11 +1759,11 @@ namespace Terminal.Gui { /// If the key is already bound to a different it will be /// rebound to this one /// Commands are only ever applied to the current (i.e. this feature - /// cannot be used to switch focus to another view and perform multiple commands there) + /// cannot be used to switch focus to another view and perform multiple commands there) /// /// /// The command(s) to run on the when is pressed. - /// When specifying multiple, all commands will be applied in sequence. The bound strike + /// When specifying multiple commands, all commands will be applied in sequence. The bound strike /// will be consumed if any took effect. public void AddKeyBinding (Key key, params Command [] command) { @@ -1787,18 +1793,17 @@ namespace Terminal.Gui { } /// - /// Checks if key combination already exist. + /// Checks if the key binding already exists. /// /// The key to check. - /// true If the key already exist, falseotherwise. + /// If the key already exist, otherwise. public bool ContainsKeyBinding (Key key) { return KeyBindings.ContainsKey (key); } /// - /// Removes all bound keys from the View making including the default - /// key combinations such as cursor navigation, scrolling etc + /// Removes all bound keys from the View and resets the default bindings. /// public void ClearKeybindings () { @@ -1806,7 +1811,7 @@ namespace Terminal.Gui { } /// - /// Clears the existing keybinding (if any) for the given + /// Clears the existing keybinding (if any) for the given . /// /// public void ClearKeybinding (Key key) @@ -1815,7 +1820,7 @@ namespace Terminal.Gui { } /// - /// Removes all key bindings that trigger the given command. Views can have multiple different + /// Removes all key bindings that trigger the given command. Views can have multiple different /// keys bound to the same command and this method will clear all of them. /// /// @@ -1848,7 +1853,7 @@ namespace Terminal.Gui { } /// - /// Returns all commands that are supported by this + /// Returns all commands that are supported by this . /// /// public IEnumerable GetSupportedCommands () @@ -1863,7 +1868,7 @@ namespace Terminal.Gui { /// The used by a public Key GetKeyFromCommand (params Command [] command) { - return KeyBindings.First (x => x.Value.SequenceEqual (command)).Key; + return KeyBindings.First (kb => kb.Value.SequenceEqual (command)).Key; } /// @@ -1873,7 +1878,7 @@ namespace Terminal.Gui { return false; } - KeyEventEventArgs args = new KeyEventEventArgs (keyEvent); + var args = new KeyEventEventArgs (keyEvent); if (MostFocused?.Enabled == true) { MostFocused?.KeyPress?.Invoke (args); if (args.Handled) @@ -1883,6 +1888,7 @@ namespace Terminal.Gui { return true; if (subviews == null || subviews.Count == 0) return false; + foreach (var view in subviews) if (view.Enabled && view.ProcessHotKey (keyEvent)) return true; @@ -1896,7 +1902,7 @@ namespace Terminal.Gui { return false; } - KeyEventEventArgs args = new KeyEventEventArgs (keyEvent); + var args = new KeyEventEventArgs (keyEvent); KeyPress?.Invoke (args); if (args.Handled) return true; @@ -1909,6 +1915,7 @@ namespace Terminal.Gui { return true; if (subviews == null || subviews.Count == 0) return false; + foreach (var view in subviews) if (view.Enabled && view.ProcessColdKey (keyEvent)) return true; @@ -1916,7 +1923,7 @@ namespace Terminal.Gui { } /// - /// Invoked when a key is pressed + /// Invoked when a key is pressed. /// public event Action KeyDown; @@ -1927,7 +1934,7 @@ namespace Terminal.Gui { return false; } - KeyEventEventArgs args = new KeyEventEventArgs (keyEvent); + var args = new KeyEventEventArgs (keyEvent); KeyDown?.Invoke (args); if (args.Handled) { return true; @@ -1946,7 +1953,7 @@ namespace Terminal.Gui { } /// - /// Invoked when a key is released + /// Invoked when a key is released. /// public event Action KeyUp; @@ -1957,7 +1964,7 @@ namespace Terminal.Gui { return false; } - KeyEventEventArgs args = new KeyEventEventArgs (keyEvent); + var args = new KeyEventEventArgs (keyEvent); KeyUp?.Invoke (args); if (args.Handled) { return true; @@ -1976,7 +1983,7 @@ namespace Terminal.Gui { } /// - /// Finds the first view in the hierarchy that wants to get the focus if nothing is currently focused, otherwise, it does nothing. + /// Finds the first view in the hierarchy that wants to get the focus if nothing is currently focused, otherwise, does nothing. /// public void EnsureFocus () { @@ -2025,10 +2032,10 @@ namespace Terminal.Gui { return; } - for (int i = tabIndexes.Count; i > 0;) { + for (var i = tabIndexes.Count; i > 0;) { i--; - View v = tabIndexes [i]; + var v = tabIndexes [i]; if (v.CanFocus && v.tabStop && v.Visible && v.Enabled) { SetFocus (v); return; @@ -2039,7 +2046,7 @@ namespace Terminal.Gui { /// /// Focuses the previous view. /// - /// true, if previous was focused, false otherwise. + /// if previous was focused, otherwise. public bool FocusPrev () { if (!CanBeVisible (this)) { @@ -2054,21 +2061,22 @@ namespace Terminal.Gui { FocusLast (); return focused != null; } - int focused_idx = -1; - for (int i = tabIndexes.Count; i > 0;) { + + var focusedIdx = -1; + for (var i = tabIndexes.Count; i > 0;) { i--; - View w = tabIndexes [i]; + var w = tabIndexes [i]; if (w.HasFocus) { if (w.FocusPrev ()) return true; - focused_idx = i; + focusedIdx = i; continue; } - if (w.CanFocus && focused_idx != -1 && w.tabStop && w.Visible && w.Enabled) { + if (w.CanFocus && focusedIdx != -1 && w.tabStop && w.Visible && w.Enabled) { focused.SetHasFocus (false, w); - if (w != null && w.CanFocus && w.tabStop && w.Visible && w.Enabled) + if (w.CanFocus && w.tabStop && w.Visible && w.Enabled) w.FocusLast (); SetFocus (w); @@ -2085,7 +2093,7 @@ namespace Terminal.Gui { /// /// Focuses the next view. /// - /// true, if next was focused, false otherwise. + /// if next was focused, otherwise. public bool FocusNext () { if (!CanBeVisible (this)) { @@ -2100,21 +2108,21 @@ namespace Terminal.Gui { FocusFirst (); return focused != null; } - int n = tabIndexes.Count; - int focused_idx = -1; - for (int i = 0; i < n; i++) { - View w = tabIndexes [i]; + var n = tabIndexes.Count; + var focusedIdx = -1; + for (var i = 0; i < n; i++) { + var w = tabIndexes [i]; if (w.HasFocus) { if (w.FocusNext ()) return true; - focused_idx = i; + focusedIdx = i; continue; } - if (w.CanFocus && focused_idx != -1 && w.tabStop && w.Visible && w.Enabled) { + if (w.CanFocus && focusedIdx != -1 && w.tabStop && w.Visible && w.Enabled) { focused.SetHasFocus (false, w); - if (w != null && w.CanFocus && w.tabStop && w.Visible && w.Enabled) + if (w.CanFocus && w.tabStop && w.Visible && w.Enabled) w.FocusFirst (); SetFocus (w); @@ -2131,14 +2139,10 @@ namespace Terminal.Gui { View GetMostFocused (View view) { if (view == null) { - return view; + return null; } - if (view.focused != null) { - return GetMostFocused (view.focused); - } else { - return view; - } + return view.focused != null ? GetMostFocused (view.focused) : view; } /// @@ -2150,7 +2154,7 @@ namespace Terminal.Gui { /// internal void SetRelativeLayout (Rect hostFrame) { - int w, h, _x, _y; + int actW, actH, actX, actY; var s = Size.Empty; if (AutoSize) { @@ -2159,71 +2163,112 @@ namespace Terminal.Gui { if (x is Pos.PosCenter) { if (width == null) { - w = AutoSize ? s.Width : hostFrame.Width; + actW = AutoSize ? s.Width : hostFrame.Width; } else { - w = width.Anchor (hostFrame.Width); - w = AutoSize && s.Width > w ? s.Width : w; + actW = width.Anchor (hostFrame.Width); + actW = AutoSize && s.Width > actW ? s.Width : actW; } - _x = x.Anchor (hostFrame.Width - w); + actX = x.Anchor (hostFrame.Width - actW); } else { - if (x == null) - _x = 0; - else - _x = x.Anchor (hostFrame.Width); - if (width == null) { - w = AutoSize ? s.Width : hostFrame.Width; - } else if (width is Dim.DimFactor && !((Dim.DimFactor)width).IsFromRemaining ()) { - w = width.Anchor (hostFrame.Width); - w = AutoSize && s.Width > w ? s.Width : w; - } else { - w = Math.Max (width.Anchor (hostFrame.Width - _x), 0); - w = AutoSize && s.Width > w ? s.Width : w; - } + actX = x?.Anchor (hostFrame.Width) ?? 0; + + actW = Math.Max (CalculateActualWidth (width, hostFrame, actX, s), 0); } if (y is Pos.PosCenter) { if (height == null) { - h = AutoSize ? s.Height : hostFrame.Height; + actH = AutoSize ? s.Height : hostFrame.Height; } else { - h = height.Anchor (hostFrame.Height); - h = AutoSize && s.Height > h ? s.Height : h; + actH = height.Anchor (hostFrame.Height); + actH = AutoSize && s.Height > actH ? s.Height : actH; } - _y = y.Anchor (hostFrame.Height - h); + actY = y.Anchor (hostFrame.Height - actH); } else { - if (y == null) - _y = 0; - else - _y = y.Anchor (hostFrame.Height); - if (height == null) { - h = AutoSize ? s.Height : hostFrame.Height; - } else if (height is Dim.DimFactor && !((Dim.DimFactor)height).IsFromRemaining ()) { - h = height.Anchor (hostFrame.Height); - h = AutoSize && s.Height > h ? s.Height : h; - } else { - h = Math.Max (height.Anchor (hostFrame.Height - _y), 0); - h = AutoSize && s.Height > h ? s.Height : h; - } + actY = y?.Anchor (hostFrame.Height) ?? 0; + + actH = Math.Max (CalculateActualHight (height, hostFrame, actY, s), 0); } - var r = new Rect (_x, _y, w, h); + + var r = new Rect (actX, actY, actW, actH); if (Frame != r) { - Frame = new Rect (_x, _y, w, h); + Frame = r; if (!SetMinWidthHeight ()) TextFormatter.Size = GetBoundsTextFormatterSize (); } } + private int CalculateActualWidth (Dim width, Rect hostFrame, int actX, Size s) + { + int actW; + switch (width) { + case null: + actW = AutoSize ? s.Width : hostFrame.Width; + break; + case Dim.DimCombine combine: + int leftActW = CalculateActualWidth (combine.left, hostFrame, actX, s); + int rightActW = CalculateActualWidth (combine.right, hostFrame, actX, s); + if (combine.add) { + actW = leftActW + rightActW; + } else { + actW = leftActW - rightActW; + } + actW = AutoSize && s.Width > actW ? s.Width : actW; + break; + case Dim.DimFactor factor when !factor.IsFromRemaining (): + actW = width.Anchor (hostFrame.Width); + actW = AutoSize && s.Width > actW ? s.Width : actW; + break; + default: + actW = Math.Max (width.Anchor (hostFrame.Width - actX), 0); + actW = AutoSize && s.Width > actW ? s.Width : actW; + break; + } + + return actW; + } + + private int CalculateActualHight (Dim height, Rect hostFrame, int actY, Size s) + { + int actH; + switch (height) { + case null: + actH = AutoSize ? s.Height : hostFrame.Height; + break; + case Dim.DimCombine combine: + int leftActH = CalculateActualHight (combine.left, hostFrame, actY, s); + int rightActH = CalculateActualHight (combine.right, hostFrame, actY, s); + if (combine.add) { + actH = leftActH + rightActH; + } else { + actH = leftActH - rightActH; + } + actH = AutoSize && s.Height > actH ? s.Height : actH; + break; + case Dim.DimFactor factor when !factor.IsFromRemaining (): + actH = height.Anchor (hostFrame.Height); + actH = AutoSize && s.Height > actH ? s.Height : actH; + break; + default: + actH = Math.Max (height.Anchor (hostFrame.Height - actY), 0); + actH = AutoSize && s.Height > actH ? s.Height : actH; + break; + } + + return actH; + } + // https://en.wikipedia.org/wiki/Topological_sorting - List TopologicalSort (HashSet nodes, HashSet<(View From, View To)> edges) + List TopologicalSort (IEnumerable nodes, ICollection<(View From, View To)> edges) { var result = new List (); // Set of all nodes with no incoming edges - var S = new HashSet (nodes.Where (n => edges.All (e => !e.To.Equals (n)))); + var noEdgeNodes = new HashSet (nodes.Where (n => edges.All (e => !e.To.Equals (n)))); - while (S.Any ()) { + while (noEdgeNodes.Any ()) { // remove a node n from S - var n = S.First (); - S.Remove (n); + var n = noEdgeNodes.First (); + noEdgeNodes.Remove (n); // add n to tail of L if (n != this?.SuperView) @@ -2239,13 +2284,13 @@ namespace Terminal.Gui { // if m has no other incoming edges then if (edges.All (me => !me.To.Equals (m)) && m != this?.SuperView) { // insert m into S - S.Add (m); + noEdgeNodes.Add (m); } } } if (edges.Any ()) { - var (from, to) = edges.First (); + (var from, var to) = edges.First (); if (from != Application.Top) { if (!ReferenceEquals (from, to)) { throw new InvalidOperationException ($"TopologicalSort (for Pos/Dim) cannot find {from} linked with {to}. Did you forget to add it to {this}?"); @@ -2270,7 +2315,7 @@ namespace Terminal.Gui { } /// - /// Fired after the Views's method has completed. + /// Fired after the View's method has completed. /// /// /// Subscribe to this event to perform tasks when the has been resized or the layout has otherwise changed. @@ -2286,7 +2331,7 @@ namespace Terminal.Gui { } /// - /// Fired after the Views's method has completed. + /// Fired after the View's method has completed. /// /// /// Subscribe to this event to perform tasks when the has been resized or the layout has otherwise changed. @@ -2321,7 +2366,7 @@ namespace Terminal.Gui { return; } - Rect oldBounds = Bounds; + var oldBounds = Bounds; OnLayoutStarted (new LayoutEventArgs () { OldBounds = oldBounds }); TextFormatter.Size = GetBoundsTextFormatterSize (); @@ -2333,7 +2378,8 @@ namespace Terminal.Gui { void CollectPos (Pos pos, View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges) { - if (pos is Pos.PosView pv) { + switch (pos) { + case Pos.PosView pv: if (pv.Target != this) { nEdges.Add ((pv.Target, from)); } @@ -2341,18 +2387,19 @@ namespace Terminal.Gui { CollectAll (v, ref nNodes, ref nEdges); } return; - } - if (pos is Pos.PosCombine pc) { + case Pos.PosCombine pc: foreach (var v in from.InternalSubviews) { CollectPos (pc.left, from, ref nNodes, ref nEdges); CollectPos (pc.right, from, ref nNodes, ref nEdges); } + break; } } void CollectDim (Dim dim, View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges) { - if (dim is Dim.DimView dv) { + switch (dim) { + case Dim.DimView dv: if (dv.Target != this) { nEdges.Add ((dv.Target, from)); } @@ -2360,12 +2407,12 @@ namespace Terminal.Gui { CollectAll (v, ref nNodes, ref nEdges); } return; - } - if (dim is Dim.DimCombine dc) { + case Dim.DimCombine dc: foreach (var v in from.InternalSubviews) { CollectDim (dc.left, from, ref nNodes, ref nEdges); CollectDim (dc.right, from, ref nNodes, ref nEdges); } + break; } } @@ -2438,10 +2485,10 @@ namespace Terminal.Gui { /// /// Gets or sets a flag that determines whether the View will be automatically resized to fit the . - /// The default is `false`. Set to `true` to turn on AutoSize. If is `true` the + /// The default is . Set to to turn on AutoSize. If is the /// and will always be used if the text size is lower. If the text size is higher the bounds will /// be resized to fit it. - /// In addition, if is `true` the new values of and + /// In addition, if is the new values of and /// must be of the same types of the existing one to avoid breaking the settings. /// public virtual bool AutoSize { @@ -2459,10 +2506,11 @@ namespace Terminal.Gui { } /// - /// Gets or sets a flag that determines whether will have trailing spaces preserved - /// or not when is enabled. If `true` any trailing spaces will be trimmed when - /// either the property is changed or when is set to `true`. - /// The default is `false`. + /// Gets or sets a flag that determines whether will have trailing spaces preserved + /// or not when is enabled. If + /// any trailing spaces will be trimmed when either the property is changed or + /// when is set to . + /// The default is . /// public virtual bool PreserveTrailingSpaces { get => TextFormatter.PreserveTrailingSpaces; @@ -2488,7 +2536,7 @@ namespace Terminal.Gui { } /// - /// Gets or sets how the View's is aligned verticaly when drawn. Changing this property will redisplay the . + /// Gets or sets how the View's is aligned vertically when drawn. Changing this property will redisplay the . /// /// The text alignment. public virtual VerticalTextAlignment VerticalTextAlignment { @@ -2507,7 +2555,7 @@ namespace Terminal.Gui { get => TextFormatter.Direction; set { if (TextFormatter.Direction != value) { - var isValidOldAutSize = autoSize && IsValidAutoSize (out Size autSize); + var isValidOldAutSize = autoSize && IsValidAutoSize (out var _); var directionChanged = TextFormatter.IsHorizontalDirection (TextFormatter.Direction) != TextFormatter.IsHorizontalDirection (value); @@ -2558,7 +2606,7 @@ namespace Terminal.Gui { foreach (var view in subviews) { if (!value) { view.oldEnabled = view.Enabled; - view.Enabled = value; + view.Enabled = false; } else { view.Enabled = view.oldEnabled; view.addingView = false; @@ -2618,7 +2666,7 @@ namespace Terminal.Gui { void SetHotKey () { - TextFormatter.FindHotKey (text, HotKeySpecifier, true, out _, out Key hk); + TextFormatter.FindHotKey (text, HotKeySpecifier, true, out _, out var hk); if (hotKey != hk) { HotKey = hk; } @@ -2645,9 +2693,9 @@ namespace Terminal.Gui { bool SetWidthHeight (Size nBounds) { - bool aSize = false; - var canSizeW = SetWidth (nBounds.Width - GetHotKeySpecifierLength (), out int rW); - var canSizeH = SetHeight (nBounds.Height - GetHotKeySpecifierLength (false), out int rH); + var aSize = false; + var canSizeW = SetWidth (nBounds.Width - GetHotKeySpecifierLength (), out var rW); + var canSizeH = SetHeight (nBounds.Height - GetHotKeySpecifierLength (false), out var rH); if (canSizeW) { aSize = true; width = rW; @@ -2702,10 +2750,10 @@ namespace Terminal.Gui { } /// - /// Get the width or height of the length. + /// Get the width or height of the length. /// - /// trueif is the width (default)falseif is the height. - /// The length of the . + /// if is the width (default) if is the height. + /// The length of the . public int GetHotKeySpecifierLength (bool isWidth = true) { if (isWidth) { @@ -2720,9 +2768,9 @@ namespace Terminal.Gui { } /// - /// Gets the bounds size from a . + /// Gets the bounds size from a . /// - /// The bounds size minus the length. + /// The bounds size minus the length. public Size GetTextFormatterBoundsSize () { return new Size (TextFormatter.Size.Width - GetHotKeySpecifierLength (), @@ -2732,7 +2780,7 @@ namespace Terminal.Gui { /// /// Gets the text formatter size from a size. /// - /// The text formatter size more the length. + /// The text formatter size more the length. public Size GetBoundsTextFormatterSize () { if (ustring.IsNullOrEmpty (TextFormatter.Text)) @@ -2743,7 +2791,9 @@ namespace Terminal.Gui { } /// - /// Specifies the event arguments for + /// Specifies the event arguments for . This is a higher-level construct + /// than the wrapped class and is used for the events defined on + /// and subclasses of View (e.g. and ). /// public class MouseEventArgs : EventArgs { /// @@ -2755,11 +2805,17 @@ namespace Terminal.Gui { /// The for the event. /// public MouseEvent MouseEvent { get; set; } + /// /// Indicates if the current mouse event has already been processed and the driver should stop notifying any other event subscriber. /// Its important to set this value to true specially when updating any View's layout from inside the subscriber method. /// - public bool Handled { get; set; } + /// This property forwards to the property and is provided as a convenience and for + /// backwards compatibility + public bool Handled { + get => MouseEvent.Handled; + set => MouseEvent.Handled = value; + } } /// @@ -2773,14 +2829,10 @@ namespace Terminal.Gui { return false; } - MouseEventArgs args = new MouseEventArgs (mouseEvent); + var args = new MouseEventArgs (mouseEvent); MouseEnter?.Invoke (args); - if (args.Handled) - return true; - if (base.OnMouseEnter (mouseEvent)) - return true; - return false; + return args.Handled || base.OnMouseEnter (mouseEvent); } /// @@ -2794,21 +2846,17 @@ namespace Terminal.Gui { return false; } - MouseEventArgs args = new MouseEventArgs (mouseEvent); + var args = new MouseEventArgs (mouseEvent); MouseLeave?.Invoke (args); - if (args.Handled) - return true; - if (base.OnMouseLeave (mouseEvent)) - return true; - return false; + return args.Handled || base.OnMouseLeave (mouseEvent); } /// /// Method invoked when a mouse event is generated /// /// - /// true, if the event was handled, false otherwise. + /// , if the event was handled, otherwise. public virtual bool OnMouseEvent (MouseEvent mouseEvent) { if (!Enabled) { @@ -2819,7 +2867,7 @@ namespace Terminal.Gui { return false; } - MouseEventArgs args = new MouseEventArgs (mouseEvent); + var args = new MouseEventArgs (mouseEvent); if (OnMouseClick (args)) return true; if (MouseEvent (mouseEvent)) @@ -2861,8 +2909,8 @@ namespace Terminal.Gui { /// protected override void Dispose (bool disposing) { - for (int i = InternalSubviews.Count - 1; i >= 0; i--) { - View subview = InternalSubviews [i]; + for (var i = InternalSubviews.Count - 1; i >= 0; i--) { + var subview = InternalSubviews [i]; Remove (subview); subview.Dispose (); } @@ -2919,7 +2967,7 @@ namespace Terminal.Gui { bool CanSetWidth (int desiredWidth, out int resultWidth) { - int w = desiredWidth; + var w = desiredWidth; bool canSetWidth; if (Width is Dim.DimCombine || Width is Dim.DimView || Width is Dim.DimFill) { // It's a Dim.DimCombine and so can't be assigned. Let it have it's width anchored. @@ -2943,13 +2991,17 @@ namespace Terminal.Gui { bool CanSetHeight (int desiredHeight, out int resultHeight) { - int h = desiredHeight; + var h = desiredHeight; bool canSetHeight; - if (Height is Dim.DimCombine || Height is Dim.DimView || Height is Dim.DimFill) { + switch (Height) { + case Dim.DimCombine _: + case Dim.DimView _: + case Dim.DimFill _: // It's a Dim.DimCombine and so can't be assigned. Let it have it's height anchored. h = Height.Anchor (h); canSetHeight = !ForceValidatePosDim; - } else if (Height is Dim.DimFactor factor) { + break; + case Dim.DimFactor factor: // Tries to get the SuperView height otherwise the view height. var sh = SuperView != null ? SuperView.Frame.Height : h; if (factor.IsFromRemaining ()) { @@ -2957,8 +3009,10 @@ namespace Terminal.Gui { } h = Height.Anchor (sh); canSetHeight = !ForceValidatePosDim; - } else { + break; + default: canSetHeight = true; + break; } resultHeight = h; @@ -2970,7 +3024,7 @@ namespace Terminal.Gui { /// /// The desired width. /// The real result width. - /// true if the width can be directly assigned, false otherwise. + /// if the width can be directly assigned, otherwise. public bool SetWidth (int desiredWidth, out int resultWidth) { return CanSetWidth (desiredWidth, out resultWidth); @@ -2981,7 +3035,7 @@ namespace Terminal.Gui { /// /// The desired height. /// The real result height. - /// true if the height can be directly assigned, false otherwise. + /// if the height can be directly assigned, otherwise. public bool SetHeight (int desiredHeight, out int resultHeight) { return CanSetHeight (desiredHeight, out resultHeight); @@ -2991,10 +3045,10 @@ namespace Terminal.Gui { /// Gets the current width based on the settings. /// /// The real current width. - /// true if the width can be directly assigned, false otherwise. + /// if the width can be directly assigned, otherwise. public bool GetCurrentWidth (out int currentWidth) { - SetRelativeLayout (SuperView == null ? frame : SuperView.frame); + SetRelativeLayout (SuperView?.frame ?? frame); currentWidth = frame.Width; return CanSetWidth (0, out _); @@ -3004,10 +3058,10 @@ namespace Terminal.Gui { /// Calculate the height based on the settings. /// /// The real current height. - /// true if the height can be directly assigned, false otherwise. + /// if the height can be directly assigned, otherwise. public bool GetCurrentHeight (out int currentHeight) { - SetRelativeLayout (SuperView == null ? frame : SuperView.frame); + SetRelativeLayout (SuperView?.frame ?? frame); currentHeight = frame.Height; return CanSetHeight (0, out _); @@ -3016,14 +3070,25 @@ namespace Terminal.Gui { /// /// Determines the current based on the value. /// - /// if is - /// or if is . + /// if is + /// or if is . /// If it's overridden can return other values. public virtual Attribute GetNormalColor () { return Enabled ? ColorScheme.Normal : ColorScheme.Disabled; } + /// + /// Determines the current based on the value. + /// + /// if is + /// or if is . + /// If it's overridden can return other values. + public virtual Attribute GetHotNormalColor () + { + return Enabled ? ColorScheme.HotNormal : ColorScheme.Disabled; + } + /// /// Get the top superview of a given . /// @@ -3032,9 +3097,7 @@ namespace Terminal.Gui { { View top = Application.Top; for (var v = this?.SuperView; v != null; v = v.SuperView) { - if (v != null) { - top = v; - } + top = v; } return top; diff --git a/Terminal.Gui/Core/Window.cs b/Terminal.Gui/Core/Window.cs index b79d608df..85c8929cc 100644 --- a/Terminal.Gui/Core/Window.cs +++ b/Terminal.Gui/Core/Window.cs @@ -277,18 +277,17 @@ namespace Terminal.Gui { { var padding = Border.GetSumThickness (); var scrRect = ViewToScreen (new Rect (0, 0, Frame.Width, Frame.Height)); - //var borderLength = Border.DrawMarginFrame ? 1 : 0; - // FIXED: Why do we draw the frame twice? This call is here to clear the content area, I think. Why not just clear that area? - if (!NeedDisplay.IsEmpty) { + if (!NeedDisplay.IsEmpty || ChildNeedsDisplay || LayoutNeeded) { Driver.SetAttribute (GetNormalColor ()); Clear (); + contentView.SetNeedsDisplay (); } var savedClip = contentView.ClipToBounds (); // Redraw our contentView // DONE: smartly constrict contentView.Bounds to just be what intersects with the 'bounds' we were passed - contentView.Redraw (!NeedDisplay.IsEmpty ? contentView.Bounds : bounds); + contentView.Redraw (!NeedDisplay.IsEmpty || ChildNeedsDisplay || LayoutNeeded ? contentView.Bounds : bounds); Driver.Clip = savedClip; ClearLayoutNeeded (); @@ -303,12 +302,6 @@ namespace Terminal.Gui { if (Border.DrawMarginFrame) Driver.DrawWindowTitle (scrRect, Title, padding.Left, padding.Top, padding.Right, padding.Bottom); Driver.SetAttribute (GetNormalColor ()); - - // Checks if there are any SuperView view which intersect with this window. - if (SuperView != null) { - SuperView.SetNeedsLayout (); - SuperView.SetNeedsDisplay (); - } } /// diff --git a/Terminal.Gui/README.md b/Terminal.Gui/README.md index 1b621260a..37a026fd7 100644 --- a/Terminal.Gui/README.md +++ b/Terminal.Gui/README.md @@ -1,24 +1,24 @@ # Terminal.Gui Project -Contains all files required to build the **Terminal.Gui** library (and NuGet package). +All files required to build the **Terminal.Gui** library (and NuGet package). ## Project Folder Structure - `Terminal.Gui.sln` - The Visual Studio solution - `Core/` - Source files for all types that comprise the core building blocks of **Terminal-Gui** - `Application` - A `static` class that provides the base 'application driver'. Given it defines a **Terminal.Gui** application it is both logically and literally (because `static`) a singleton. It has direct dependencies on `MainLoop`, `Events.cs` `NetDriver`, `CursesDriver`, `WindowsDriver`, `Responder`, `View`, and `TopLevel` (and nothing else). - - `MainLoop` - Defines `IMainLoopDriver` and implements the and `MainLoop` class. + - `MainLoop` - Defines `IMainLoopDriver` and implements the `MainLoop` class. - `ConsoleDriver` - Definition for the Console Driver API. - - `Events.cs` - Defines keyboard and mouse related structs & classes. + - `Events.cs` - Defines keyboard and mouse-related structs & classes. - `PosDim.cs` - Implements *Computed Layout* system. These classes have deep dependencies on `View`. - `Responder` - Base class for the windowing class hierarchy. Implements support for keyboard & mouse input. - `View` - Derived from `Responder`, the base class for non-modal visual elements such as controls. - `Toplevel` - Derived from `View`, the base class for modal visual elements such as top-level windows and dialogs. Supports the concept of `MenuBar` and `StatusBar`. - - `Window` - Derived from `TopLevel`; implements top level views with a visible frame and Title. + - `Window` - Derived from `TopLevel`; implements toplevel views with a visible frame and Title. - `Types/` - A folder (not namespace) containing implementations of `Point`, `Rect`, and `Size` which are ancient versions of the modern `System.Drawing.Point`, `System.Drawing.Size`, and `System.Drawning.Rectangle`. - `ConsoleDrivers/` - Source files for the three `ConsoleDriver`-based drivers: .NET: `NetDriver`, Unix & Mac: `UnixDriver`, and Windows: `WindowsDriver`. - `Views/` - A folder (not namespace) containing the source for all built-in classes that drive from `View` (non-modals). -- `Windows/` - A folder (not namespace) containing the source all built-in classes that derive from `Window`. +- `Windows/` - A folder (not namespace) containing the source of all built-in classes that derive from `Window`. ## Version numbers @@ -55,43 +55,36 @@ The `tag` must be of the form `v..`, e.g. `v2.3.4`. `patch` can indicate pre-release or not (e.g. `pre`, `beta`, `rc`, etc...). -### 1) Generate release notes with the list of PRs since the last release +### 1) Verify the `develop` branch is ready for release -Use `gh` to get a list with just titles to make it easy to paste into release notes: +* Ensure everything is committed and pushed to the `develop` branch +* Ensure your local `develop` branch is up-to-date with `upstream/develop` -```powershell -gh pr list --limit 500 --search "is:pr is:closed is:merged closed:>=2021-05-18" -``` +### 2) Create a pull request for the release in the `develop` branch -Use the output to update `./Terminal.Gui/Terminal.Gui.csproj` with latest release notes - -### 2) Update the API documentation - -See `./docfx/README.md`. - -### 3) Create a PR for the release in the `develop` branch - -The PR title should be "Release v2.3.4" +The PR title should be of the form "Release v2.3.4" ```powershell git checkout develop -git pull -all +git pull upstream develop git checkout -b v_2_3_4 git add . git commit -m "Release v2.3.4" git push ``` -### 4) On github.com, verify the build action worked on your fork, then merge the PR +Go to the link printed by `git push` and fill out the Pull Request. -### 5) Pull the merged `develop` from `upstream` +### 3) On github.com, verify the build action worked on your fork, then merge the PR + +### 4) Pull the merged `develop` from `upstream` ```powershell git checkout develop git pull upstream develop ``` -### 6) Merge `develop` into `main` +### 5) Merge `develop` into `main` ```powershell git checkout main @@ -101,13 +94,13 @@ git merge develop Fix any merge errors. -### 7) Create a new annotated tag for the release +### 6) Create a new annotated tag for the release on `main` ```powershell git tag v2.3.4 -a -m "Release v2.3.4" ``` -### 8) Push the new tag to `main` on `origin` +### 7) Push the new tag to `main` on `upstream` ```powershell git push --atomic upstream main v2.3.4 @@ -115,16 +108,23 @@ git push --atomic upstream main v2.3.4 *See https://stackoverflow.com/a/3745250/297526* -### 9) Monitor Github actions to ensure the Nuget publishing worked. +### 8) Monitor Github Actions to ensure the Nuget publishing worked. -### 10) Check Nuget to see the new package version (wait a few minutes): +https://github.com/gui-cs/Terminal.Gui/actions + +### 9) Check Nuget to see the new package version (wait a few minutes) https://www.nuget.org/packages/Terminal.Gui -### 11) Add a new Release in Github: https://github.com/gui-cs/Terminal.Gui/releases +### 10) Add a new Release in Github: https://github.com/gui-cs/Terminal.Gui/releases -### 12) Tweet about it +Generate release notes with the list of PRs since the last release -### 13) Update the `develop` branch +Use `gh` to get a list with just titles to make it easy to paste into release notes: + +```powershell +gh pr list --limit 500 --search "is:pr is:closed is:merged closed:>=2021-05-18" +``` +### 11) Update the `develop` branch with the new version ```powershell git checkout develop diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index 1ca5e9f92..b7ee4c523 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -16,14 +16,15 @@ 2.0 + - + - + @@ -74,93 +75,12 @@ logo.png README.md csharp, terminal, c#, f#, gui, toolkit, console, tui - Cross Platform Terminal UI toolkit for .NET + Cross platform Terminal UI toolkit for .NET Miguel de Icaza, Charlie Kindel A toolkit for building rich console apps for .NET that works on Windows, Mac, and Linux/Unix. - Terminal.Gui - Cross Platform Terminal user interface toolkit for .NET + Terminal.Gui - Cross platform Terminal User Interface (TUI) toolkit for .NET - Release v1.8.1 - * Fixes #2053. MessageBox.Query not wrapping correctly - - Release v1.8.0 - * Fixes #2043. Update to NStack v1.0.3 - * Fixes #2045. TrySetClipboardData test must be enclosed with a lock. - * Fixes #2025. API Docs are now generated via Github Action - View Source Works - * Fixes #1991. Broken link in README - * Fixes #2026. Added ClearOnVisibleFalse to flag if the view must be cleared or not. - * Fixes #2017 and #2013. MainLoopTests.InvokeLeakTest failures - * Fixes #2014. Application mouseGrabView is run twice if return true. - * Fixes #2011. Wizard no longer needs to set specific colors, because #1971 has been fixed. - * Fixes #2006. ProgressBarStyles isn't disposing the _fractionTimer on quitting if running. - * Fixes #2004. TextFormatter.Justified not adding the extra spaces. - * Fixes #2002. Added feature to fill the remaining width with spaces. - * Fixes #1999. Prevents the mouseGrabView being executed with a null view. - * Fixes #1994. BREAKING CHANGE. Ensure only a single IdleHandlers list can exist. - * Fixes #1979. MessageBox.Query not wrapping since 1.7.1 - * Fixes #1989. ListView: Ensures SelectedItem visibility on MoveDown and MoveUp. - * Fixes #1987. Textview insert text newline fix - * Fixes #1984. Setting Label.Visible to false does not hide the Label - * Fixes #820. Added HideDropdownListOnClick property. - * Fixes #1981. Added SplitNewLine method to the TextFormatter. - * Fixes #1973. Avoid positioning Submenus off screen. - * Added abstract MakeColor and CreateColors to create the colors at once. - * Fixes #1800. TextView now uses the same colors as TextField. - * Fixes #1969. ESC on CursesDriver take to long to being processed. - * Fixes #1967. New keys for DeleteAll on TextField and TextView. - * Fixes #1962 - Change KeyBindings to allow chaining commands - * Fixes #1961 Null reference in Keybindings Scenario and hotkey collision - * Fixes #1963. Only remove one character on backspace when wordwrap is on - * Fixes #1959. GoToEnd should not fail on an empty TreeView - * Fixes #1953. TextView cursor position is not updating by mouse. - * Fixes #1951. TextView with selected text doesn't scroll beyond the cursor position. - * Fixes #1948. Get unwrapped cursor position when word wrap is enabled on TextView. - * Ensures that the isButtonShift flag is disabled in all situations. - * Fixes #1943. Mouse ButtonShift is not preserving the text selected. - - Release v1.7.2 - * Fixes #1773. Base color scheme for ListView hard to read - * Fixes #1934. WindowsDriver crash when the height is less than 1 with the VS Debugger - - Release v1.7.1 - * Fixes #1930. Trailing whitespace makes MessageBox.Query buttons disappear. - * Fixes #1921. Mouse continuous button pressed is not working on ScrollView. - * Fixes #1924. Wizard: Selected help text is unreadable - - Release v1.7.0 - * Moved Terminal.Gui (and NStack) to the github.com/gui-cs organization. - * Adds multi-step Wizard View for setup experiences (#124) - * The synchronization context method Send is now blocking (#1854). - * Fixes #1917. Sometimes Clipboard.IsSupported doesn't return the correct - * Fixes #1893: Fix URLs to match gui-cs Org - * Fixes #1883. Child TopLevels now get Loaded/Ready events. - * Fixes #1867, #1866, #1796. TextView enhancements for ReadOnly and WordWrap. - * Fixes #1861. Border: Title property is preferable to Text. - * Fixes #1855. Window and Frame content view without the margin frame. - * Fixes #1848. Mouse clicks in Windows Terminal. - * Fixes #1846. TabView now clips to the draw bounds. - * Fix TableView multi selections extending to -1 indexes - * Fixes #1837. Setting Unix clipboard freezes. - * Fixes #1839. Process WindowsDriver click event if location is the same after pressed and released. - * Fixes #1830. If "libcoreclr.so" is not present then "libncursesw.so" will be used. - * Fixes #1816. MessageBox: Hides underlying dialog when visible - * Fixes #1815. Now returns false if WSL clipboard isn't supported. - * Fixes #1825. Parent MenuItem stay focused if child MenuItem is empty. - * Fixes #1812, #1797, #1791. AutoSize fixes. - * Fixes #1818. Adds Title change events to Window. - * Fixes #1810. Dialog: Closing event is not fired when ESC is pressed to close dialog. - * Fixes #1793. ScrollBarView is hiding if the host fit the available space. - * Added Pos/Dim Function feature to automate layout. - * Fixes #1786. Windows Terminal is reporting well on mouse button pressed + mouse movement. - * Fixes #1777 - Dialog button justification. Adds unit tests. - * Fixes #1739. Setting menu UseKeysUpDownAsKeysLeftRight as false by default. - * Fixes #1772. Avoids WindowsDriver flickering when resizing. - * Fixed TableView always showing selected cell(s) even when not focused - * Fixes #1769. Supports a minimum view size for non-automatic size views. - * Exposes APIs to support upcoming Web console feature - * Fixes some scrolling performance issues - * Fixes #1763. Allowing read console inputs before idle handlers. - * TableView unicode scenario usability - * Added unicode testing code to TableEditor + See: https://github.com/gui-cs/Terminal.Gui/releases \ No newline at end of file diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index cc299e78c..c5995f838 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -248,8 +248,7 @@ namespace Terminal.Gui { /// public override bool MouseEvent (MouseEvent me) { - if (me.Flags == MouseFlags.Button1Clicked || me.Flags == MouseFlags.Button1DoubleClicked || - me.Flags == MouseFlags.Button1TripleClicked) { + if (me.Flags == MouseFlags.Button1Clicked) { if (CanFocus && Enabled) { if (!HasFocus) { SetFocus (); diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index c26a340ed..4fb87d7e5 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -749,7 +749,7 @@ namespace Terminal.Gui { } SetValue (searchset [listview.SelectedItem]); - search.CursorPosition = search.Text.RuneCount; + search.CursorPosition = search.Text.ConsoleWidth; Search_Changed (search.Text); OnOpenSelectedItem (); Reset (keepSearchText: true); @@ -825,7 +825,12 @@ namespace Terminal.Gui { } } - ShowList (); + if (HasFocus) { + ShowList (); + } else if (autoHide) { + isShow = false; + HideList (); + } } /// diff --git a/Terminal.Gui/Core/ContextMenu.cs b/Terminal.Gui/Views/ContextMenu.cs similarity index 61% rename from Terminal.Gui/Core/ContextMenu.cs rename to Terminal.Gui/Views/ContextMenu.cs index 726fc52c6..84e4db4da 100644 --- a/Terminal.Gui/Core/ContextMenu.cs +++ b/Terminal.Gui/Views/ContextMenu.cs @@ -2,8 +2,24 @@ namespace Terminal.Gui { /// - /// A context menu window derived from containing menu items - /// which can be opened in any position. + /// ContextMenu provides a pop-up menu that can be positioned anywhere within a . + /// ContextMenu is analogous to and, once activated, works like a sub-menu + /// of a (but can be positioned anywhere). + /// + /// By default, a ContextMenu with sub-menus is displayed in a cascading manner, where each sub-menu pops out of the ContextMenu frame + /// (either to the right or left, depending on where the ContextMenu is relative to the edge of the screen). By setting + /// to , this behavior can be changed such that all sub-menus are + /// drawn within the ContextMenu frame. + /// + /// + /// ContextMenus can be activated using the Shift-F10 key (by default; use the to change to another key). + /// + /// + /// Callers can cause the ContextMenu to be activated on a right-mouse click (or other interaction) by calling . + /// + /// + /// ContextMenus are located using screen using screen coordinates and appear above all other Views. + /// /// public sealed class ContextMenu : IDisposable { private static MenuBar menuBar; @@ -12,15 +28,15 @@ namespace Terminal.Gui { private Toplevel container; /// - /// Initialize a context menu with empty menu items. + /// Initializes a context menu with no menu items. /// public ContextMenu () : this (0, 0, new MenuBarItem ()) { } /// - /// Initialize a context menu with menu items from a host . + /// Initializes a context menu, with a specifiying the parent/hose of the menu. /// /// The host view. - /// The menu items. + /// The menu items for the context menu. public ContextMenu (View host, MenuBarItem menuItems) : this (host.Frame.X, host.Frame.Y, menuItems) { @@ -28,15 +44,18 @@ namespace Terminal.Gui { } /// - /// Initialize a context menu with menu items. + /// Initializes a context menu with menu items at a specific screen location. /// - /// The left position. - /// The top position. + /// The left position (screen relative). + /// The top position (screen relative). /// The menu items. public ContextMenu (int x, int y, MenuBarItem menuItems) { if (IsShow) { - Hide (); + if (menuBar.SuperView != null) { + Hide (); + } + IsShow = false; } MenuItems = menuItems; Position = new Point (x, y); @@ -48,7 +67,7 @@ namespace Terminal.Gui { } /// - /// Disposes the all the context menu objects instances. + /// Disposes the context menu object. /// public void Dispose () { @@ -65,7 +84,7 @@ namespace Terminal.Gui { } /// - /// Open the menu items. + /// Shows (opens) the ContextMenu, displaying the s it contains. /// public void Show () { @@ -116,7 +135,8 @@ namespace Terminal.Gui { Y = position.Y, Width = 0, Height = 0, - UseSubMenusSingleFrame = UseSubMenusSingleFrame + UseSubMenusSingleFrame = UseSubMenusSingleFrame, + Key = Key }; menuBar.isContextMenuLoading = true; @@ -138,7 +158,7 @@ namespace Terminal.Gui { } /// - /// Close the menu items. + /// Hides (closes) the ContextMenu. /// public void Hide () { @@ -157,7 +177,7 @@ namespace Terminal.Gui { public event Action MouseFlagsChanged; /// - /// Gets or set the menu position. + /// Gets or sets the menu position. /// public Point Position { get; set; } @@ -167,7 +187,7 @@ namespace Terminal.Gui { public MenuBarItem MenuItems { get; set; } /// - /// The used to activate the context menu by keyboard. + /// specifies they keyboard key that will activate the context menu with the keyboard. /// public Key Key { get => key; @@ -179,7 +199,7 @@ namespace Terminal.Gui { } /// - /// The used to activate the context menu by mouse. + /// specifies the mouse action used to activate the context menu by mouse. /// public MouseFlags MouseFlags { get => mouseFlags; @@ -191,7 +211,7 @@ namespace Terminal.Gui { } /// - /// Gets information whether menu is showing or not. + /// Gets whether the ContextMenu is showing or not. /// public static bool IsShow { get; private set; } @@ -202,8 +222,9 @@ namespace Terminal.Gui { public View Host { get; set; } /// - /// Gets or sets whether forces the minimum position to zero - /// if the left or right position are negative. + /// Sets or gets whether the context menu be forced to the right, ensuring it is not clipped, if the x position + /// is less than zero. The default is which means the context menu will be forced to the right. + /// If set to , the context menu will be clipped on the left if x is less than zero. /// public bool ForceMinimumPosToZero { get; set; } = true; @@ -213,7 +234,9 @@ namespace Terminal.Gui { public MenuBar MenuBar { get => menuBar; } /// - /// Gets or sets if the sub-menus must be displayed in a single or multiple frames. + /// Gets or sets if sub-menus will be displayed using a "single frame" menu style. If , the ContextMenu + /// and any sub-menus that would normally cascade will be displayed within a single frame. If (the default), + /// sub-menus will cascade using separate frames for each level of the menu hierarchy. /// public bool UseSubMenusSingleFrame { get; set; } } diff --git a/Terminal.Gui/Views/GraphView.cs b/Terminal.Gui/Views/GraphView.cs index 80cb9702e..48c62a760 100644 --- a/Terminal.Gui/Views/GraphView.cs +++ b/Terminal.Gui/Views/GraphView.cs @@ -240,7 +240,13 @@ namespace Terminal.Gui { ); } - + /// + /// Also ensures that cursor is invisible after entering the . + public override bool OnEnter (View view) + { + Driver.SetCursorVisibility (CursorVisibility.Invisible); + return base.OnEnter (view); + } /// public override bool ProcessKey (KeyEvent keyEvent) diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 1a37fea3a..67d0e88c4 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -1,25 +1,7 @@ -// -// ListView.cs: ListView control -// -// Authors: -// Miguel de Icaza (miguel@gnome.org) -// -// -// TODO: -// - Should we support multiple columns, if so, how should that be done? -// - Show mark for items that have been marked. -// - Mouse support -// - Scrollbars? -// -// Column considerations: -// - Would need a way to specify widths -// - Should it automatically extract data out of structs/classes based on public fields/properties? -// - It seems that this would be useful just for the "simple" API, not the IListDAtaSource, as that one has full support for it. -// - Should a function be specified that retrieves the individual elements? -// using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using NStack; @@ -59,7 +41,7 @@ namespace Terminal.Gui { /// /// Should return whether the specified item is currently marked. /// - /// true, if marked, false otherwise. + /// , if marked, otherwise. /// Item index. bool IsMarked (int item); @@ -67,7 +49,7 @@ namespace Terminal.Gui { /// Flags the item as marked. /// /// Item index. - /// If set to true value. + /// If set to value. void SetMark (int item, bool value); /// @@ -89,8 +71,8 @@ namespace Terminal.Gui { /// /// By default uses to render the items of any /// object (e.g. arrays, , - /// and other collections). Alternatively, an object that implements the - /// interface can be provided giving full control of what is rendered. + /// and other collections). Alternatively, an object that implements + /// can be provided giving full control of what is rendered. /// /// /// can display any object that implements the interface. @@ -107,6 +89,10 @@ namespace Terminal.Gui { /// [x] or [ ] and bind the SPACE key to toggle the selection. To implement a different /// marking style set to false and implement custom rendering. /// + /// + /// Searching the ListView with the keyboard is supported. Users type the + /// first characters of an item, and the first item that starts with what the user types will be selected. + /// /// public class ListView : View { int top, left; @@ -124,6 +110,7 @@ namespace Terminal.Gui { get => source; set { source = value; + KeystrokeNavigator.Collection = source?.ToList ()?.Cast (); top = 0; selected = 0; lastSelectedItem = -1; @@ -169,22 +156,28 @@ namespace Terminal.Gui { /// /// Gets or sets whether this allows items to be marked. /// - /// true if allows marking elements of the list; otherwise, false. - /// + /// Set to to allow marking elements of the list. /// - /// If set to true, will render items marked items with "[x]", and unmarked items with "[ ]" - /// spaces. SPACE key will toggle marking. + /// If set to , will render items marked items with "[x]", and unmarked items with "[ ]" + /// spaces. SPACE key will toggle marking. The default is . /// public bool AllowsMarking { get => allowsMarking; set { allowsMarking = value; + if (allowsMarking) { + AddKeyBinding (Key.Space, Command.ToggleChecked); + } else { + ClearKeybinding (Key.Space); + } + SetNeedsDisplay (); } } /// - /// If set to true allows more than one item to be selected. If false only allow one item selected. + /// If set to more than one item can be selected. If selecting + /// an item will cause all others to be un-selected. The default is . /// public bool AllowsMultipleSelection { get => allowsMultipleSelection; @@ -198,6 +191,7 @@ namespace Terminal.Gui { } } } + SetNeedsDisplay (); } } @@ -219,7 +213,7 @@ namespace Terminal.Gui { } /// - /// Gets or sets the left column where the item start to be displayed at on the . + /// Gets or sets the leftmost column that is currently visible (when scrolling horizontally). /// /// The left position. public int LeftItem { @@ -236,7 +230,7 @@ namespace Terminal.Gui { } /// - /// Gets the widest item. + /// Gets the widest item in the list. /// public int Maxlength => (source?.Length) ?? 0; @@ -264,10 +258,12 @@ namespace Terminal.Gui { } /// - /// Initializes a new instance of that will display the contents of the object implementing the interface, + /// Initializes a new instance of that will display the + /// contents of the object implementing the interface, /// with relative positioning. /// - /// An data source, if the elements are strings or ustrings, the string is rendered, otherwise the ToString() method is invoked on the result. + /// An data source, if the elements are strings or ustrings, + /// the string is rendered, otherwise the ToString() method is invoked on the result. public ListView (IList source) : this (MakeWrapper (source)) { } @@ -296,7 +292,8 @@ namespace Terminal.Gui { /// Initializes a new instance of that will display the contents of the object implementing the interface with an absolute position. /// /// Frame for the listview. - /// An IList data source, if the elements of the IList are strings or ustrings, the string is rendered, otherwise the ToString() method is invoked on the result. + /// An IList data source, if the elements of the IList are strings or ustrings, + /// the string is rendered, otherwise the ToString() method is invoked on the result. public ListView (Rect rect, IList source) : this (rect, MakeWrapper (source)) { Initialize (); @@ -306,7 +303,9 @@ namespace Terminal.Gui { /// Initializes a new instance of with the provided data source and an absolute position /// /// Frame for the listview. - /// IListDataSource object that provides a mechanism to render the data. The number of elements on the collection should not change, if you must change, set the "Source" property to reset the internal settings of the ListView. + /// IListDataSource object that provides a mechanism to render the data. + /// The number of elements on the collection should not change, if you must change, + /// set the "Source" property to reset the internal settings of the ListView. public ListView (Rect rect, IListDataSource source) : base (rect) { this.source = source; @@ -331,13 +330,13 @@ namespace Terminal.Gui { AddCommand (Command.ToggleChecked, () => MarkUnmarkRow ()); // Default keybindings for all ListViews - AddKeyBinding (Key.CursorUp,Command.LineUp); + AddKeyBinding (Key.CursorUp, Command.LineUp); AddKeyBinding (Key.P | Key.CtrlMask, Command.LineUp); AddKeyBinding (Key.CursorDown, Command.LineDown); AddKeyBinding (Key.N | Key.CtrlMask, Command.LineDown); - AddKeyBinding(Key.PageUp,Command.PageUp); + AddKeyBinding (Key.PageUp, Command.PageUp); AddKeyBinding (Key.PageDown, Command.PageDown); AddKeyBinding (Key.V | Key.CtrlMask, Command.PageDown); @@ -347,8 +346,6 @@ namespace Terminal.Gui { AddKeyBinding (Key.End, Command.BottomEnd); AddKeyBinding (Key.Enter, Command.OpenSelectedItem); - - AddKeyBinding (Key.Space, Command.ToggleChecked); } /// @@ -386,7 +383,8 @@ namespace Terminal.Gui { Driver.SetAttribute (current); } if (allowsMarking) { - Driver.AddRune (source.IsMarked (item) ? (AllowsMultipleSelection ? Driver.Checked : Driver.Selected) : (AllowsMultipleSelection ? Driver.UnChecked : Driver.UnSelected)); + Driver.AddRune (source.IsMarked (item) ? (AllowsMultipleSelection ? Driver.Checked : Driver.Selected) : + (AllowsMultipleSelection ? Driver.UnChecked : Driver.UnSelected)); Driver.AddRune (' '); } Source.Render (this, Driver, isSelected, item, col, row, f.Width - col, start); @@ -409,23 +407,43 @@ namespace Terminal.Gui { /// public event Action RowRender; + /// + /// Gets the that searches the collection as + /// the user types. + /// + public CollectionNavigator KeystrokeNavigator { get; private set; } = new CollectionNavigator (); + /// public override bool ProcessKey (KeyEvent kb) { - if (source == null) + if (source == null) { return base.ProcessKey (kb); + } var result = InvokeKeybindings (kb); - if (result != null) + if (result != null) { return (bool)result; + } + + // Enable user to find & select an item by typing text + if (CollectionNavigator.IsCompatibleKey (kb)) { + var newItem = KeystrokeNavigator?.GetNextMatchingItem (SelectedItem, (char)kb.KeyValue); + if (newItem is int && newItem != -1) { + SelectedItem = (int)newItem; + EnsureSelectedItemVisible (); + SetNeedsDisplay (); + return true; + } + } return false; } /// - /// Prevents marking if it's not allowed mark and if it's not allows multiple selection. + /// If and are both , + /// unmarks all marked items other than the currently selected. /// - /// + /// if unmarking was successful. public virtual bool AllowsAll () { if (!allowsMarking) @@ -442,9 +460,9 @@ namespace Terminal.Gui { } /// - /// Marks an unmarked row. + /// Marks the if it is not already marked. /// - /// + /// if the was marked. public virtual bool MarkUnmarkRow () { if (AllowsAll ()) { @@ -457,7 +475,7 @@ namespace Terminal.Gui { } /// - /// Moves the selected item index to the next page. + /// Changes the to the item at the top of the visible list. /// /// public virtual bool MovePageUp () @@ -476,7 +494,8 @@ namespace Terminal.Gui { } /// - /// Moves the selected item index to the previous page. + /// Changes the to the item just below the bottom + /// of the visible list, scrolling if needed. /// /// public virtual bool MovePageDown () @@ -498,7 +517,8 @@ namespace Terminal.Gui { } /// - /// Moves the selected item index to the next row. + /// Changes the to the next item in the list, + /// scrolling the list if needed. /// /// public virtual bool MoveDown () @@ -538,7 +558,8 @@ namespace Terminal.Gui { } /// - /// Moves the selected item index to the previous row. + /// Changes the to the previous item in the list, + /// scrolling the list if needed. /// /// public virtual bool MoveUp () @@ -574,7 +595,8 @@ namespace Terminal.Gui { } /// - /// Moves the selected item index to the last row. + /// Changes the to last item in the list, + /// scrolling the list if needed. /// /// public virtual bool MoveEnd () @@ -592,7 +614,8 @@ namespace Terminal.Gui { } /// - /// Moves the selected item index to the first row. + /// Changes the to the first item in the list, + /// scrolling the list if needed. /// /// public virtual bool MoveHome () @@ -608,23 +631,23 @@ namespace Terminal.Gui { } /// - /// Scrolls the view down. + /// Scrolls the view down by items. /// - /// Number of lines to scroll down. - public virtual bool ScrollDown (int lines) + /// Number of items to scroll down. + public virtual bool ScrollDown (int items) { - top = Math.Max (Math.Min (top + lines, source.Count - 1), 0); + top = Math.Max (Math.Min (top + items, source.Count - 1), 0); SetNeedsDisplay (); return true; } /// - /// Scrolls the view up. + /// Scrolls the view up by items. /// - /// Number of lines to scroll up. - public virtual bool ScrollUp (int lines) + /// Number of items to scroll up. + public virtual bool ScrollUp (int items) { - top = Math.Max (top - lines, 0); + top = Math.Max (top - items, 0); SetNeedsDisplay (); return true; } @@ -655,7 +678,7 @@ namespace Terminal.Gui { private bool allowsMultipleSelection = true; /// - /// Invokes the SelectedChanged event if it is defined. + /// Invokes the event if it is defined. /// /// public virtual bool OnSelectedChanged () @@ -673,7 +696,7 @@ namespace Terminal.Gui { } /// - /// Invokes the OnOpenSelectedItem event if it is defined. + /// Invokes the event if it is defined. /// /// public virtual bool OnOpenSelectedItem () @@ -704,8 +727,7 @@ namespace Terminal.Gui { Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); if (lastSelectedItem == -1) { - EnsuresVisibilitySelectedItem (); - OnSelectedChanged (); + EnsureSelectedItemVisible (); } return base.OnEnter (view); @@ -721,7 +743,10 @@ namespace Terminal.Gui { return base.OnLeave (view); } - void EnsuresVisibilitySelectedItem () + /// + /// Ensures the selected item is always visible on the screen. + /// + public void EnsureSelectedItemVisible () { SuperView?.LayoutSubviews (); if (selected < top) { @@ -788,23 +813,15 @@ namespace Terminal.Gui { return true; } - - } - /// - /// Implements an that renders arbitrary instances for . - /// - /// Implements support for rendering marked items. + /// public class ListWrapper : IListDataSource { IList src; BitArray marks; int count, len; - /// - /// Initializes a new instance of given an - /// - /// + /// public ListWrapper (IList source) { if (source != null) { @@ -815,14 +832,10 @@ namespace Terminal.Gui { } } - /// - /// Gets the number of items in the . - /// + /// public int Count => src != null ? src.Count : 0; - /// - /// Gets the maximum item length in the . - /// + /// public int Length => len; int GetMaxLengthItem () @@ -836,7 +849,7 @@ namespace Terminal.Gui { var t = src [i]; int l; if (t is ustring u) { - l = u.RuneCount; + l = TextFormatter.GetTextWidth (u); } else if (t is string s) { l = s.Length; } else { @@ -853,33 +866,15 @@ namespace Terminal.Gui { void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width, int start = 0) { - int byteLen = ustr.Length; - int used = 0; - for (int i = start; i < byteLen;) { - (var rune, var size) = Utf8.DecodeRune (ustr, i, i - byteLen); - var count = Rune.ColumnWidth (rune); - if (used + count > width) - break; - driver.AddRune (rune); - used += count; - i += size; - } - for (; used < width; used++) { + var u = TextFormatter.ClipAndJustify (ustr, width, TextAlignment.Left); + driver.AddStr (u); + width -= TextFormatter.GetTextWidth (u); + while (width-- > 0) { driver.AddRune (' '); } } - /// - /// Renders a item to the appropriate type. - /// - /// The ListView. - /// The driver used by the caller. - /// Informs if it's marked or not. - /// The item. - /// The col where to move. - /// The line where to move. - /// The item width. - /// The index of the string to be displayed. + /// public void Render (ListView container, ConsoleDriver driver, bool marked, int item, int col, int line, int width, int start = 0) { container.Move (col, line); @@ -897,11 +892,7 @@ namespace Terminal.Gui { } } - /// - /// Returns true if the item is marked, false otherwise. - /// - /// The item. - /// trueIf is marked.falseotherwise. + /// public bool IsMarked (int item) { if (item >= 0 && item < count) @@ -909,25 +900,40 @@ namespace Terminal.Gui { return false; } - /// - /// Sets the item as marked or unmarked based on the value is true or false, respectively. - /// - /// The item - /// Marks the item.Unmarked the item.The value. + /// public void SetMark (int item, bool value) { if (item >= 0 && item < count) marks [item] = value; } - /// - /// Returns the source as IList. - /// - /// + /// public IList ToList () { return src; } + + /// + public int StartsWith (string search) + { + if (src == null || src?.Count == 0) { + return -1; + } + + for (int i = 0; i < src.Count; i++) { + var t = src [i]; + if (t is ustring 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; + } } /// diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs index 804ddb5ed..2244531d1 100644 --- a/Terminal.Gui/Views/Menu.cs +++ b/Terminal.Gui/Views/Menu.cs @@ -1,13 +1,3 @@ -// -// Menu.cs: application menus and submenus -// -// Authors: -// Miguel de Icaza (miguel@gnome.org) -// -// TODO: -// Add accelerator support, but should also support chords (Shortcut in MenuItem) -// Allow menus inside menus - using System; using NStack; using System.Linq; @@ -21,23 +11,24 @@ namespace Terminal.Gui { [Flags] public enum MenuItemCheckStyle { /// - /// The menu item will be shown normally, with no check indicator. + /// 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 . + /// 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. + /// The menu item is part of a menu radio group (see ) and will indicate selected state. /// Radio = 0b_0000_0010, }; /// - /// A has a title, an associated help text, and an action to execute on activation. + /// A has title, an associated help text, and an action to execute on activation. + /// MenuItems can also have a checked indicator (see ). /// public class MenuItem { ustring title; @@ -78,14 +69,28 @@ namespace Terminal.Gui { } /// - /// The HotKey is used when the menu is active, the shortcut can be triggered when the menu is not active. - /// For example HotKey would be "N" when the File Menu is open (assuming there is a "_New" entry - /// if the Shortcut is set to "Control-N", this would be a global hotkey that would trigger as well + /// 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 Rune HotKey; /// - /// This is the global setting that can be used as a global to invoke the action on the menu. + /// Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the that is + /// the parent of the or this . + /// + /// The will be drawn on the MenuItem to the right of the and text. See . + /// /// public Key Shortcut { get => shortcutHelper.Shortcut; @@ -97,12 +102,12 @@ namespace Terminal.Gui { } /// - /// The keystroke combination used in the as string. + /// Gets the text describing the keystroke combination defined by . /// public ustring ShortcutTag => ShortcutHelper.GetShortcutTag (shortcutHelper.Shortcut); /// - /// Gets or sets the title. + /// Gets or sets the title of the menu item . /// /// The title. public ustring Title { @@ -116,34 +121,46 @@ namespace Terminal.Gui { } /// - /// Gets or sets the help text for the menu item. + /// Gets or sets the help text for the menu item. The help text is drawn to the right of the . /// /// The help text. public ustring Help { get; set; } /// - /// Gets or sets the action to be invoked when the menu is triggered + /// Gets or sets the action to be invoked when the menu item is triggered. /// /// Method to invoke. public Action Action { get; set; } /// - /// Gets or sets the action to be invoked if the menu can be triggered + /// 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 action is ready to be executed. + /// Function to determine if the action is can be executed or not. public Func CanExecute { get; set; } /// - /// Shortcut to check if the menu item is enabled + /// Returns if the menu item is enabled. This method is a wrapper around . /// public bool IsEnabled () { return CanExecute == null ? true : CanExecute (); } - internal int Width => 1 + TitleLength + (Help.ConsoleWidth > 0 ? Help.ConsoleWidth + 2 : 0) + - (Checked || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio) ? 2 : 0) + - (ShortcutTag.ConsoleWidth > 0 ? ShortcutTag.ConsoleWidth + 2 : 0) + 2; + // + // ┌─────────────────────────────┐ + // │ 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 || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio) ? 2 : 0) + // check glyph + space + (Help.ConsoleWidth > 0 ? 2 + Help.ConsoleWidth : 0) + // Two spaces before Help + (ShortcutTag.ConsoleWidth > 0 ? 2 + ShortcutTag.ConsoleWidth : 0); // Pad two spaces before shortcut tag (which are also aligned right) /// /// Sets or gets whether the shows a check indicator or not. See . @@ -151,12 +168,12 @@ namespace Terminal.Gui { public bool Checked { set; get; } /// - /// Sets or gets the type selection indicator the menu item will be displayed with. + /// Sets or gets the of a menu item where is set to . /// public MenuItemCheckStyle CheckType { get; set; } /// - /// Gets or sets the parent for this . + /// Gets the parent for this . /// /// The parent. public MenuItem Parent { get; internal set; } @@ -167,7 +184,7 @@ namespace Terminal.Gui { internal bool IsFromSubMenu { get { return Parent != null; } } /// - /// Merely a debugging aid to see the interaction with main + /// Merely a debugging aid to see the interaction with main. /// public MenuItem GetMenuItem () { @@ -175,7 +192,7 @@ namespace Terminal.Gui { } /// - /// Merely a debugging aid to see the interaction with main + /// Merely a debugging aid to see the interaction with main. /// public bool GetMenuBarItem () { @@ -213,14 +230,15 @@ namespace Terminal.Gui { } /// - /// A contains s or s. + /// is a menu item on an app's . + /// MenuBarItems do not support . /// public class MenuBarItem : MenuItem { /// /// Initializes a new as a . /// /// Title for the menu item. - /// Help text to display. + /// 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 exist, otherwise is null. @@ -289,19 +307,6 @@ namespace Terminal.Gui { } } - //static int GetMaxTitleLength (MenuItem [] children) - //{ - // int maxLength = 0; - // foreach (var item in children) { - // int len = GetMenuBarItemLength (item.Title); - // if (len > maxLength) - // maxLength = len; - // item.IsFromSubMenu = true; - // } - - // return maxLength; - //} - void SetChildrensParent (MenuItem [] childrens) { foreach (var child in childrens) { @@ -363,12 +368,6 @@ namespace Terminal.Gui { Title = title; } - ///// - ///// Gets or sets the title to display. - ///// - ///// The title. - //public ustring Title { get; set; } - /// /// Gets or sets an array of objects that are the children of this /// @@ -391,8 +390,8 @@ namespace Terminal.Gui { } int minX = x; int minY = y; - int maxW = (items.Max (z => z?.Width) ?? 0) + 2; - int maxH = items.Length + 2; + int maxW = (items.Max (z => z?.Width) ?? 0) + 2; // This 2 is frame border? + int maxH = items.Length + 2; // This 2 is frame border? if (parent != null && x + maxW > Driver.Cols) { minX = Math.Max (parent.Frame.Right - parent.Frame.Width - maxW, 0); } @@ -459,6 +458,7 @@ namespace Terminal.Gui { return GetNormalColor (); } + // Draws the Menu, within the Frame public override void Redraw (Rect bounds) { Driver.SetAttribute (GetNormalColor ()); @@ -477,13 +477,14 @@ namespace Terminal.Gui { Move (1, i + 1); Driver.SetAttribute (DetermineColorSchemeFor (item, i)); - for (int p = Bounds.X; p < Frame.Width - 2; p++) { + for (int p = Bounds.X; p < Frame.Width - 2; p++) { // This - 2 is for the border if (p < 0) continue; if (item == null) Driver.AddRune (Driver.HLine); else if (i == 0 && p == 0 && host.UseSubMenusSingleFrame && item.Parent.Parent != null) Driver.AddRune (Driver.LeftArrow); + // This `- 3` is left border + right border + one row in from right else if (p == Frame.Width - 3 && barItems.SubMenu (barItems.Children [i]) != null) Driver.AddRune (Driver.RightArrow); else @@ -527,6 +528,7 @@ namespace Terminal.Gui { HotKeySpecifier = MenuBar.HotKeySpecifier, Text = textToDraw }; + // The -3 is left/right border + one space (not sure what for) tf.Draw (ViewToScreen (new Rect (2, i + 1, Frame.Width - 3, 1)), i == current ? ColorScheme.Focus : GetNormalColor (), i == current ? ColorScheme.HotFocus : ColorScheme.HotNormal, @@ -630,7 +632,7 @@ namespace Terminal.Gui { } } } - return false; + return host.ProcessHotKey (kb); } void RunSelected () @@ -688,6 +690,7 @@ namespace Terminal.Gui { } } while (barItems.Children [current] == null || disabled); SetNeedsDisplay (); + SetParentSetNeedsDisplay (); if (!host.UseSubMenusSingleFrame) host.OnMenuOpened (); return true; @@ -735,11 +738,24 @@ namespace Terminal.Gui { } } while (barItems.Children [current] == null || disabled); SetNeedsDisplay (); + SetParentSetNeedsDisplay (); if (!host.UseSubMenusSingleFrame) host.OnMenuOpened (); return true; } + private void SetParentSetNeedsDisplay () + { + if (host.openSubMenu != null) { + foreach (var menu in host.openSubMenu) { + menu.SetNeedsDisplay (); + } + } + + host?.openMenu.SetNeedsDisplay (); + host.SetNeedsDisplay (); + } + public override bool MouseEvent (MouseEvent me) { if (!host.handled && !host.HandleGrabView (me, this)) { @@ -756,6 +772,7 @@ namespace Terminal.Gui { return true; var item = barItems.Children [meY]; if (item == null || !item.IsEnabled ()) disabled = true; + if (disabled) return true; current = meY; if (item != null && !disabled) RunSelected (); @@ -775,6 +792,7 @@ namespace Terminal.Gui { current = me.Y - 1; if (host.UseSubMenusSingleFrame || !CheckSubMenu ()) { SetNeedsDisplay (); + SetParentSetNeedsDisplay (); return true; } host.OnMenuOpened (); @@ -803,6 +821,7 @@ namespace Terminal.Gui { return host.CloseMenu (false, true); } else { SetNeedsDisplay (); + SetParentSetNeedsDisplay (); } return true; } @@ -832,17 +851,27 @@ namespace Terminal.Gui { } } - - /// - /// Provides a menu bar with drop-down and cascading menus. + /// + /// 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 terminal. + /// The appears on the first row of the parent View and uses the full width. /// /// - /// The provides global hotkeys for the application. + /// The provides global hotkeys for the application. See . + /// + /// + /// See also: /// /// public class MenuBar : View { @@ -850,7 +879,7 @@ namespace Terminal.Gui { internal int selectedSub; /// - /// Gets or sets the array of s for the menu. Only set this when the is visible. + /// Gets or sets the array of s for the menu. Only set this after the is visible. /// /// The menu array. public MenuBarItem [] Menus { get; set; } @@ -873,7 +902,7 @@ namespace Terminal.Gui { static ustring shortcutDelimiter = "+"; /// - /// Used for change the shortcut delimiter separator. + /// Sets or gets the shortcut delimiter separator. The default is "+". /// public static ustring ShortcutDelimiter { get => shortcutDelimiter; @@ -893,6 +922,13 @@ namespace Terminal.Gui { /// /// 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; @@ -905,6 +941,11 @@ namespace Terminal.Gui { } } + /// + /// The used to activate the menu bar by keyboard. + /// + public Key Key { get; set; } = Key.F9; + /// /// Initializes a new instance of the . /// @@ -1024,6 +1065,14 @@ namespace Terminal.Gui { isCleaning = false; } + // The column where the MenuBar starts + static int xOrigin = 0; + // Spaces before the Title + static int leftPadding = 1; + // Spaces after the Title + static int rightPadding = 1; + // Spaces after the submenu Title, before Help + static int parensAroundHelp = 3; /// public override void Redraw (Rect bounds) { @@ -1033,7 +1082,7 @@ namespace Terminal.Gui { Driver.AddRune (' '); Move (1, 0); - int pos = 1; + int pos = 0; for (int i = 0; i < Menus.Length; i++) { var menu = Menus [i]; @@ -1041,17 +1090,14 @@ namespace Terminal.Gui { Attribute hotColor, normalColor; if (i == selected && IsMenuOpen) { hotColor = i == selected ? ColorScheme.HotFocus : ColorScheme.HotNormal; - normalColor = i == selected ? ColorScheme.Focus : - GetNormalColor (); - } else if (openedByAltKey) { + normalColor = i == selected ? ColorScheme.Focus : GetNormalColor (); + } else { hotColor = ColorScheme.HotNormal; normalColor = GetNormalColor (); - } else { - hotColor = GetNormalColor (); - normalColor = GetNormalColor (); } - DrawHotString (menu.Help.IsEmpty ? $" {menu.Title} " : $" {menu.Title} {menu.Help} ", hotColor, normalColor); - pos += 1 + menu.TitleLength + (menu.Help.ConsoleWidth > 0 ? menu.Help.ConsoleWidth + 2 : 0) + 2; + // Note Help on MenuBar is drawn with parens around it + DrawHotString (menu.Help.IsEmpty ? $" {menu.Title} " : $" {menu.Title} ({menu.Help}) ", hotColor, normalColor); + pos += leftPadding + menu.TitleLength + (menu.Help.ConsoleWidth > 0 ? leftPadding + menu.Help.ConsoleWidth + parensAroundHelp : 0) + rightPadding; } PositionCursor (); } @@ -1066,14 +1112,10 @@ namespace Terminal.Gui { for (int i = 0; i < Menus.Length; i++) { if (i == selected) { pos++; - if (IsMenuOpen) - Move (pos + 1, 0); - else { - Move (pos + 1, 0); - } + Move (pos + 1, 0); return; } else { - pos += 1 + Menus [i].TitleLength + (Menus [i].Help.ConsoleWidth > 0 ? Menus [i].Help.ConsoleWidth + 2 : 0) + 2; + pos += leftPadding + Menus [i].TitleLength + (Menus [i].Help.ConsoleWidth > 0 ? Menus [i].Help.ConsoleWidth + parensAroundHelp : 0) + rightPadding; } } } @@ -1111,7 +1153,7 @@ namespace Terminal.Gui { public event Action MenuClosing; /// - /// Raised when all the menu are closed. + /// Raised when all the menu is closed. /// public event Action MenuAllClosed; @@ -1134,7 +1176,7 @@ namespace Terminal.Gui { internal bool isMenuClosing; /// - /// True if the menu is open; otherwise false. + /// if the menu is open; otherwise . /// public bool IsMenuOpen { get; protected set; } @@ -1156,7 +1198,9 @@ namespace Terminal.Gui { public virtual void OnMenuOpened () { MenuItem mi = null; - if (openCurrentMenu.barItems.Children != null && openCurrentMenu?.current > -1) { + if (openCurrentMenu.barItems.Children != null && openCurrentMenu.barItems.Children.Length > 0 + && openCurrentMenu?.current > -1) { + mi = openCurrentMenu.barItems.Children [openCurrentMenu.current]; } else if (openCurrentMenu.barItems.IsTopLevel) { mi = openCurrentMenu.barItems; @@ -1167,7 +1211,7 @@ namespace Terminal.Gui { } /// - /// Virtual method that will invoke the + /// Virtual method that will invoke the . /// /// The current menu to be closed. /// Whether the current menu will be reopen. @@ -1180,7 +1224,7 @@ namespace Terminal.Gui { } /// - /// Virtual method that will invoke the + /// Virtual method that will invoke the . /// public virtual void OnMenuAllClosed () { @@ -1190,7 +1234,7 @@ namespace Terminal.Gui { View lastFocused; /// - /// Get the lasted focused view before open the menu. + /// Gets the view that was last focused before opening the menu. /// public View LastFocused { get; private set; } @@ -1208,6 +1252,7 @@ namespace Terminal.Gui { int pos = 0; switch (subMenu) { case null: + // Open a submenu below a MenuBar lastFocused = lastFocused ?? (SuperView == null ? Application.Current.MostFocused : SuperView.MostFocused); if (openSubMenu != null && !CloseMenu (false, true)) return; @@ -1220,8 +1265,10 @@ namespace Terminal.Gui { openMenu.Dispose (); } + // This positions the submenu horizontally aligned with the first character of the + // menu it belongs to's text for (int i = 0; i < index; i++) - pos += 1 + Menus [i].TitleLength + (Menus [i].Help.ConsoleWidth > 0 ? Menus [i].Help.ConsoleWidth + 2 : 0) + 2; + pos += Menus [i].TitleLength + (Menus [i].Help.ConsoleWidth > 0 ? Menus [i].Help.ConsoleWidth + 2 : 0) + leftPadding + rightPadding; openMenu = new Menu (this, Frame.X + pos, Frame.Y + 1, Menus [index]); openCurrentMenu = openMenu; openCurrentMenu.previousSubFocused = openMenu; @@ -1234,6 +1281,7 @@ namespace Terminal.Gui { openMenu.SetFocus (); break; default: + // Opens a submenu next to another submenu (openSubMenu) if (openSubMenu == null) openSubMenu = new List (); if (sIndex > -1) { @@ -1274,7 +1322,7 @@ namespace Terminal.Gui { } /// - /// Opens the current Menu programatically. + /// Opens the Menu programatically, as though the F9 key were pressed. /// public void OpenMenu () { @@ -1356,7 +1404,7 @@ namespace Terminal.Gui { } /// - /// Closes the current Menu programatically, if open and not canceled. + /// Closes the Menu programmatically if open and not canceled (as though F9 were pressed). /// public bool CloseMenu (bool ignoreUseSubMenusSingleFrame = false) { @@ -1458,26 +1506,6 @@ namespace Terminal.Gui { if (openSubMenu.Count > 0) openCurrentMenu = openSubMenu.Last (); - //if (openMenu.Subviews.Count == 0) - // return; - //if (index == 0) { - // //SuperView.SetFocus (previousSubFocused); - // FocusPrev (); - // return; - //} - - //for (int i = openMenu.Subviews.Count - 1; i > index; i--) { - // isMenuClosing = true; - // if (openMenu.Subviews.Count - 1 > 0) - // SuperView.SetFocus (openMenu.Subviews [i - 1]); - // else - // SuperView.SetFocus (openMenu); - // if (openMenu != null) { - // Remove (openMenu.Subviews [i]); - // openMenu.Remove (openMenu.Subviews [i]); - // } - // RemoveSubMenu (i); - //} isMenuClosing = false; } @@ -1577,7 +1605,7 @@ namespace Terminal.Gui { var subMenu = openCurrentMenu.current > -1 && openCurrentMenu.barItems.Children.Length > 0 ? openCurrentMenu.barItems.SubMenu (openCurrentMenu.barItems.Children [openCurrentMenu.current]) : null; - if ((selectedSub == -1 || openSubMenu == null || openSubMenu?.Count == selectedSub) && subMenu == null) { + if ((selectedSub == -1 || openSubMenu == null || openSubMenu?.Count - 1 == selectedSub) && subMenu == null) { if (openSubMenu != null && !CloseMenu (false, true)) return; NextMenu (false, ignoreUseSubMenusSingleFrame); @@ -1678,7 +1706,7 @@ namespace Terminal.Gui { /// public override bool ProcessHotKey (KeyEvent kb) { - if (kb.Key == Key.F9) { + if (kb.Key == Key) { if (!IsMenuOpen) OpenMenu (); else @@ -1773,10 +1801,10 @@ namespace Terminal.Gui { if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked || me.Flags == MouseFlags.Button1Clicked || (me.Flags == MouseFlags.ReportMousePosition && selected > -1) || (me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && selected > -1)) { - int pos = 1; + int pos = xOrigin; int cx = me.X; for (int i = 0; i < Menus.Length; i++) { - if (cx >= pos && cx < pos + 1 + Menus [i].TitleLength + Menus [i].Help.ConsoleWidth + 2) { + if (cx >= pos && cx < pos + leftPadding + Menus [i].TitleLength + Menus [i].Help.ConsoleWidth + rightPadding) { if (me.Flags == MouseFlags.Button1Clicked) { if (Menus [i].IsTopLevel) { var menu = new Menu (this, i, 0, Menus [i]); @@ -1805,7 +1833,7 @@ namespace Terminal.Gui { } return true; } - pos += 1 + Menus [i].TitleLength + 2; + pos += leftPadding + Menus [i].TitleLength + rightPadding; } } return false; @@ -1878,47 +1906,6 @@ namespace Terminal.Gui { handled = false; return false; } - //if (me.View != this && me.Flags != MouseFlags.Button1Pressed) - // return true; - //else if (me.View != this && me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) { - // Application.UngrabMouse (); - // host.CloseAllMenus (); - // return true; - //} - - - //if (!(me.View is MenuBar) && !(me.View is Menu) && me.Flags != MouseFlags.Button1Pressed)) - // return false; - - //if (Application.MouseGrabView != null) { - // if (me.View is MenuBar || me.View is Menu) { - // me.X -= me.OfX; - // me.Y -= me.OfY; - // me.View.MouseEvent (me); - // return true; - // } else if (!(me.View is MenuBar || me.View is Menu) && me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) { - // Application.UngrabMouse (); - // CloseAllMenus (); - // } - //} else if (!isMenuClosed && selected == -1 && me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) { - // Application.GrabMouse (this); - // return true; - //} - - //if (Application.MouseGrabView != null) { - // if (Application.MouseGrabView == me.View && me.View == current) { - // me.X -= me.OfX; - // me.Y -= me.OfY; - // } else if (me.View != current && me.View is MenuBar && me.View is Menu) { - // Application.UngrabMouse (); - // Application.GrabMouse (me.View); - // } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) { - // Application.UngrabMouse (); - // CloseMenu (); - // } - //} else if ((!isMenuClosed && selected > -1)) { - // Application.GrabMouse (current); - //} handled = true; @@ -1972,12 +1959,13 @@ namespace Terminal.Gui { /// public MenuBarItem NewMenuBarItem { get; set; } /// - /// Flag that allows you to cancel the opening of the menu. + /// 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; } /// - /// Initializes a new instance of + /// Initializes a new instance of . /// /// The current parent. public MenuOpeningEventArgs (MenuBarItem currentMenu) @@ -1996,7 +1984,7 @@ namespace Terminal.Gui { public MenuBarItem CurrentMenu { get; } /// - /// Indicates whether the current menu will be reopen. + /// Indicates whether the current menu will reopen. /// public bool Reopen { get; } @@ -2006,15 +1994,16 @@ namespace Terminal.Gui { public bool IsSubMenu { get; } /// - /// Flag that allows you to cancel the opening of the menu. + /// 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; } /// - /// Initializes a new instance of + /// Initializes a new instance of . /// /// The current parent. - /// Whether the current menu will be reopen. + /// Whether the current menu will reopen. /// Indicates whether it is a sub-menu. public MenuClosingEventArgs (MenuBarItem currentMenu, bool reopen, bool isSubMenu) { diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index b97d87b60..aaa510abf 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -67,6 +67,7 @@ namespace Terminal.Gui { Frame = rect; } CanFocus = true; + HotKeySpecifier = new Rune ('_'); // Things this view knows how to do AddCommand (Command.LineUp, () => { MoveUp (); return true; }); @@ -215,9 +216,36 @@ namespace Terminal.Gui { Move (horizontal [i].pos, 0); break; } + var rl = radioLabels [i]; Driver.SetAttribute (GetNormalColor ()); Driver.AddStr (ustring.Make (new Rune [] { i == selected ? Driver.Selected : Driver.UnSelected, ' ' })); - DrawHotString (radioLabels [i], HasFocus && i == cursor, ColorScheme); + TextFormatter.FindHotKey (rl, HotKeySpecifier, true, out int hotPos, out Key hotKey); + if (hotPos != -1 && (hotKey != Key.Null || hotKey != Key.Unknown)) { + var rlRunes = rl.ToRunes (); + for (int j = 0; j < rlRunes.Length; j++) { + Rune rune = rlRunes [j]; + if (j == hotPos && i == cursor) { + Application.Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : GetHotNormalColor ()); + } else if (j == hotPos && i != cursor) { + Application.Driver.SetAttribute (GetHotNormalColor ()); + } else if (HasFocus && i == cursor) { + Application.Driver.SetAttribute (ColorScheme.Focus); + } + if (rune == HotKeySpecifier && j + 1 < rlRunes.Length) { + j++; + rune = rlRunes [j]; + if (i == cursor) { + Application.Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : GetHotNormalColor ()); + } else if (i != cursor) { + Application.Driver.SetAttribute (GetHotNormalColor ()); + } + } + Application.Driver.AddRune (rune); + Driver.SetAttribute (GetNormalColor ()); + } + } else { + DrawHotString (rl, HasFocus && i == cursor, ColorScheme); + } } } @@ -280,11 +308,12 @@ namespace Terminal.Gui { key = Char.ToUpper ((char)key); foreach (var l in radioLabels) { bool nextIsHot = false; - foreach (var c in l) { - if (c == '_') + TextFormatter.FindHotKey (l, HotKeySpecifier, true, out _, out Key hotKey); + foreach (Rune c in l) { + if (c == HotKeySpecifier) { nextIsHot = true; - else { - if (nextIsHot && c == key) { + } else { + if ((nextIsHot && Rune.ToUpper (c) == key) || (key == (uint)hotKey)) { SelectedItem = i; cursor = i; if (!HasFocus) diff --git a/Terminal.Gui/Views/ScrollBarView.cs b/Terminal.Gui/Views/ScrollBarView.cs index 80c5b6ee3..ad0f469f5 100644 --- a/Terminal.Gui/Views/ScrollBarView.cs +++ b/Terminal.Gui/Views/ScrollBarView.cs @@ -462,7 +462,7 @@ namespace Terminal.Gui { return; } - Driver.SetAttribute (GetNormalColor ()); + Driver.SetAttribute (Host.HasFocus ? ColorScheme.Focus : GetNormalColor ()); if ((vertical && Bounds.Height == 0) || (!vertical && Bounds.Width == 0)) { return; @@ -613,13 +613,13 @@ namespace Terminal.Gui { int posBarOffset; /// - public override bool MouseEvent (MouseEvent me) + public override bool MouseEvent (MouseEvent mouseEvent) { - if (me.Flags != MouseFlags.Button1Pressed && me.Flags != MouseFlags.Button1DoubleClicked && - !me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && - me.Flags != MouseFlags.Button1Released && me.Flags != MouseFlags.WheeledDown && - me.Flags != MouseFlags.WheeledUp && me.Flags != MouseFlags.WheeledRight && - me.Flags != MouseFlags.WheeledLeft && me.Flags != MouseFlags.Button1TripleClicked) { + if (mouseEvent.Flags != MouseFlags.Button1Pressed && mouseEvent.Flags != MouseFlags.Button1DoubleClicked && + !mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && + mouseEvent.Flags != MouseFlags.Button1Released && mouseEvent.Flags != MouseFlags.WheeledDown && + mouseEvent.Flags != MouseFlags.WheeledUp && mouseEvent.Flags != MouseFlags.WheeledRight && + mouseEvent.Flags != MouseFlags.WheeledLeft && mouseEvent.Flags != MouseFlags.Button1TripleClicked) { return false; } @@ -630,24 +630,24 @@ namespace Terminal.Gui { Host.SetFocus (); } - int location = vertical ? me.Y : me.X; + int location = vertical ? mouseEvent.Y : mouseEvent.X; int barsize = vertical ? Bounds.Height : Bounds.Width; int posTopLeftTee = vertical ? posTopTee + 1 : posLeftTee + 1; int posBottomRightTee = vertical ? posBottomTee + 1 : posRightTee + 1; barsize -= 2; var pos = Position; - if (me.Flags != MouseFlags.Button1Released + if (mouseEvent.Flags != MouseFlags.Button1Released && (Application.MouseGrabView == null || Application.MouseGrabView != this)) { Application.GrabMouse (this); - } else if (me.Flags == MouseFlags.Button1Released && Application.MouseGrabView != null && Application.MouseGrabView == this) { + } else if (mouseEvent.Flags == MouseFlags.Button1Released && Application.MouseGrabView != null && Application.MouseGrabView == this) { lastLocation = -1; Application.UngrabMouse (); return true; } - if (showScrollIndicator && (me.Flags == MouseFlags.WheeledDown || me.Flags == MouseFlags.WheeledUp || - me.Flags == MouseFlags.WheeledRight || me.Flags == MouseFlags.WheeledLeft)) { - return Host.MouseEvent (me); + if (showScrollIndicator && (mouseEvent.Flags == MouseFlags.WheeledDown || mouseEvent.Flags == MouseFlags.WheeledUp || + mouseEvent.Flags == MouseFlags.WheeledRight || mouseEvent.Flags == MouseFlags.WheeledLeft)) { + return Host.MouseEvent (mouseEvent); } if (location == 0) { @@ -668,7 +668,7 @@ namespace Terminal.Gui { //} if (lastLocation > -1 || (location >= posTopLeftTee && location <= posBottomRightTee - && me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) { + && mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) { if (lastLocation == -1) { lastLocation = location; posBarOffset = keepContentAlwaysInViewport ? Math.Max (location - posTopLeftTee, 1) : 0; diff --git a/Terminal.Gui/Views/ScrollView.cs b/Terminal.Gui/Views/ScrollView.cs index 820275178..23bfedbce 100644 --- a/Terminal.Gui/Views/ScrollView.cs +++ b/Terminal.Gui/Views/ScrollView.cs @@ -13,7 +13,6 @@ using System; using System.Linq; -using System.Reflection; namespace Terminal.Gui { /// @@ -30,7 +29,14 @@ namespace Terminal.Gui { /// /// public class ScrollView : View { - View contentView = null; + private class ContentView : View { + public ContentView (Rect frame) : base (frame) + { + CanFocus = true; + } + } + + ContentView contentView; ScrollBarView vertical, horizontal; /// @@ -53,7 +59,7 @@ namespace Terminal.Gui { void Initialize (Rect frame) { - contentView = new View (frame); + contentView = new ContentView (frame); vertical = new ScrollBarView (1, 0, isVertical: true) { X = Pos.AnchorEnd (1), Y = 0, @@ -178,6 +184,12 @@ namespace Terminal.Gui { set { if (autoHideScrollBars != value) { autoHideScrollBars = value; + if (Subviews.Contains (vertical)) { + vertical.AutoHideScrollBars = value; + } + if (Subviews.Contains (horizontal)) { + horizontal.AutoHideScrollBars = value; + } SetNeedsDisplay (); } } @@ -217,7 +229,7 @@ namespace Terminal.Gui { /// The view to add to the scrollview. public override void Add (View view) { - if (!IsOverridden (view)) { + if (!IsOverridden (view, "MouseEvent")) { view.MouseEnter += View_MouseEnter; view.MouseLeave += View_MouseLeave; } @@ -237,14 +249,6 @@ namespace Terminal.Gui { Application.GrabMouse (this); } - bool IsOverridden (View view) - { - Type t = view.GetType (); - MethodInfo m = t.GetMethod ("MouseEvent"); - - return (m.DeclaringType == t || m.ReflectedType == t) && m.GetBaseDefinition ().DeclaringType == typeof (Responder); - } - /// /// Gets or sets the visibility for the horizontal scroll indicator. /// @@ -260,6 +264,8 @@ namespace Terminal.Gui { SetNeedsLayout (); if (value) { base.Add (horizontal); + horizontal.ShowScrollIndicator = value; + horizontal.AutoHideScrollBars = autoHideScrollBars; horizontal.OtherScrollBarView = vertical; horizontal.OtherScrollBarView.ShowScrollIndicator = value; horizontal.MouseEnter += View_MouseEnter; @@ -299,6 +305,8 @@ namespace Terminal.Gui { SetNeedsLayout (); if (value) { base.Add (vertical); + vertical.ShowScrollIndicator = value; + vertical.AutoHideScrollBars = autoHideScrollBars; vertical.OtherScrollBarView = horizontal; vertical.OtherScrollBarView.ShowScrollIndicator = value; vertical.MouseEnter += View_MouseEnter; @@ -318,7 +326,7 @@ namespace Terminal.Gui { { Driver.SetAttribute (GetNormalColor ()); SetViewsNeedsDisplay (); - Clear (); + //Clear (); var savedClip = ClipToBounds (); OnDrawContent (new Rect (ContentOffset, @@ -331,10 +339,12 @@ namespace Terminal.Gui { ShowHideScrollBars (); } else { if (ShowVerticalScrollIndicator) { + vertical.SetRelativeLayout (Bounds); vertical.Redraw (vertical.Bounds); } if (ShowHorizontalScrollIndicator) { + horizontal.SetRelativeLayout (Bounds); horizontal.Redraw (horizontal.Bounds); } } @@ -498,7 +508,7 @@ namespace Terminal.Gui { { if (me.Flags != MouseFlags.WheeledDown && me.Flags != MouseFlags.WheeledUp && me.Flags != MouseFlags.WheeledRight && me.Flags != MouseFlags.WheeledLeft && - me.Flags != MouseFlags.Button1Pressed && me.Flags != MouseFlags.Button1Clicked && +// me.Flags != MouseFlags.Button1Pressed && me.Flags != MouseFlags.Button1Clicked && !me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) { return false; } @@ -515,7 +525,7 @@ namespace Terminal.Gui { vertical.MouseEvent (me); } else if (me.Y == horizontal.Frame.Y && ShowHorizontalScrollIndicator) { horizontal.MouseEvent (me); - } else if (IsOverridden (me.View)) { + } else if (IsOverridden (me.View, "MouseEvent")) { Application.UngrabMouse (); } return true; diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs index f8db8920c..5d3ec65f1 100644 --- a/Terminal.Gui/Views/StatusBar.cs +++ b/Terminal.Gui/Views/StatusBar.cs @@ -55,6 +55,12 @@ namespace Terminal.Gui { /// /// Action to invoke. public Action Action { get; } + + /// + /// Gets or sets arbitrary data for the status item. + /// + /// This property is not used internally. + public object Data { get; set; } }; /// @@ -64,8 +70,6 @@ namespace Terminal.Gui { /// So for each context must be a new instance of a statusbar. /// public class StatusBar : View { - bool disposedValue; - /// /// The items that compose the /// @@ -87,39 +91,9 @@ namespace Terminal.Gui { CanFocus = false; ColorScheme = Colors.Menu; X = 0; + Y = Pos.AnchorEnd (1); Width = Dim.Fill (); Height = 1; - - Initialized += StatusBar_Initialized; - Application.Resized += Application_Resized (); - } - - private void StatusBar_Initialized (object sender, EventArgs e) - { - if (SuperView.Frame == Rect.Empty) { - ((Toplevel)SuperView).Loaded += StatusBar_Loaded; - } else { - Y = Math.Max (SuperView.Frame.Height - (Visible ? 1 : 0), 0); - } - } - - private void StatusBar_Loaded () - { - Y = Math.Max (SuperView.Frame.Height - (Visible ? 1 : 0), 0); - ((Toplevel)SuperView).Loaded -= StatusBar_Loaded; - } - - private Action Application_Resized () - { - return delegate { - X = 0; - Height = 1; - if (SuperView != null || SuperView is Toplevel) { - if (Frame.Y != SuperView.Frame.Height - (Visible ? 1 : 0)) { - Y = SuperView.Frame.Height - (Visible ? 1 : 0); - } - } - }; } static ustring shortcutDelimiter = "-"; @@ -145,12 +119,6 @@ namespace Terminal.Gui { /// public override void Redraw (Rect bounds) { - //if (Frame.Y != Driver.Rows - 1) { - // Frame = new Rect (Frame.X, Driver.Rows - 1, Frame.Width, Frame.Height); - // Y = Driver.Rows - 1; - // SetNeedsDisplay (); - //} - Move (0, 0); Driver.SetAttribute (GetNormalColor ()); for (int i = 0; i < Frame.Width; i++) @@ -228,17 +196,6 @@ namespace Terminal.Gui { }); } - /// - protected override void Dispose (bool disposing) - { - if (!disposedValue) { - if (disposing) { - Application.Resized -= Application_Resized (); - } - disposedValue = true; - } - } - /// public override bool OnEnter (View view) { diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs index 2ea48c006..8d60a3b0d 100644 --- a/Terminal.Gui/Views/TabView.cs +++ b/Terminal.Gui/Views/TabView.cs @@ -53,6 +53,14 @@ namespace Terminal.Gui { /// public event EventHandler SelectedTabChanged; + + /// + /// Event fired when a is clicked. Can be used to cancel navigation, + /// show context menu (e.g. on right click) etc. + /// + public event EventHandler TabClicked; + + /// /// The currently selected member of chosen by the user /// @@ -98,7 +106,7 @@ namespace Terminal.Gui { /// - /// Initialzies a class using layout. + /// Initializes a class using layout. /// public TabView () : base () { @@ -182,7 +190,7 @@ namespace Terminal.Gui { if (Style.ShowBorder) { - // How muc space do we need to leave at the bottom to show the tabs + // How much space do we need to leave at the bottom to show the tabs int spaceAtBottom = Math.Max (0, GetTabHeight (false) - 1); int startAtY = Math.Max (0, GetTabHeight (true) - 1); @@ -347,8 +355,10 @@ namespace Terminal.Gui { var maxWidth = Math.Max (0, Math.Min (bounds.Width - 3, MaxTabTextWidth)); // if tab view is width <= 3 don't render any tabs - if (maxWidth == 0) - yield break; + if (maxWidth == 0) { + yield return new TabToRender (i, tab, string.Empty, Equals (SelectedTab, tab), 0); + break; + } if (tabTextWidth > maxWidth) { text = tab.Text.ToString ().Substring (0, (int)maxWidth); @@ -412,7 +422,7 @@ namespace Terminal.Gui { // if the currently selected tab is no longer a member of Tabs if (SelectedTab == null || !Tabs.Contains (SelectedTab)) { - // select the tab closest to the one that disapeared + // select the tab closest to the one that disappeared var toSelect = Math.Max (idx - 1, 0); if (toSelect < Tabs.Count) { @@ -464,31 +474,10 @@ namespace Terminal.Gui { Width = Dim.Fill (); } - /// - /// Positions the cursor at the start of the currently selected tab - /// - public override void PositionCursor () + public override bool OnEnter (View view) { - base.PositionCursor (); - - var selected = host.CalculateViewport (Bounds).FirstOrDefault (t => Equals (host.SelectedTab, t.Tab)); - - if (selected == null) { - return; - } - - int y; - - if (host.Style.TabsOnBottom) { - y = 1; - } else { - y = host.Style.ShowTopLine ? 1 : 0; - } - - Move (selected.X, y); - - - + Driver.SetCursorVisibility (CursorVisibility.Invisible); + return base.OnEnter (view); } public override void Redraw (Rect bounds) @@ -657,7 +646,7 @@ namespace Terminal.Gui { Driver.AddRune (Driver.LeftArrow); } - // if there are mmore tabs to the right not visible + // if there are more tabs to the right not visible if (ShouldDrawRightScrollIndicator (tabLocations)) { Move (width - 1, y); @@ -684,6 +673,22 @@ namespace Terminal.Gui { public override bool MouseEvent (MouseEvent me) { + var hit = ScreenToTab (me.X, me.Y); + + bool isClick = me.Flags.HasFlag (MouseFlags.Button1Clicked) || + me.Flags.HasFlag (MouseFlags.Button2Clicked) || + me.Flags.HasFlag (MouseFlags.Button3Clicked); + + if (isClick) { + host.OnTabClicked (new TabMouseEventArgs (hit, me)); + + // user canceled click + if (me.Handled) { + return true; + } + } + + if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) && !me.Flags.HasFlag (MouseFlags.Button1TripleClicked)) @@ -708,7 +713,7 @@ namespace Terminal.Gui { return true; } - var hit = ScreenToTab (me.X, me.Y); + if (hit != null) { host.SelectedTab = hit; SetNeedsDisplay (); @@ -757,6 +762,45 @@ namespace Terminal.Gui { } } + /// + /// Raises the event. + /// + /// + protected virtual private void OnTabClicked (TabMouseEventArgs tabMouseEventArgs) + { + TabClicked?.Invoke (this, tabMouseEventArgs); + } + + /// + /// Describes a mouse event over a specific in a . + /// + public class TabMouseEventArgs : EventArgs { + + /// + /// Gets the (if any) that the mouse + /// was over when the occurred. + /// + /// This will be null if the click is after last tab + /// or before first. + public Tab Tab { get; } + + /// + /// Gets the actual mouse event. Use to cancel this event + /// and perform custom behavior (e.g. show a context menu). + /// + public MouseEvent MouseEvent { get; } + + /// + /// Creates a new instance of the class. + /// + /// that the mouse was over when the event occurred. + /// The mouse activity being reported + public TabMouseEventArgs (Tab tab, MouseEvent mouseEvent) + { + Tab = tab; + MouseEvent = mouseEvent; + } + } /// /// A single tab in a diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 74a8862bd..1a69dec07 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -109,7 +109,7 @@ namespace Terminal.Gui { get => columnOffset; //try to prevent this being set to an out of bounds column - set => columnOffset = Table == null ? 0 : Math.Max (0, Math.Min (Table.Columns.Count - 1, value)); + set => columnOffset = TableIsNullOrInvisible() ? 0 : Math.Max (0, Math.Min (Table.Columns.Count - 1, value)); } /// @@ -117,7 +117,7 @@ namespace Terminal.Gui { /// public int RowOffset { get => rowOffset; - set => rowOffset = Table == null ? 0 : Math.Max (0, Math.Min (Table.Rows.Count - 1, value)); + set => rowOffset = TableIsNullOrInvisible () ? 0 : Math.Max (0, Math.Min (Table.Rows.Count - 1, value)); } /// @@ -130,7 +130,7 @@ namespace Terminal.Gui { var oldValue = selectedColumn; //try to prevent this being set to an out of bounds column - selectedColumn = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); + selectedColumn = TableIsNullOrInvisible () ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); if (oldValue != selectedColumn) OnSelectedCellChanged (new SelectedCellChangedEventArgs (Table, oldValue, SelectedColumn, SelectedRow, SelectedRow)); @@ -146,7 +146,7 @@ namespace Terminal.Gui { var oldValue = selectedRow; - selectedRow = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); + selectedRow = TableIsNullOrInvisible () ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); if (oldValue != selectedRow) OnSelectedCellChanged (new SelectedCellChangedEventArgs (Table, SelectedColumn, SelectedColumn, oldValue, selectedRow)); @@ -315,7 +315,7 @@ namespace Terminal.Gui { var rowToRender = RowOffset + (line - headerLinesConsumed); //if we have run off the end of the table - if (Table == null || rowToRender >= Table.Rows.Count || rowToRender < 0) + if (TableIsNullOrInvisible () || rowToRender >= Table.Rows.Count || rowToRender < 0) continue; RenderRow (line, rowToRender, columnsToRender); @@ -427,6 +427,36 @@ namespace Terminal.Gui { private void RenderHeaderUnderline (int row, int availableWidth, ColumnToRender [] columnsToRender) { + /* + * First lets work out if we should be rendering scroll indicators + */ + + // are there are visible columns to the left that have been pushed + // off the screen due to horizontal scrolling? + bool moreColumnsToLeft = ColumnOffset > 0; + + // if we moved left would we find a new column (or are they all invisible?) + if(!TryGetNearestVisibleColumn (ColumnOffset-1, false, false, out _)) { + moreColumnsToLeft = false; + } + + // are there visible columns to the right that have not yet been reached? + // lets find out, what is the column index of the last column we are rendering + int lastColumnIdxRendered = ColumnOffset + columnsToRender.Length - 1; + + // are there more valid indexes? + bool moreColumnsToRight = lastColumnIdxRendered < Table.Columns.Count; + + // if we went right from the last column would we find a new visible column? + if(!TryGetNearestVisibleColumn (lastColumnIdxRendered + 1, true, false, out _)) { + // no we would not + moreColumnsToRight = false; + } + + /* + * Now lets draw the line itself + */ + // Renders a line below the table headers (when visible) like: // ├──────────┼───────────┼───────────────────┼──────────┼────────┼─────────────┤ @@ -436,7 +466,7 @@ namespace Terminal.Gui { // whole way but update to instead draw a header indicator // or scroll arrow etc var rune = Driver.HLine; - + if (Style.ShowVerticalHeaderLines) { if (c == 0) { // for first character render line @@ -445,7 +475,7 @@ namespace Terminal.Gui { // unless we have horizontally scrolled along // in which case render an arrow, to indicate user // can scroll left - if(Style.ShowHorizontalScrollIndicators && ColumnOffset > 0) + if(Style.ShowHorizontalScrollIndicators && moreColumnsToLeft) { rune = Driver.LeftArrow; scrollLeftPoint = new Point(c,row); @@ -465,8 +495,7 @@ namespace Terminal.Gui { // unless there is more of the table we could horizontally // scroll along to see. In which case render an arrow, // to indicate user can scroll right - if(Style.ShowHorizontalScrollIndicators && - ColumnOffset + columnsToRender.Length < Table.Columns.Count) + if(Style.ShowHorizontalScrollIndicators && moreColumnsToRight) { rune = Driver.RightArrow; scrollRightPoint = new Point(c,row); @@ -683,7 +712,7 @@ namespace Terminal.Gui { /// public override bool ProcessKey (KeyEvent keyEvent) { - if (Table == null || Table.Columns.Count <= 0) { + if (TableIsNullOrInvisible ()) { PositionCursor (); return false; } @@ -705,6 +734,12 @@ namespace Terminal.Gui { /// True to create a multi cell selection or adjust an existing one public void SetSelection (int col, int row, bool extendExistingSelection) { + // if we are trying to increase the column index then + // we are moving right otherwise we are moving left + bool lookRight = col > selectedColumn; + + col = GetNearestVisibleColumn (col, lookRight, true); + if (!MultiSelect || !extendExistingSelection) MultiSelectedRegions.Clear (); @@ -726,6 +761,41 @@ namespace Terminal.Gui { SelectedRow = row; } + /// + /// Unions the current selected cell (and/or regions) with the provided cell and makes + /// it the active one. + /// + /// + /// + private void UnionSelection (int col, int row) + { + if (!MultiSelect || TableIsNullOrInvisible()) { + return; + } + + EnsureValidSelection (); + + var oldColumn = SelectedColumn; + var oldRow = SelectedRow; + + // move us to the new cell + SelectedColumn = col; + SelectedRow = row; + MultiSelectedRegions.Push ( + CreateTableSelection (col, row) + ); + + // if the old cell was not part of a rectangular select + // or otherwise selected we need to retain it in the selection + + if (!IsSelected (oldColumn, oldRow)) { + MultiSelectedRegions.Push ( + CreateTableSelection (oldColumn, oldRow) + ); + } + } + + /// /// Moves the and by the provided offsets. Optionally starting a box selection (see ) /// @@ -759,22 +829,28 @@ namespace Terminal.Gui { } /// - /// Moves or extends the selection to the first cell in the table (0,0) + /// Moves or extends the selection to the first cell in the table (0,0). + /// If is enabled then selection instead moves + /// to (,0) i.e. no horizontal scrolling. /// /// true to extend the current selection (if any) instead of replacing public void ChangeSelectionToStartOfTable (bool extend) { - SetSelection (0, 0, extend); + SetSelection (FullRowSelect ? SelectedColumn : 0, 0, extend); Update (); } /// - /// Moves or extends the selection to the final cell in the table + /// Moves or extends the selection to the final cell in the table (nX,nY). + /// If is enabled then selection instead moves + /// to (,nY) i.e. no horizontal scrolling. /// /// true to extend the current selection (if any) instead of replacing public void ChangeSelectionToEndOfTable(bool extend) { - SetSelection (Table.Columns.Count - 1, Table.Rows.Count - 1, extend); + var finalColumn = Table.Columns.Count - 1; + + SetSelection (FullRowSelect ? SelectedColumn : finalColumn, Table.Rows.Count - 1, extend); Update (); } @@ -804,7 +880,7 @@ namespace Terminal.Gui { /// public void SelectAll () { - if (Table == null || !MultiSelect || Table.Rows.Count == 0) + if (TableIsNullOrInvisible() || !MultiSelect || Table.Rows.Count == 0) return; MultiSelectedRegions.Clear (); @@ -817,10 +893,12 @@ namespace Terminal.Gui { /// /// Returns all cells in any (if is enabled) and the selected cell /// + /// Return value is not affected by (i.e. returned s are not expanded to + /// include all points on row). /// public IEnumerable GetAllSelectedCells () { - if (Table == null || Table.Rows.Count == 0) + if (TableIsNullOrInvisible () || Table.Rows.Count == 0) yield break; EnsureValidSelection (); @@ -880,13 +958,30 @@ namespace Terminal.Gui { } /// - /// Returns true if the given cell is selected either because it is the active cell or part of a multi cell selection (e.g. ) + /// Returns a single point as a + /// + /// + /// + /// + private TableSelection CreateTableSelection (int x, int y) + { + return CreateTableSelection (x, y, x, y); + } + /// + /// + /// Returns true if the given cell is selected either because it is the active cell or part of a multi cell selection (e.g. ). + /// + /// Returns if is . /// /// /// /// public bool IsSelected (int col, int row) { + if(!IsColumnVisible(col)) { + return false; + } + // Cell is also selected if in any multi selection region if (MultiSelect && MultiSelectedRegions.Any (r => r.Rect.Contains (col, row))) return true; @@ -899,12 +994,28 @@ namespace Terminal.Gui { (col == SelectedColumn || FullRowSelect); } + /// + /// Returns true if the given indexes a visible + /// column otherwise false. Returns false for indexes that are out of bounds. + /// + /// + /// + private bool IsColumnVisible (int columnIndex) + { + // if the column index provided is out of bounds + if (columnIndex < 0 || columnIndex >= table.Columns.Count) { + return false; + } + + return this.Style.GetColumnStyleIfAny (Table.Columns [columnIndex])?.Visible ?? true; + } + /// /// Positions the cursor in the area of the screen in which the start of the active cell is rendered. Calls base implementation if active cell is not visible due to scrolling or table is loaded etc /// public override void PositionCursor () { - if (Table == null) { + if (TableIsNullOrInvisible ()) { base.PositionCursor (); return; } @@ -927,7 +1038,7 @@ namespace Terminal.Gui { SetFocus (); } - if (Table == null || Table.Columns.Count <= 0) { + if (TableIsNullOrInvisible ()) { return false; } @@ -981,7 +1092,12 @@ namespace Terminal.Gui { var hit = ScreenToCell (me.X, me.Y); if (hit != null) { - SetSelection (hit.Value.X, hit.Value.Y, me.Flags.HasFlag (MouseFlags.ButtonShift)); + if(MultiSelect && HasControlOrAlt(me)) { + UnionSelection(hit.Value.X, hit.Value.Y); + } else { + SetSelection (hit.Value.X, hit.Value.Y, me.Flags.HasFlag (MouseFlags.ButtonShift)); + } + Update (); } } @@ -997,15 +1113,33 @@ namespace Terminal.Gui { return false; } - /// - /// Returns the column and row of that corresponds to a given point on the screen (relative to the control client area). Returns null if the point is in the header, no table is loaded or outside the control bounds + private bool HasControlOrAlt (MouseEvent me) + { + return me.Flags.HasFlag (MouseFlags.ButtonAlt) || me.Flags.HasFlag (MouseFlags.ButtonCtrl); + } + + /// . + /// Returns the column and row of that corresponds to a given point + /// on the screen (relative to the control client area). Returns null if the point is + /// in the header, no table is loaded or outside the control bounds. /// - /// X offset from the top left of the control - /// Y offset from the top left of the control - /// + /// X offset from the top left of the control. + /// Y offset from the top left of the control. + /// Cell clicked or null. public Point? ScreenToCell (int clientX, int clientY) { - if (Table == null || Table.Columns.Count <= 0) + return ScreenToCell(clientX, clientY, out _); + } + + /// + /// X offset from the top left of the control. + /// Y offset from the top left of the control. + /// If the click is in a header this is the column clicked. + public Point? ScreenToCell (int clientX, int clientY, out DataColumn headerIfAny) + { + headerIfAny = null; + + if (TableIsNullOrInvisible ()) return null; var viewPort = CalculateViewport (Bounds); @@ -1015,11 +1149,20 @@ namespace Terminal.Gui { var col = viewPort.LastOrDefault (c => c.X <= clientX); // Click is on the header section of rendered UI - if (clientY < headerHeight) + if (clientY < headerHeight) { + headerIfAny = col?.Column; return null; + } + var rowIdx = RowOffset - headerHeight + clientY; + // if click is off bottom of the rows don't give an + // invalid index back to user! + if (rowIdx >= Table.Rows.Count) { + return null; + } + if (col != null && rowIdx >= 0) { return new Point (col.Column.Ordinal, rowIdx); @@ -1036,7 +1179,7 @@ namespace Terminal.Gui { /// public Point? CellToScreen (int tableColumn, int tableRow) { - if (Table == null || Table.Columns.Count <= 0) + if (TableIsNullOrInvisible ()) return null; var viewPort = CalculateViewport (Bounds); @@ -1065,7 +1208,7 @@ namespace Terminal.Gui { /// This always calls public void Update () { - if (Table == null) { + if (TableIsNullOrInvisible ()) { SetNeedsDisplay (); return; } @@ -1084,7 +1227,7 @@ namespace Terminal.Gui { /// Changes will not be immediately visible in the display until you call public void EnsureValidScrollOffsets () { - if (Table == null) { + if (TableIsNullOrInvisible ()) { return; } @@ -1099,7 +1242,7 @@ namespace Terminal.Gui { /// Changes will not be immediately visible in the display until you call public void EnsureValidSelection () { - if (Table == null) { + if (TableIsNullOrInvisible()) { // Table doesn't exist, we should probably clear those selections MultiSelectedRegions.Clear (); @@ -1109,6 +1252,9 @@ namespace Terminal.Gui { SelectedColumn = Math.Max (Math.Min (SelectedColumn, Table.Columns.Count - 1), 0); SelectedRow = Math.Max (Math.Min (SelectedRow, Table.Rows.Count - 1), 0); + // If SelectedColumn is invisible move it to a visible one + SelectedColumn = GetNearestVisibleColumn (SelectedColumn, lookRight: true, true); + var oldRegions = MultiSelectedRegions.ToArray ().Reverse (); MultiSelectedRegions.Clear (); @@ -1137,7 +1283,100 @@ namespace Terminal.Gui { MultiSelectedRegions.Push (region); } + } + /// + /// Returns true if the is not set or all the + /// in the have an explicit + /// that marks them + /// . + /// + /// + private bool TableIsNullOrInvisible () + { + return Table == null || + Table.Columns.Count <= 0 || + Table.Columns.Cast ().All ( + c => (Style.GetColumnStyleIfAny (c)?.Visible ?? true) == false); + } + + /// + /// Returns unless the is false for + /// the indexed . If so then the index returned is nudged to the nearest visible + /// column. + /// + /// Returns unchanged if it is invalid (e.g. out of bounds). + /// The input column index. + /// When nudging invisible selections look right first. + /// to look right, to look left. + /// If we cannot find anything visible when + /// looking in direction of then should we look in the opposite + /// direction instead? Use true if you want to push a selection to a valid index no matter what. + /// Use false if you are primarily interested in learning about directional column visibility. + private int GetNearestVisibleColumn (int columnIndex, bool lookRight, bool allowBumpingInOppositeDirection) + { + if(TryGetNearestVisibleColumn(columnIndex,lookRight,allowBumpingInOppositeDirection, out var answer)) + { + return answer; + } + + return columnIndex; + } + + private bool TryGetNearestVisibleColumn (int columnIndex, bool lookRight, bool allowBumpingInOppositeDirection, out int idx) + { + // if the column index provided is out of bounds + if (columnIndex < 0 || columnIndex >= table.Columns.Count) { + + idx = columnIndex; + return false; + } + + // get the column visibility by index (if no style visible is true) + bool [] columnVisibility = Table.Columns.Cast () + .Select (c => this.Style.GetColumnStyleIfAny (c)?.Visible ?? true) + .ToArray(); + + // column is visible + if (columnVisibility [columnIndex]) { + idx = columnIndex; + return true; + } + + int increment = lookRight ? 1 : -1; + + // move in that direction + for (int i = columnIndex; i >=0 && i < columnVisibility.Length; i += increment) { + // if we find a visible column + if(columnVisibility [i]) + { + idx = i; + return true; + } + } + + // Caller only wants to look in one direction and we did not find any + // visible columns in that direction + if(!allowBumpingInOppositeDirection) { + idx = columnIndex; + return false; + } + + // Caller will let us look in the other direction so + // now look other way + increment = -increment; + + for (int i = columnIndex; i >= 0 && i < columnVisibility.Length; i += increment) { + // if we find a visible column + if (columnVisibility [i]) { + idx = i; + return true; + } + } + + // nothing seems to be visible so just return input index + idx = columnIndex; + return false; } /// @@ -1217,7 +1456,7 @@ namespace Terminal.Gui { /// private IEnumerable CalculateViewport (Rect bounds, int padding = 1) { - if (Table == null || Table.Columns.Count <= 0) + if (TableIsNullOrInvisible ()) yield break; int usedSpace = 0; @@ -1242,6 +1481,12 @@ namespace Terminal.Gui { var colStyle = Style.GetColumnStyleIfAny (col); int colWidth; + // if column is not being rendered + if(colStyle?.Visible == false) { + // do not add it to the returned columns + continue; + } + // is there enough space for this column (and it's data)? colWidth = CalculateMaxCellWidth (col, rowsToRender, colStyle) + padding; @@ -1289,7 +1534,7 @@ namespace Terminal.Gui { private bool ShouldRenderHeaders () { - if (Table == null || Table.Columns.Count == 0) + if (TableIsNullOrInvisible ()) return false; return Style.AlwaysShowHeaders || rowOffset == 0; @@ -1397,6 +1642,7 @@ namespace Terminal.Gui { /// Return null for the default /// public CellColorGetterDelegate ColorGetter; + private bool visible = true; /// /// Defines the format for values e.g. "yyyy-MM-dd" for dates @@ -1427,6 +1673,15 @@ namespace Terminal.Gui { /// public int MinAcceptableWidth { get; set; } = DefaultMinAcceptableWidth; + /// + /// Gets or Sets a value indicating whether the column should be visible to the user. + /// This affects both whether it is rendered and whether it can be selected. Defaults to + /// true. + /// + /// If is 0 then will always return false. + public bool Visible { get => MaxWidth >= 0 && visible; set => visible = value; } + + /// /// Returns the alignment for the cell based on and / /// diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 5852c7051..14fc56394 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -235,7 +235,10 @@ namespace Terminal.Gui { private void HistoryText_ChangeText (HistoryText.HistoryTextItem obj) { - Text = ustring.Make (obj.Lines [obj.CursorPosition.Y]); + if (obj == null) + return; + + Text = ustring.Make (obj?.Lines [obj.CursorPosition.Y]); CursorPosition = obj.CursorPosition.X; Adjust (); } @@ -295,6 +298,7 @@ namespace Terminal.Gui { } return; } + ClearAllSelection (); text = TextModel.ToRunes (newText.NewText); if (!Secret && !historyText.IsFromHistory) { diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index edbdb8bd3..3c9e54b09 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -1,28 +1,4 @@ -// // TextView.cs: multi-line text editing -// -// Authors: -// Miguel de Icaza (miguel@gnome.org) -// -// -// TODO: -// In ReadOnly mode backspace/space behave like pageup/pagedown -// Attributed text on spans -// Replace insertion with Insert method -// String accumulation (Control-k, control-k is not preserving the last new line, see StringToRunes -// Alt-D, Alt-Backspace -// API to set the cursor position -// API to scroll to a particular place -// keybindings to go to top/bottom -// public API to insert, remove ranges -// Add word forward/word backwards commands -// Save buffer API -// Mouse -// -// Desirable: -// Move all the text manipulation into the TextModel - - using System; using System.Collections.Generic; using System.Globalization; @@ -33,6 +9,7 @@ using System.Text; using System.Threading; using NStack; using Terminal.Gui.Resources; +using static Terminal.Gui.Graphs.PathAnnotation; using Rune = System.Rune; namespace Terminal.Gui { @@ -742,6 +719,7 @@ namespace Terminal.Gui { historyTextItems.Clear (); idxHistoryText = -1; originalText = text; + OnChangeText (null); } public bool IsDirty (ustring text) @@ -873,7 +851,7 @@ namespace Terminal.Gui { var firstLine = wrappedModelLines.IndexOf (r => r.ModelLine == modelLine); int modelCol = 0; - for (int i = firstLine; i <= line; i++) { + for (int i = firstLine; i <= Math.Min (line, wrappedModelLines.Count - 1); i++) { var wLine = wrappedModelLines [i]; if (i < line) { @@ -1037,120 +1015,119 @@ namespace Terminal.Gui { } /// - /// Multi-line text editing + /// Multi-line text editing . /// /// - /// - /// provides a multi-line text editor. Users interact - /// with it with the standard Emacs commands for movement or the arrow - /// keys. - /// - /// - /// - /// Shortcut - /// Action performed - /// - /// - /// Left cursor, Control-b - /// - /// Moves the editing point left. - /// - /// - /// - /// Right cursor, Control-f - /// - /// Moves the editing point right. - /// - /// - /// - /// Alt-b - /// - /// Moves one word back. - /// - /// - /// - /// Alt-f - /// - /// Moves one word forward. - /// - /// - /// - /// Up cursor, Control-p - /// - /// Moves the editing point one line up. - /// - /// - /// - /// Down cursor, Control-n - /// - /// Moves the editing point one line down - /// - /// - /// - /// Home key, Control-a - /// - /// Moves the cursor to the beginning of the line. - /// - /// - /// - /// End key, Control-e - /// - /// Moves the cursor to the end of the line. - /// - /// - /// - /// Control-Home - /// - /// Scrolls to the first line and moves the cursor there. - /// - /// - /// - /// Control-End - /// - /// Scrolls to the last line and moves the cursor there. - /// - /// - /// - /// Delete, Control-d - /// - /// Deletes the character in front of the cursor. - /// - /// - /// - /// Backspace - /// - /// Deletes the character behind the cursor. - /// - /// - /// - /// Control-k - /// - /// Deletes the text until the end of the line and replaces the kill buffer - /// with the deleted text. You can paste this text in a different place by - /// using Control-y. - /// - /// - /// - /// Control-y - /// - /// Pastes the content of the kill ring into the current position. - /// - /// - /// - /// Alt-d - /// - /// Deletes the word above the cursor and adds it to the kill ring. You - /// can paste the contents of the kill ring with Control-y. - /// - /// - /// - /// Control-q - /// - /// Quotes the next input character, to prevent the normal processing of - /// key handling to take place. - /// - /// - /// + /// + /// provides a multi-line text editor. Users interact + /// with it with the standard Windows, Mac, and Linux (Emacs) commands. + /// + /// + /// + /// Shortcut + /// Action performed + /// + /// + /// Left cursor, Control-b + /// + /// Moves the editing point left. + /// + /// + /// + /// Right cursor, Control-f + /// + /// Moves the editing point right. + /// + /// + /// + /// Alt-b + /// + /// Moves one word back. + /// + /// + /// + /// Alt-f + /// + /// Moves one word forward. + /// + /// + /// + /// Up cursor, Control-p + /// + /// Moves the editing point one line up. + /// + /// + /// + /// Down cursor, Control-n + /// + /// Moves the editing point one line down + /// + /// + /// + /// Home key, Control-a + /// + /// Moves the cursor to the beginning of the line. + /// + /// + /// + /// End key, Control-e + /// + /// Moves the cursor to the end of the line. + /// + /// + /// + /// Control-Home + /// + /// Scrolls to the first line and moves the cursor there. + /// + /// + /// + /// Control-End + /// + /// Scrolls to the last line and moves the cursor there. + /// + /// + /// + /// Delete, Control-d + /// + /// Deletes the character in front of the cursor. + /// + /// + /// + /// Backspace + /// + /// Deletes the character behind the cursor. + /// + /// + /// + /// Control-k + /// + /// Deletes the text until the end of the line and replaces the kill buffer + /// with the deleted text. You can paste this text in a different place by + /// using Control-y. + /// + /// + /// + /// Control-y + /// + /// Pastes the content of the kill ring into the current position. + /// + /// + /// + /// Alt-d + /// + /// Deletes the word above the cursor and adds it to the kill ring. You + /// can paste the contents of the kill ring with Control-y. + /// + /// + /// + /// Control-q + /// + /// Quotes the next input character, to prevent the normal processing of + /// key handling to take place. + /// + /// + /// /// public class TextView : View { TextModel model = new TextModel (); @@ -1172,10 +1149,24 @@ namespace Terminal.Gui { CultureInfo currentCulture; /// - /// Raised when the of the changes. + /// Raised when the property of the changes. /// + /// + /// The property of only changes when it is explicitly + /// set, not as the user types. To be notified as the user changes the contents of the TextView + /// see . + /// public event Action TextChanged; + /// + /// Raised when the contents of the are changed. + /// + /// + /// Unlike the event, this event is raised whenever the user types or + /// otherwise changes the contents of the . + /// + public event Action ContentsChanged; + /// /// Invoked with the unwrapped . /// @@ -1183,22 +1174,12 @@ namespace Terminal.Gui { /// /// Provides autocomplete context menu based on suggestions at the current cursor - /// position. Populate to enable this feature + /// position. Populate to enable this feature /// public IAutocomplete Autocomplete { get; protected set; } = new TextViewAutocomplete (); -#if false /// - /// Changed event, raised when the text has clicked. - /// - /// - /// Client code can hook up to this event, it is - /// raised when the text in the entry changes. - /// - public Action Changed; -#endif - /// - /// Initializes a on the specified area, with absolute position and size. + /// Initializes a on the specified area, with absolute position and size. /// /// /// @@ -1208,8 +1189,8 @@ namespace Terminal.Gui { } /// - /// Initializes a on the specified area, - /// with dimensions controlled with the X, Y, Width and Height properties. + /// Initializes a on the specified area, + /// with dimensions controlled with the X, Y, Width and Height properties. /// public TextView () : base () { @@ -1404,48 +1385,56 @@ namespace Terminal.Gui { private void Model_LinesLoaded () { - historyText.Clear (Text); + // This call is not needed. Model_LinesLoaded gets invoked when + // model.LoadString (value) is called. LoadString is called from one place + // (Text.set) and historyText.Clear() is called immediately after. + // If this call happens, HistoryText_ChangeText will get called multiple times + // when Text is set, which is wrong. + //historyText.Clear (Text); } private void HistoryText_ChangeText (HistoryText.HistoryTextItem obj) { SetWrapModel (); - var startLine = obj.CursorPosition.Y; + if (obj != null) { + var startLine = obj.CursorPosition.Y; - if (obj.RemovedOnAdded != null) { - int offset; - if (obj.IsUndoing) { - offset = Math.Max (obj.RemovedOnAdded.Lines.Count - obj.Lines.Count, 1); - } else { - offset = obj.RemovedOnAdded.Lines.Count - 1; - } - for (int i = 0; i < offset; i++) { - if (Lines > obj.RemovedOnAdded.CursorPosition.Y) { - model.RemoveLine (obj.RemovedOnAdded.CursorPosition.Y); + if (obj.RemovedOnAdded != null) { + int offset; + if (obj.IsUndoing) { + offset = Math.Max (obj.RemovedOnAdded.Lines.Count - obj.Lines.Count, 1); } else { - break; + offset = obj.RemovedOnAdded.Lines.Count - 1; + } + for (int i = 0; i < offset; i++) { + if (Lines > obj.RemovedOnAdded.CursorPosition.Y) { + model.RemoveLine (obj.RemovedOnAdded.CursorPosition.Y); + } else { + break; + } } } - } - for (int i = 0; i < obj.Lines.Count; i++) { - if (i == 0) { - model.ReplaceLine (startLine, obj.Lines [i]); - } else if ((obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Removed) - || !obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Added) { - model.AddLine (startLine, obj.Lines [i]); - } else if (Lines > obj.CursorPosition.Y + 1) { - model.RemoveLine (obj.CursorPosition.Y + 1); + for (int i = 0; i < obj.Lines.Count; i++) { + if (i == 0) { + model.ReplaceLine (startLine, obj.Lines [i]); + } else if ((obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Removed) + || !obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Added) { + model.AddLine (startLine, obj.Lines [i]); + } else if (Lines > obj.CursorPosition.Y + 1) { + model.RemoveLine (obj.CursorPosition.Y + 1); + } + startLine++; } - startLine++; - } - CursorPosition = obj.FinalCursorPosition; + CursorPosition = obj.FinalCursorPosition; + } UpdateWrapModel (); Adjust (); + OnContentsChanged (); } void TextView_Initialized (object sender, EventArgs e) @@ -1454,6 +1443,7 @@ namespace Terminal.Gui { Application.Top.AlternateForwardKeyChanged += Top_AlternateForwardKeyChanged; Application.Top.AlternateBackwardKeyChanged += Top_AlternateBackwardKeyChanged; + OnContentsChanged (); } void Top_AlternateBackwardKeyChanged (Key obj) @@ -1480,9 +1470,11 @@ namespace Terminal.Gui { } /// - /// Sets or gets the text in the . + /// Sets or gets the text in the . /// /// + /// The event is fired whenever this property is set. Note, however, + /// that Text is not set by as the user types. /// public override ustring Text { get { @@ -1559,12 +1551,12 @@ namespace Terminal.Gui { public int Maxlength => model.GetMaxVisibleLine (topRow, topRow + Frame.Height, TabWidth); /// - /// Gets the number of lines. + /// Gets the number of lines. /// public int Lines => model.Count; /// - /// Sets or gets the current cursor position. + /// Sets or gets the current cursor position. /// public Point CursorPosition { get => new Point (currentColumn, currentRow); @@ -1828,7 +1820,7 @@ namespace Terminal.Gui { } /// - /// Loads the contents of the file into the . + /// Loads the contents of the file into the . /// /// true, if file was loaded, false otherwise. /// Path to the file to load. @@ -1838,6 +1830,7 @@ namespace Terminal.Gui { try { SetWrapModel (); res = model.LoadFile (path); + historyText.Clear (Text); ResetPosition (); } catch (Exception) { throw; @@ -1850,19 +1843,20 @@ namespace Terminal.Gui { } /// - /// Loads the contents of the stream into the . + /// Loads the contents of the stream into the . /// /// true, if stream was loaded, false otherwise. /// Stream to load the contents from. public void LoadStream (Stream stream) { model.LoadStream (stream); + historyText.Clear (Text); ResetPosition (); SetNeedsDisplay (); } /// - /// Closes the contents of the stream into the . + /// Closes the contents of the stream into the . /// /// true, if stream was closed, false otherwise. public bool CloseFile () @@ -1874,7 +1868,7 @@ namespace Terminal.Gui { } /// - /// Gets the current cursor row. + /// Gets the current cursor row. /// public int CurrentRow => currentRow; @@ -1885,7 +1879,7 @@ namespace Terminal.Gui { public int CurrentColumn => currentColumn; /// - /// Positions the cursor on the current row and column + /// Positions the cursor on the current row and column /// public override void PositionCursor () { @@ -1936,7 +1930,7 @@ namespace Terminal.Gui { } /// - /// Sets the driver to the default color for the control where no text is being rendered. Defaults to . + /// Sets the driver to the default color for the control where no text is being rendered. Defaults to . /// protected virtual void SetNormalColor () { @@ -1945,7 +1939,7 @@ namespace Terminal.Gui { /// /// Sets the to an appropriate color for rendering the given of the - /// current . Override to provide custom coloring by calling + /// current . Override to provide custom coloring by calling /// Defaults to . /// /// @@ -1957,7 +1951,7 @@ namespace Terminal.Gui { /// /// Sets the to an appropriate color for rendering the given of the - /// current . Override to provide custom coloring by calling + /// current . Override to provide custom coloring by calling /// Defaults to . /// /// @@ -1969,7 +1963,7 @@ namespace Terminal.Gui { /// /// Sets the to an appropriate color for rendering the given of the - /// current . Override to provide custom coloring by calling + /// current . Override to provide custom coloring by calling /// Defaults to . /// /// @@ -1987,7 +1981,7 @@ namespace Terminal.Gui { /// /// Sets the to an appropriate color for rendering the given of the - /// current . Override to provide custom coloring by calling + /// current . Override to provide custom coloring by calling /// Defaults to . /// /// @@ -2000,7 +1994,7 @@ namespace Terminal.Gui { bool isReadOnly = false; /// - /// Gets or sets whether the is in read-only mode or not + /// Gets or sets whether the is in read-only mode or not /// /// Boolean value(Default false) public bool ReadOnly { @@ -2504,6 +2498,12 @@ namespace Terminal.Gui { InsertText (new KeyEvent () { Key = key }); } + + if (NeedDisplay.IsEmpty) { + PositionCursor (); + } else { + Adjust (); + } } void Insert (Rune rune) @@ -2521,6 +2521,7 @@ namespace Terminal.Gui { if (!wrapNeeded) { SetNeedsDisplay (new Rect (0, prow, Math.Max (Frame.Width, 0), Math.Max (prow + 1, 0))); } + } ustring StringFromRunes (List runes) @@ -2584,6 +2585,8 @@ namespace Terminal.Gui { UpdateWrapModel (); + OnContentsChanged (); + return; } @@ -2690,6 +2693,42 @@ namespace Terminal.Gui { OnUnwrappedCursorPosition (); } + /// + /// Event arguments for events for when the contents of the TextView change. E.g. the event. + /// + public class ContentsChangedEventArgs : EventArgs { + /// + /// Creates a new instance. + /// + /// Contains the row where the change occurred. + /// Contains the column where the change occured. + public ContentsChangedEventArgs (int currentRow, int currentColumn) + { + Row = currentRow; + Col = currentColumn; + } + + /// + /// + /// Contains the row where the change occurred. + /// + public int Row { get; private set; } + + /// + /// Contains the column where the change occurred. + /// + public int Col { get; private set; } + } + + /// + /// Called when the contents of the TextView change. E.g. when the user types text or deletes text. Raises + /// the event. + /// + public virtual void OnContentsChanged () + { + ContentsChanged?.Invoke (new ContentsChangedEventArgs (CurrentRow, CurrentColumn)); + } + (int width, int height) OffSetBackground () { int w = 0; @@ -2708,7 +2747,7 @@ namespace Terminal.Gui { /// will scroll the to display the specified column at the left if is false. /// /// Row that should be displayed at the top or Column that should be displayed at the left, - /// if the value is negative it will be reset to zero + /// if the value is negative it will be reset to zero /// If true (default) the is a row, column otherwise. public void ScrollTo (int idx, bool isRow = true) { @@ -3178,6 +3217,7 @@ namespace Terminal.Gui { UpdateWrapModel (); DoNeededAction (); + OnContentsChanged (); return true; } @@ -3674,6 +3714,7 @@ namespace Terminal.Gui { HistoryText.LineStatus.Replaced); UpdateWrapModel (); + OnContentsChanged (); return true; } @@ -3883,6 +3924,7 @@ namespace Terminal.Gui { UpdateWrapModel (); selecting = false; DoNeededAction (); + OnContentsChanged (); } /// @@ -3913,6 +3955,7 @@ namespace Terminal.Gui { historyText.Add (new List> () { new List (GetCurrentLine ()) }, CursorPosition, HistoryText.LineStatus.Replaced); + OnContentsChanged (); } else { if (selecting) { ClearRegion (); @@ -4423,6 +4466,7 @@ namespace Terminal.Gui { } } + /// /// Renders an overlay on another view at a given point that allows selecting /// from a range of 'autocomplete' options. diff --git a/Terminal.Gui/Views/TreeView.cs b/Terminal.Gui/Views/TreeView.cs index 5067295fb..7b8942c39 100644 --- a/Terminal.Gui/Views/TreeView.cs +++ b/Terminal.Gui/Views/TreeView.cs @@ -1,5 +1,5 @@ // 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 +// 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. using NStack; @@ -12,18 +12,18 @@ using Terminal.Gui.Trees; namespace Terminal.Gui { /// - /// Interface for all non generic members of + /// Interface for all non generic members of . /// /// See TreeView Deep Dive for more information. /// public interface ITreeView { /// - /// Contains options for changing how the tree is rendered + /// Contains options for changing how the tree is rendered. /// TreeStyle Style { get; set; } /// - /// Removes all objects from the tree and clears selection + /// Removes all objects from the tree and clears selection. /// void ClearObjects (); @@ -43,7 +43,7 @@ namespace Terminal.Gui { /// /// Creates a new instance of the tree control with absolute positioning and initialises - /// with default based builder + /// with default based builder. /// public TreeView () { @@ -53,8 +53,8 @@ namespace Terminal.Gui { } /// - /// Hierarchical tree view with expandable branches. Branch objects are dynamically determined - /// when expanded using a user defined + /// Hierarchical tree view with expandable branches. Branch objects are dynamically determined + /// when expanded using a user defined . /// /// See TreeView Deep Dive for more information. /// @@ -64,7 +64,7 @@ namespace Terminal.Gui { /// /// Determines how sub branches of the tree are dynamically built at runtime as the user - /// expands root nodes + /// expands root nodes. /// /// public ITreeBuilder TreeBuilder { get; set; } @@ -74,30 +74,27 @@ namespace Terminal.Gui { /// T selectedObject; - /// - /// Contains options for changing how the tree is rendered + /// Contains options for changing how the tree is rendered. /// public TreeStyle Style { get; set; } = new TreeStyle (); - /// - /// True to allow multiple objects to be selected at once + /// True to allow multiple objects to be selected at once. /// /// public bool MultiSelect { get; set; } = true; - /// /// True makes a letter key press navigate to the next visible branch that begins with - /// that letter/digit + /// that letter/digit. /// /// public bool AllowLetterBasedNavigation { get; set; } = true; /// - /// The currently selected object in the tree. When is true this - /// is the object at which the cursor is at + /// The currently selected object in the tree. When is true this + /// is the object at which the cursor is at. /// public T SelectedObject { get => selectedObject; @@ -111,16 +108,15 @@ namespace Terminal.Gui { } } - /// /// This event is raised when an object is activated e.g. by double clicking or - /// pressing + /// pressing . /// public event Action> ObjectActivated; /// /// Key which when pressed triggers . - /// Defaults to Enter + /// Defaults to Enter. /// public Key ObjectActivationKey { get => objectActivationKey; @@ -140,15 +136,14 @@ namespace Terminal.Gui { /// public MouseFlags? ObjectActivationButton { get; set; } = MouseFlags.Button1DoubleClicked; - /// - /// Delegate for multi colored tree views. Return the to use + /// Delegate for multi colored tree views. Return the to use /// for each passed object or null to use the default. /// - public Func ColorGetter {get;set;} + public Func ColorGetter { get; set; } /// - /// Secondary selected regions of tree when is true + /// Secondary selected regions of tree when is true. /// private Stack> multiSelectedRegions = new Stack> (); @@ -157,36 +152,35 @@ namespace Terminal.Gui { /// private IReadOnlyCollection> cachedLineMap; - /// /// Error message to display when the control is not properly initialized at draw time - /// (nodes added but no tree builder set) + /// (nodes added but no tree builder set). /// public static ustring NoBuilderError = "ERROR: TreeBuilder Not Set"; private Key objectActivationKey = Key.Enter; /// - /// Called when the changes + /// Called when the changes. /// public event EventHandler> SelectionChanged; /// - /// The root objects in the tree, note that this collection is of root objects only + /// The root objects in the tree, note that this collection is of root objects only. /// public IEnumerable Objects { get => roots.Keys; } /// - /// Map of root objects to the branches under them. All objects have - /// a even if that branch has no children + /// Map of root objects to the branches under them. All objects have + /// a even if that branch has no children. /// internal Dictionary> roots { get; set; } = new Dictionary> (); /// /// The amount of tree view that has been scrolled off the top of the screen (by the user - /// scrolling down) + /// scrolling down). /// - /// Setting a value of less than 0 will result in a offset of 0. To see changes - /// in the UI call + /// Setting a value of less than 0 will result in a offset of 0. To see changes + /// in the UI call . public int ScrollOffsetVertical { get => scrollOffsetVertical; set { @@ -194,12 +188,11 @@ namespace Terminal.Gui { } } - /// - /// The amount of tree view that has been scrolled to the right (horizontally) + /// The amount of tree view that has been scrolled to the right (horizontally). /// - /// Setting a value of less than 0 will result in a offset of 0. To see changes - /// in the UI call + /// Setting a value of less than 0 will result in a offset of 0. To see changes + /// in the UI call . public int ScrollOffsetHorizontal { get => scrollOffsetHorizontal; set { @@ -208,37 +201,42 @@ namespace Terminal.Gui { } /// - /// The current number of rows in the tree (ignoring the controls bounds) + /// The current number of rows in the tree (ignoring the controls bounds). /// public int ContentHeight => BuildLineMap ().Count (); /// - /// Returns the string representation of model objects hosted in the tree. Default - /// implementation is to call + /// Returns the string representation of model objects hosted in the tree. Default + /// implementation is to call . /// /// public AspectGetterDelegate AspectGetter { get; set; } = (o) => o.ToString () ?? ""; - CursorVisibility desiredCursorVisibility = CursorVisibility.Default; + CursorVisibility desiredCursorVisibility = CursorVisibility.Invisible; /// - /// Get / Set the wished cursor when the tree is focused + /// Get / Set the wished cursor when the tree is focused. + /// Only applies when is true. + /// Defaults to . /// public CursorVisibility DesiredCursorVisibility { - get => desiredCursorVisibility; + get { + return MultiSelect ? desiredCursorVisibility : CursorVisibility.Invisible; + } set { - if (desiredCursorVisibility != value && HasFocus) { - Application.Driver.SetCursorVisibility (value); + if (desiredCursorVisibility != value) { + desiredCursorVisibility = value; + if (HasFocus) { + Application.Driver.SetCursorVisibility (DesiredCursorVisibility); + } } - - desiredCursorVisibility = value; } } /// - /// Creates a new tree view with absolute positioning. + /// Creates a new tree view with absolute positioning. /// Use to set set root objects for the tree. - /// Children will not be rendered until you set + /// Children will not be rendered until you set . /// public TreeView () : base () { @@ -295,7 +293,7 @@ namespace Terminal.Gui { /// /// Initialises .Creates a new tree view with absolute - /// positioning. Use to set set root + /// positioning. Use to set set root /// objects for the tree. /// public TreeView (ITreeBuilder builder) : this () @@ -312,7 +310,7 @@ namespace Terminal.Gui { } /// - /// Adds a new root level object unless it is already a root of the tree + /// Adds a new root level object unless it is already a root of the tree. /// /// public void AddObject (T o) @@ -324,9 +322,8 @@ namespace Terminal.Gui { } } - /// - /// Removes all objects from the tree and clears + /// Removes all objects from the tree and clears . /// public void ClearObjects () { @@ -341,7 +338,7 @@ namespace Terminal.Gui { /// Removes the given root object from the tree /// /// If is the currently then the - /// selection is cleared + /// selection is cleared. /// public void Remove (T o) { @@ -357,9 +354,9 @@ namespace Terminal.Gui { } /// - /// Adds many new root level objects. Objects that are already root objects are ignored + /// Adds many new root level objects. Objects that are already root objects are ignored. /// - /// Objects to add as new root level objects + /// Objects to add as new root level objects..\ public void AddObjects (IEnumerable collection) { bool objectsAdded = false; @@ -378,13 +375,13 @@ namespace Terminal.Gui { } /// - /// Refreshes the state of the object in the tree. This will - /// recompute children, string representation etc + /// Refreshes the state of the object in the tree. This will + /// recompute children, string representation etc. /// /// This has no effect if the object is not exposed in the tree. /// /// True to also refresh all ancestors of the objects branch - /// (starting with the root). False to refresh only the passed node + /// (starting with the root). False to refresh only the passed node. public void RefreshObject (T o, bool startAtTop = false) { var branch = ObjectToBranch (o); @@ -399,7 +396,7 @@ namespace Terminal.Gui { /// /// Rebuilds the tree structure for all exposed objects starting with the root objects. /// Call this method when you know there are changes to the tree but don't know which - /// objects have changed (otherwise use ) + /// objects have changed (otherwise use ). /// public void RebuildTree () { @@ -412,10 +409,10 @@ namespace Terminal.Gui { } /// - /// Returns the currently expanded children of the passed object. Returns an empty - /// collection if the branch is not exposed or not expanded + /// Returns the currently expanded children of the passed object. Returns an empty + /// collection if the branch is not exposed or not expanded. /// - /// An object in the tree + /// An object in the tree. /// public IEnumerable GetChildren (T o) { @@ -428,10 +425,10 @@ namespace Terminal.Gui { return branch.ChildBranches?.Values?.Select (b => b.Model)?.ToArray () ?? new T [0]; } /// - /// Returns the parent object of in the tree. Returns null if - /// the object is not exposed in the tree + /// Returns the parent object of in the tree. Returns null if + /// the object is not exposed in the tree. /// - /// An object in the tree + /// An object in the tree. /// public T GetParent (T o) { @@ -468,20 +465,19 @@ namespace Terminal.Gui { Driver.SetAttribute (GetNormalColor ()); Driver.AddStr (new string (' ', bounds.Width)); } - } } /// /// Returns the index of the object if it is currently exposed (it's - /// parent(s) have been expanded). This can be used with - /// and to scroll to a specific object + /// parent(s) have been expanded). This can be used with + /// and to scroll to a specific object. /// /// Uses the Equals method and returns the first index at which the object is found - /// or -1 if it is not found - /// An object that appears in your tree and is currently exposed + /// or -1 if it is not found. + /// An object that appears in your tree and is currently exposed. /// The index the object was found at or -1 if it is not currently revealed or - /// not in the tree at all + /// not in the tree at all. public int GetScrollOffsetOf (T o) { var map = BuildLineMap (); @@ -496,11 +492,11 @@ namespace Terminal.Gui { } /// - /// Returns the maximum width line in the tree including prefix and expansion symbols + /// Returns the maximum width line in the tree including prefix and expansion symbols. /// /// True to consider only rows currently visible (based on window - /// bounds and . False to calculate the width of - /// every exposed branch in the tree + /// bounds and . False to calculate the width of + /// every exposed branch in the tree. /// public int GetContentWidth (bool visible) { @@ -531,7 +527,7 @@ namespace Terminal.Gui { /// /// Calculates all currently visible/expanded branches (including leafs) and outputs them - /// by index from the top of the screen + /// by index from the top of the screen. /// /// Index 0 of the returned array is the first item that should be visible in the /// top of the control, index 1 is the next etc. @@ -548,7 +544,11 @@ namespace Terminal.Gui { toReturn.AddRange (AddToLineMap (root)); } - return cachedLineMap = new ReadOnlyCollection> (toReturn); + cachedLineMap = new ReadOnlyCollection> (toReturn); + + // Update the collection used for search-typing + KeystrokeNavigator.Collection = cachedLineMap.Select (b => AspectGetter (b.Model)).ToArray (); + return cachedLineMap; } private IEnumerable> AddToLineMap (Branch currentBranch) @@ -556,7 +556,6 @@ namespace Terminal.Gui { yield return currentBranch; if (currentBranch.IsExpanded) { - foreach (var subBranch in currentBranch.ChildBranches.Values) { foreach (var sub in AddToLineMap (subBranch)) { yield return sub; @@ -565,6 +564,12 @@ namespace Terminal.Gui { } } + /// + /// Gets the that searches the collection as + /// the user types. + /// + public CollectionNavigator KeystrokeNavigator { get; private set; } = new CollectionNavigator (); + /// public override bool ProcessKey (KeyEvent keyEvent) { @@ -572,21 +577,33 @@ namespace Terminal.Gui { return false; } - // if it is a single character pressed without any control keys - if (keyEvent.KeyValue > 0 && keyEvent.KeyValue < 0xFFFF) { - - if (char.IsLetterOrDigit ((char)keyEvent.KeyValue) && AllowLetterBasedNavigation && !keyEvent.IsShift && !keyEvent.IsAlt && !keyEvent.IsCtrl) { - AdjustSelectionToNextItemBeginningWith ((char)keyEvent.KeyValue); - return true; - } - } - try { + // First of all deal with any registered keybindings var result = InvokeKeybindings (keyEvent); - if (result != null) + if (result != null) { return (bool)result; - } finally { + } + // If not a keybinding, is the key a searchable key press? + if (CollectionNavigator.IsCompatibleKey (keyEvent) && AllowLetterBasedNavigation) { + IReadOnlyCollection> map; + + // If there has been a call to InvalidateMap since the last time + // we need a new one to reflect the new exposed tree state + map = BuildLineMap (); + + // Find the current selected object within the tree + var current = map.IndexOf (b => b.Model == SelectedObject); + var newIndex = KeystrokeNavigator?.GetNextMatchingItem (current, (char)keyEvent.KeyValue); + + if (newIndex is int && newIndex != -1) { + SelectedObject = map.ElementAt ((int)newIndex).Model; + EnsureVisible (selectedObject); + SetNeedsDisplay (); + return true; + } + } + } finally { PositionCursor (); } @@ -597,7 +614,7 @@ namespace Terminal.Gui { /// /// Triggers the event with the . /// - /// This method also ensures that the selected object is visible + /// This method also ensures that the selected object is visible. /// public void ActivateSelectedObjectIfAny () { @@ -622,7 +639,7 @@ namespace Terminal.Gui { /// /// /// - public int? GetObjectRow(T toFind) + public int? GetObjectRow (T toFind) { var idx = BuildLineMap ().IndexOf (o => o.Model.Equals (toFind)); @@ -633,11 +650,11 @@ namespace Terminal.Gui { } /// - /// Moves the to the next item that begins with - /// This method will loop back to the start of the tree if reaching the end without finding a match + /// Moves the to the next item that begins with . + /// This method will loop back to the start of the tree if reaching the end without finding a match. /// - /// The first character of the next item you want selected - /// Case sensitivity of the search + /// The first character of the next item you want selected. + /// Case sensitivity of the search. public void AdjustSelectionToNextItemBeginningWith (char character, StringComparison caseSensitivity = StringComparison.CurrentCultureIgnoreCase) { // search for next branch that begins with that letter @@ -650,7 +667,7 @@ namespace Terminal.Gui { /// /// Moves the selection up by the height of the control (1 page). /// - /// True if the navigation should add the covered nodes to the selected current selection + /// True if the navigation should add the covered nodes to the selected current selection. /// public void MovePageUp (bool expandSelection = false) { @@ -660,7 +677,7 @@ namespace Terminal.Gui { /// /// Moves the selection down by the height of the control (1 page). /// - /// True if the navigation should add the covered nodes to the selected current selection + /// True if the navigation should add the covered nodes to the selected current selection. /// public void MovePageDown (bool expandSelection = false) { @@ -668,25 +685,29 @@ namespace Terminal.Gui { } /// - /// Scrolls the view area down a single line without changing the current selection + /// Scrolls the view area down a single line without changing the current selection. /// public void ScrollDown () { - ScrollOffsetVertical++; - SetNeedsDisplay (); + if (ScrollOffsetVertical <= ContentHeight - 2) { + ScrollOffsetVertical++; + SetNeedsDisplay (); + } } /// - /// Scrolls the view area up a single line without changing the current selection + /// Scrolls the view area up a single line without changing the current selection. /// public void ScrollUp () { - ScrollOffsetVertical--; - SetNeedsDisplay (); + if (scrollOffsetVertical > 0) { + ScrollOffsetVertical--; + SetNeedsDisplay (); + } } /// - /// Raises the event + /// Raises the event. /// /// protected virtual void OnObjectActivated (ObjectActivatedEventArgs e) @@ -695,15 +716,15 @@ namespace Terminal.Gui { } /// - /// Returns the object in the tree list that is currently visible - /// at the provided row. Returns null if no object is at that location. + /// Returns the object in the tree list that is currently visible. + /// at the provided row. Returns null if no object is at that location. /// /// /// If you have screen coordinates then use /// to translate these into the client area of the . /// - /// The row of the of the - /// The object currently displayed on this row or null + /// The row of the of the . + /// The object currently displayed on this row or null. public T GetObjectOnRow (int row) { return HitTest (row)?.Model; @@ -728,7 +749,6 @@ namespace Terminal.Gui { SetFocus (); } - if (me.Flags == MouseFlags.WheeledDown) { ScrollDown (); @@ -784,7 +804,6 @@ namespace Terminal.Gui { multiSelectedRegions.Clear (); } } else { - // It is a first click somewhere in the current line that doesn't look like an expansion/collapse attempt SelectedObject = clickedBranch.Model; multiSelectedRegions.Clear (); @@ -814,16 +833,15 @@ namespace Terminal.Gui { // mouse event is handled. return true; } - return false; } /// /// Returns the branch at the given client - /// coordinate e.g. following a click event + /// coordinate e.g. following a click event. /// - /// Client Y position in the controls bounds - /// The clicked branch or null if outside of tree region + /// Client Y position in the controls bounds. + /// The clicked branch or null if outside of tree region. private Branch HitTest (int y) { var map = BuildLineMap (); @@ -840,7 +858,7 @@ namespace Terminal.Gui { } /// - /// Positions the cursor at the start of the selected objects line (if visible) + /// Positions the cursor at the start of the selected objects line (if visible). /// public override void PositionCursor () { @@ -861,11 +879,10 @@ namespace Terminal.Gui { } } - /// - /// Determines systems behaviour when the left arrow key is pressed. Default behaviour is + /// Determines systems behaviour when the left arrow key is pressed. Default behaviour is /// to collapse the current tree node if possible otherwise changes selection to current - /// branches parent + /// branches parent. /// protected virtual void CursorLeft (bool ctrl) { @@ -889,7 +906,7 @@ namespace Terminal.Gui { /// /// Changes the to the first root object and resets - /// the to 0 + /// the to 0. /// public void GoToFirst () { @@ -901,7 +918,7 @@ namespace Terminal.Gui { /// /// Changes the to the last object in the tree and scrolls so - /// that it is visible + /// that it is visible. /// public void GoToEnd () { @@ -914,8 +931,8 @@ namespace Terminal.Gui { /// /// Changes the to and scrolls to ensure - /// it is visible. Has no effect if is not exposed in the tree (e.g. - /// its parents are collapsed) + /// it is visible. Has no effect if is not exposed in the tree (e.g. + /// its parents are collapsed). /// /// public void GoTo (T toSelect) @@ -930,14 +947,14 @@ namespace Terminal.Gui { } /// - /// The number of screen lines to move the currently selected object by. Supports negative - /// . Each branch occupies 1 line on screen + /// The number of screen lines to move the currently selected object by. Supports negative values. + /// . Each branch occupies 1 line on screen. /// /// If nothing is currently selected or the selected object is no longer in the tree - /// then the first object in the tree is selected instead + /// then the first object in the tree is selected instead. /// Positive to move the selection down the screen, negative to move it up /// True to expand the selection (assuming - /// is enabled). False to replace + /// is enabled). False to replace. public void AdjustSelection (int offset, bool expandSelection = false) { // if it is not a shift click or we don't allow multi select @@ -953,7 +970,6 @@ namespace Terminal.Gui { var idx = map.IndexOf (b => b.Model.Equals (SelectedObject)); if (idx == -1) { - // The current selection has disapeared! SelectedObject = roots.Keys.FirstOrDefault (); } else { @@ -977,14 +993,12 @@ namespace Terminal.Gui { EnsureVisible (SelectedObject); } - } - SetNeedsDisplay (); } /// - /// Moves the selection to the first child in the currently selected level + /// Moves the selection to the first child in the currently selected level. /// public void AdjustSelectionToBranchStart () { @@ -1024,7 +1038,7 @@ namespace Terminal.Gui { } /// - /// Moves the selection to the last child in the currently selected level + /// Moves the selection to the last child in the currently selected level. /// public void AdjustSelectionToBranchEnd () { @@ -1058,13 +1072,12 @@ namespace Terminal.Gui { currentBranch = next; next = map.ElementAt (currentIdx); } - GoToEnd (); } /// - /// Sets the selection to the next branch that matches the + /// Sets the selection to the next branch that matches the . /// /// private void AdjustSelectionToNext (Func, bool> predicate) @@ -1102,7 +1115,7 @@ namespace Terminal.Gui { /// /// Adjusts the to ensure the given - /// is visible. Has no effect if already visible + /// is visible. Has no effect if already visible. /// public void EnsureVisible (T model) { @@ -1129,7 +1142,7 @@ namespace Terminal.Gui { } /// - /// Expands the currently + /// Expands the currently . /// public void Expand () { @@ -1138,9 +1151,9 @@ namespace Terminal.Gui { /// /// Expands the supplied object if it is contained in the tree (either as a root object or - /// as an exposed branch object) + /// as an exposed branch object). /// - /// The object to expand + /// The object to expand. public void Expand (T toExpand) { if (toExpand == null) { @@ -1153,9 +1166,9 @@ namespace Terminal.Gui { } /// - /// Expands the supplied object and all child objects + /// Expands the supplied object and all child objects. /// - /// The object to expand + /// The object to expand. public void ExpandAll (T toExpand) { if (toExpand == null) { @@ -1168,7 +1181,7 @@ namespace Terminal.Gui { } /// /// Fully expands all nodes in the tree, if the tree is very big and built dynamically this - /// may take a while (e.g. for file system) + /// may take a while (e.g. for file system). /// public void ExpandAll () { @@ -1181,7 +1194,7 @@ namespace Terminal.Gui { } /// /// Returns true if the given object is exposed in the tree and can be - /// expanded otherwise false + /// expanded otherwise false. /// /// /// @@ -1192,7 +1205,7 @@ namespace Terminal.Gui { /// /// Returns true if the given object is exposed in the tree and - /// expanded otherwise false + /// expanded otherwise false. /// /// /// @@ -1210,26 +1223,26 @@ namespace Terminal.Gui { } /// - /// Collapses the supplied object if it is currently expanded + /// Collapses the supplied object if it is currently expanded . /// - /// The object to collapse + /// The object to collapse. public void Collapse (T toCollapse) { CollapseImpl (toCollapse, false); } /// - /// Collapses the supplied object if it is currently expanded. Also collapses all children - /// branches (this will only become apparent when/if the user expands it again) + /// Collapses the supplied object if it is currently expanded. Also collapses all children + /// branches (this will only become apparent when/if the user expands it again). /// - /// The object to collapse + /// The object to collapse. public void CollapseAll (T toCollapse) { CollapseImpl (toCollapse, true); } /// - /// Collapses all root nodes in the tree + /// Collapses all root nodes in the tree. /// public void CollapseAll () { @@ -1242,19 +1255,17 @@ namespace Terminal.Gui { } /// - /// Implementation of and . Performs - /// operation and updates selection if disapeared + /// Implementation of and . Performs + /// operation and updates selection if disapeared. /// /// /// protected void CollapseImpl (T toCollapse, bool all) { - if (toCollapse == null) { return; } - var branch = ObjectToBranch (toCollapse); // Nothing to collapse @@ -1287,12 +1298,12 @@ namespace Terminal.Gui { /// /// Returns the corresponding in the tree for - /// . This will not work for objects hidden - /// by their parent being collapsed + /// . This will not work for objects hidden + /// by their parent being collapsed. /// /// /// The branch for or null if it is not currently - /// exposed in the tree + /// exposed in the tree. private Branch ObjectToBranch (T toFind) { return BuildLineMap ().FirstOrDefault (o => o.Model.Equals (toFind)); @@ -1300,7 +1311,7 @@ namespace Terminal.Gui { /// /// Returns true if the is either the - /// or part of a + /// or part of a . /// /// /// @@ -1335,7 +1346,7 @@ namespace Terminal.Gui { /// /// Selects all objects in the tree when is enabled otherwise - /// does nothing + /// does nothing. /// public void SelectAll () { @@ -1357,9 +1368,8 @@ namespace Terminal.Gui { OnSelectionChanged (new SelectionChangedEventArgs (this, SelectedObject, SelectedObject)); } - /// - /// Raises the SelectionChanged event + /// Raises the SelectionChanged event. /// /// protected virtual void OnSelectionChanged (SelectionChangedEventArgs e) @@ -1401,5 +1411,4 @@ namespace Terminal.Gui { return included.Contains (model); } } - } \ No newline at end of file diff --git a/Terminal.Gui/Windows/Dialog.cs b/Terminal.Gui/Windows/Dialog.cs index c4d159eac..e4b5f3361 100644 --- a/Terminal.Gui/Windows/Dialog.cs +++ b/Terminal.Gui/Windows/Dialog.cs @@ -20,7 +20,7 @@ namespace Terminal.Gui { /// or buttons added to the dialog calls . /// public class Dialog : Window { - List