diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 4488bb645..1542752d7 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -120,7 +120,7 @@ jobs: matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] - timeout-minutes: 15 + timeout-minutes: 60 steps: - name: Checkout code @@ -154,35 +154,81 @@ jobs: shell: bash run: echo "VSTEST_DUMP_PATH=logs/UnitTestsParallelizable/${{ runner.os }}/" >> $GITHUB_ENV - - name: Run UnitTestsParallelizable + - name: Run UnitTestsParallelizable (10 iterations with varying parallelization) shell: bash run: | - if [ "${{ runner.os }}" == "Linux" ]; then - # Run with coverage on Linux only - dotnet test Tests/UnitTestsParallelizable \ - --no-build \ - --verbosity normal \ - --collect:"XPlat Code Coverage" \ - --settings Tests/UnitTests/runsettings.coverage.xml \ - --diag:logs/UnitTestsParallelizable/${{ runner.os }}/logs.txt \ - --blame \ - --blame-crash \ - --blame-hang \ - --blame-hang-timeout 60s \ - --blame-crash-collect-always - else - # Run without coverage on Windows/macOS for speed - dotnet test Tests/UnitTestsParallelizable \ - --no-build \ - --verbosity normal \ - --settings Tests/UnitTestsParallelizable/runsettings.xml \ - --diag:logs/UnitTestsParallelizable/${{ runner.os }}/logs.txt \ - --blame \ - --blame-crash \ - --blame-hang \ - --blame-hang-timeout 60s \ - --blame-crash-collect-always - fi + # Run tests 10 times with different parallelization settings to expose concurrency issues + for RUN in {1..10}; do + echo "============================================" + echo "Starting test run $RUN of 10" + echo "============================================" + + # Use a combination of run number and timestamp to create different execution patterns + SEED=$((1000 + $RUN + $(date +%s) % 1000)) + echo "Using randomization seed: $SEED" + + # Vary the xUnit parallelization based on run number to expose race conditions + # Runs 1-3: Default parallelization (2x CPU cores) + # Runs 4-6: Max parallelization (unlimited) + # Runs 7-9: Single threaded (1) + # Run 10: Random (1-4 threads) + if [ $RUN -le 3 ]; then + XUNIT_MAX_PARALLEL_THREADS="2x" + echo "Run $RUN: Using default parallelization (2x)" + elif [ $RUN -le 6 ]; then + XUNIT_MAX_PARALLEL_THREADS="unlimited" + echo "Run $RUN: Using maximum parallelization (unlimited)" + elif [ $RUN -le 9 ]; then + XUNIT_MAX_PARALLEL_THREADS="1" + echo "Run $RUN: Using single-threaded execution" + else + # Random parallelization based on seed + PROC_COUNT=$(( ($SEED % 4) + 1 )) + XUNIT_MAX_PARALLEL_THREADS="$PROC_COUNT" + echo "Run $RUN: Using random parallelization with $PROC_COUNT threads" + fi + + # Run tests with or without coverage based on OS and run number + if [ "${{ runner.os }}" == "Linux" ] && [ $RUN -eq 1 ]; then + echo "Run $RUN: Running with coverage collection" + dotnet test Tests/UnitTestsParallelizable \ + --no-build \ + --verbosity normal \ + --collect:"XPlat Code Coverage" \ + --settings Tests/UnitTests/runsettings.coverage.xml \ + --diag:logs/UnitTestsParallelizable/${{ runner.os }}/run${RUN}-logs.txt \ + --blame \ + --blame-crash \ + --blame-hang \ + --blame-hang-timeout 60s \ + --blame-crash-collect-always \ + -- xUnit.MaxParallelThreads=${XUNIT_MAX_PARALLEL_THREADS} + else + dotnet test Tests/UnitTestsParallelizable \ + --no-build \ + --verbosity normal \ + --settings Tests/UnitTestsParallelizable/runsettings.xml \ + --diag:logs/UnitTestsParallelizable/${{ runner.os }}/run${RUN}-logs.txt \ + --blame \ + --blame-crash \ + --blame-hang \ + --blame-hang-timeout 60s \ + --blame-crash-collect-always \ + -- xUnit.MaxParallelThreads=${XUNIT_MAX_PARALLEL_THREADS} + fi + + if [ $? -ne 0 ]; then + echo "ERROR: Test run $RUN failed!" + exit 1 + fi + + echo "Test run $RUN completed successfully" + echo "" + done + + echo "============================================" + echo "All 10 test runs completed successfully!" + echo "============================================" - name: Upload UnitTestsParallelizable Logs if: always() diff --git a/.gitignore b/.gitignore index cdec09ec2..79eff65ea 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,4 @@ log.* !/Tests/coverage/.gitkeep # keep folder in repo /Tests/report/ *.cobertura.xml +/docfx/docs/migratingfromv1.md diff --git a/Examples/Example/Example.cs b/Examples/Example/Example.cs index 82cd60d6e..f6c13eb6b 100644 --- a/Examples/Example/Example.cs +++ b/Examples/Example/Example.cs @@ -77,13 +77,13 @@ public class ExampleWindow : Window { if (userNameText.Text == "admin" && passwordText.Text == "password") { - MessageBox.Query ("Logging In", "Login Successful", "Ok"); + MessageBox.Query (App, "Logging In", "Login Successful", "Ok"); UserName = userNameText.Text; Application.RequestStop (); } else { - MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok"); + MessageBox.ErrorQuery (App, "Logging In", "Incorrect username or password", "Ok"); } // When Accepting is handled, set e.Handled to true to prevent further processing. e.Handled = true; diff --git a/Examples/NativeAot/Program.cs b/Examples/NativeAot/Program.cs index 3de9bfeec..501adb2ed 100644 --- a/Examples/NativeAot/Program.cs +++ b/Examples/NativeAot/Program.cs @@ -101,13 +101,13 @@ public class ExampleWindow : Window { if (userNameText.Text == "admin" && passwordText.Text == "password") { - MessageBox.Query ("Logging In", "Login Successful", "Ok"); + MessageBox.Query (App, "Logging In", "Login Successful", "Ok"); UserName = userNameText.Text; Application.RequestStop (); } else { - MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok"); + MessageBox.ErrorQuery (App, "Logging In", "Incorrect username or password", "Ok"); } // Anytime Accepting is handled, make sure to set e.Handled to true. e.Handled = true; diff --git a/Examples/RunnableWrapperExample/Program.cs b/Examples/RunnableWrapperExample/Program.cs index 41a2df32b..8dadab75e 100644 --- a/Examples/RunnableWrapperExample/Program.cs +++ b/Examples/RunnableWrapperExample/Program.cs @@ -13,41 +13,42 @@ var textField = new TextField { Width = 40, Text = "Default text" }; textField.Title = "Enter your name"; textField.BorderStyle = LineStyle.Single; -var textRunnable = textField.AsRunnable (tf => tf.Text); +RunnableWrapper textRunnable = textField.AsRunnable (tf => tf.Text); app.Run (textRunnable); if (textRunnable.Result is { } name) { - MessageBox.Query ("Result", $"You entered: {name}", "OK"); + MessageBox.Query (app, "Result", $"You entered: {name}", "OK"); } else { - MessageBox.Query ("Result", "Canceled", "OK"); + MessageBox.Query (app, "Result", "Canceled", "OK"); } + textRunnable.Dispose (); // Example 2: Use IApplication.RunView() for one-liner -var selectedColor = app.RunView ( - new ColorPicker - { - Title = "Pick a Color", - BorderStyle = LineStyle.Single - }, - cp => cp.SelectedColor); +Color selectedColor = app.RunView ( + new ColorPicker + { + Title = "Pick a Color", + BorderStyle = LineStyle.Single + }, + cp => cp.SelectedColor); -MessageBox.Query ("Result", $"Selected color: {selectedColor}", "OK"); +MessageBox.Query (app, "Result", $"Selected color: {selectedColor}", "OK"); // Example 3: FlagSelector with typed enum result -var flagSelector = new FlagSelector +FlagSelector flagSelector = new() { Title = "Choose Styles", BorderStyle = LineStyle.Single }; -var flagsRunnable = flagSelector.AsRunnable (fs => fs.Value); +RunnableWrapper, SelectorStyles?> flagsRunnable = flagSelector.AsRunnable (fs => fs.Value); app.Run (flagsRunnable); -MessageBox.Query ("Result", $"Selected styles: {flagsRunnable.Result}", "OK"); +MessageBox.Query (app, "Result", $"Selected styles: {flagsRunnable.Result}", "OK"); flagsRunnable.Dispose (); // Example 4: Any View without result extraction @@ -58,26 +59,28 @@ var label = new Label Y = Pos.Center () }; -var labelRunnable = label.AsRunnable (); +RunnableWrapper labelRunnable = label.AsRunnable (); app.Run (labelRunnable); // Can still access the wrapped view -MessageBox.Query ("Result", $"Label text was: {labelRunnable.WrappedView.Text}", "OK"); +MessageBox.Query (app, "Result", $"Label text was: {labelRunnable.WrappedView.Text}", "OK"); labelRunnable.Dispose (); // Example 5: Complex custom View made runnable -var formView = CreateCustomForm (); -var formRunnable = formView.AsRunnable (ExtractFormData); +View formView = CreateCustomForm (); +RunnableWrapper formRunnable = formView.AsRunnable (ExtractFormData); app.Run (formRunnable); if (formRunnable.Result is { } formData) { MessageBox.Query ( - "Form Results", - $"Name: {formData.Name}\nAge: {formData.Age}\nAgreed: {formData.Agreed}", - "OK"); + app, + "Form Results", + $"Name: {formData.Name}\nAge: {formData.Age}\nAgreed: {formData.Agreed}", + "OK"); } + formRunnable.Dispose (); app.Shutdown (); @@ -126,10 +129,10 @@ View CreateCustomForm () }; okButton.Accepting += (s, e) => - { - form.App?.RequestStop (); - e.Handled = true; - }; + { + form.App?.RequestStop (); + e.Handled = true; + }; form.Add (new Label { Text = "Name:", X = 2, Y = 1 }); form.Add (nameField); @@ -148,7 +151,7 @@ FormData ExtractFormData (View form) var ageField = form.SubViews.FirstOrDefault (v => v.Id == "ageField") as TextField; var agreeCheckbox = form.SubViews.FirstOrDefault (v => v.Id == "agreeCheckbox") as CheckBox; - return new FormData + return new() { Name = nameField?.Text ?? string.Empty, Age = int.TryParse (ageField?.Text, out int age) ? age : 0, @@ -157,7 +160,7 @@ FormData ExtractFormData (View form) } // Result type for custom form -record FormData +internal record FormData { public string Name { get; init; } = string.Empty; public int Age { get; init; } diff --git a/Examples/SelfContained/Program.cs b/Examples/SelfContained/Program.cs index 02109bf3a..aa226273b 100644 --- a/Examples/SelfContained/Program.cs +++ b/Examples/SelfContained/Program.cs @@ -100,13 +100,13 @@ public class ExampleWindow : Window { if (userNameText.Text == "admin" && passwordText.Text == "password") { - MessageBox.Query ("Logging In", "Login Successful", "Ok"); + MessageBox.Query (App, "Logging In", "Login Successful", "Ok"); UserName = userNameText.Text; Application.RequestStop (); } else { - MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok"); + MessageBox.ErrorQuery (App, "Logging In", "Incorrect username or password", "Ok"); } // When Accepting is handled, set e.Handled to true to prevent further processing. e.Handled = true; diff --git a/Examples/UICatalog/Scenario.cs b/Examples/UICatalog/Scenario.cs index 4d9f0c759..d52308b84 100644 --- a/Examples/UICatalog/Scenario.cs +++ b/Examples/UICatalog/Scenario.cs @@ -67,7 +67,7 @@ namespace UICatalog; /// }; /// /// var button = new Button { X = Pos.Center (), Y = Pos.Center (), Text = "Press me!" }; -/// button.Accept += (s, e) => MessageBox.ErrorQuery ("Error", "You pressed the button!", "Ok"); +/// button.Accept += (s, e) => MessageBox.ErrorQuery (App, "Error", "You pressed the button!", "Ok"); /// appWindow.Add (button); /// /// // Run - Start the application. @@ -210,12 +210,12 @@ public class Scenario : IDisposable void OnClearedContents (object? sender, EventArgs args) => BenchmarkResults.ClearedContentCount++; } - private void OnApplicationOnIteration (object? s, IterationEventArgs a) + private void OnApplicationOnIteration (object? s, EventArgs a) { BenchmarkResults.IterationCount++; if (BenchmarkResults.IterationCount > BENCHMARK_MAX_NATURAL_ITERATIONS + (_demoKeys!.Count * BENCHMARK_KEY_PACING)) { - Application.RequestStop (); + a.Value?.RequestStop (); } } diff --git a/Examples/UICatalog/Scenarios/Adornments.cs b/Examples/UICatalog/Scenarios/Adornments.cs index 938d23a53..6dd491f65 100644 --- a/Examples/UICatalog/Scenarios/Adornments.cs +++ b/Examples/UICatalog/Scenarios/Adornments.cs @@ -11,7 +11,7 @@ public class Adornments : Scenario { Application.Init (); - Window app = new () + Window appWindow = new () { Title = GetQuitKeyAndName (), BorderStyle = LineStyle.None @@ -28,7 +28,7 @@ public class Adornments : Scenario editor.Border!.Thickness = new (1, 2, 1, 1); - app.Add (editor); + appWindow.Add (editor); var window = new Window { @@ -38,7 +38,7 @@ public class Adornments : Scenario Width = Dim.Fill (Dim.Func (_ => editor.Frame.Width)), Height = Dim.Fill () }; - app.Add (window); + appWindow.Add (window); var tf1 = new TextField { Width = 10, Text = "TextField" }; var color = new ColorPicker16 { Title = "BG", BoxHeight = 1, BoxWidth = 1, X = Pos.AnchorEnd () }; @@ -60,7 +60,7 @@ public class Adornments : Scenario var button = new Button { X = Pos.Center (), Y = Pos.Center (), Text = "Press me!" }; button.Accepting += (s, e) => - MessageBox.Query (20, 7, "Hi", $"Am I a {window.GetType ().Name}?", "Yes", "No"); + MessageBox.Query (appWindow.App, 20, 7, "Hi", $"Am I a {window.GetType ().Name}?", "Yes", "No"); var label = new TextView { @@ -121,7 +121,7 @@ public class Adornments : Scenario Text = "text (Y = 1)", CanFocus = true }; - textFieldInPadding.Accepting += (s, e) => MessageBox.Query (20, 7, "TextField", textFieldInPadding.Text, "Ok"); + textFieldInPadding.Accepting += (s, e) => MessageBox.Query (appWindow.App, 20, 7, "TextField", textFieldInPadding.Text, "Ok"); window.Padding.Add (textFieldInPadding); var btnButtonInPadding = new Button @@ -132,7 +132,7 @@ public class Adornments : Scenario CanFocus = true, HighlightStates = MouseState.None, }; - btnButtonInPadding.Accepting += (s, e) => MessageBox.Query (20, 7, "Hi", "Button in Padding Pressed!", "Ok"); + btnButtonInPadding.Accepting += (s, e) => MessageBox.Query (appWindow.App, 20, 7, "Hi", "Button in Padding Pressed!", "Ok"); btnButtonInPadding.BorderStyle = LineStyle.Dashed; btnButtonInPadding.Border!.Thickness = new (1, 1, 1, 1); window.Padding.Add (btnButtonInPadding); @@ -155,8 +155,8 @@ public class Adornments : Scenario editor.AutoSelectSuperView = window; editor.AutoSelectAdornments = true; - Application.Run (app); - app.Dispose (); + Application.Run (appWindow); + appWindow.Dispose (); Application.Shutdown (); } diff --git a/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs b/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs index 7d1b36020..a35453970 100644 --- a/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs +++ b/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs @@ -78,7 +78,7 @@ public class AnimationScenario : Scenario if (!f.Exists) { Debug.WriteLine ($"Could not find {f.FullName}"); - MessageBox.ErrorQuery ("Could not find gif", $"Could not find\n{f.FullName}", "Ok"); + MessageBox.ErrorQuery (_imageView?.App, "Could not find gif", $"Could not find\n{f.FullName}", "Ok"); return; } diff --git a/Examples/UICatalog/Scenarios/Bars.cs b/Examples/UICatalog/Scenarios/Bars.cs index 669f4e9c0..688af56cf 100644 --- a/Examples/UICatalog/Scenarios/Bars.cs +++ b/Examples/UICatalog/Scenarios/Bars.cs @@ -309,7 +309,7 @@ public class Bars : Scenario // new TimeSpan (0), // () => // { - // MessageBox.Query ("File", "New"); + // MessageBox.Query (App, "File", "New"); // return false; // }); @@ -331,7 +331,7 @@ public class Bars : Scenario // new TimeSpan (0), // () => // { - // MessageBox.Query ("File", "Open"); + // MessageBox.Query (App, "File", "Open"); // return false; // }); @@ -353,7 +353,7 @@ public class Bars : Scenario // new TimeSpan (0), // () => // { - // MessageBox.Query ("File", "Save"); + // MessageBox.Query (App, "File", "Save"); // return false; // }); @@ -375,7 +375,7 @@ public class Bars : Scenario // new TimeSpan (0), // () => // { - // MessageBox.Query ("File", "Save As"); + // MessageBox.Query (App, "File", "Save As"); // return false; // }); @@ -555,7 +555,7 @@ public class Bars : Scenario return; - void Button_Clicked (object sender, EventArgs e) { MessageBox.Query ("Hi", $"You clicked {sender}"); } + void Button_Clicked (object sender, EventArgs e) { MessageBox.Query ((sender as View)?.App, "Hi", $"You clicked {sender}"); } } diff --git a/Examples/UICatalog/Scenarios/Buttons.cs b/Examples/UICatalog/Scenarios/Buttons.cs index f2ea4572f..404bbf4e4 100644 --- a/Examples/UICatalog/Scenarios/Buttons.cs +++ b/Examples/UICatalog/Scenarios/Buttons.cs @@ -59,7 +59,7 @@ public class Buttons : Scenario if (e.Handled) { - MessageBox.ErrorQuery ("Error", "This button is no longer the Quit button; the Swap Default button is.", "_Ok"); + MessageBox.ErrorQuery ((s as View)?.App, "Error", "This button is no longer the Quit button; the Swap Default button is.", "_Ok"); } }; main.Add (swapButton); @@ -69,7 +69,7 @@ public class Buttons : Scenario button.Accepting += (s, e) => { string btnText = button.Text; - MessageBox.Query ("Message", $"Did you click {txt}?", "Yes", "No"); + MessageBox.Query ((s as View)?.App, "Message", $"Did you click {txt}?", "Yes", "No"); e.Handled = true; }; } @@ -112,7 +112,7 @@ public class Buttons : Scenario ); button.Accepting += (s, e) => { - MessageBox.Query ("Message", "Question?", "Yes", "No"); + MessageBox.Query ((s as View)?.App, "Message", "Question?", "Yes", "No"); e.Handled = true; }; diff --git a/Examples/UICatalog/Scenarios/ChineseUI.cs b/Examples/UICatalog/Scenarios/ChineseUI.cs index cc80c7ea9..26545dc8b 100644 --- a/Examples/UICatalog/Scenarios/ChineseUI.cs +++ b/Examples/UICatalog/Scenarios/ChineseUI.cs @@ -32,8 +32,9 @@ public class ChineseUI : Scenario btn.Accepting += (s, e) => { - int result = MessageBox.Query ( - "Confirm", + int? result = MessageBox.Query ( + (s as View)?.App, + "Confirm", "Are you sure you want to quit ui?", 0, "Yes", diff --git a/Examples/UICatalog/Scenarios/ConfigurationEditor.cs b/Examples/UICatalog/Scenarios/ConfigurationEditor.cs index c0bfcb03d..600f4b98c 100644 --- a/Examples/UICatalog/Scenarios/ConfigurationEditor.cs +++ b/Examples/UICatalog/Scenarios/ConfigurationEditor.cs @@ -153,9 +153,9 @@ public class ConfigurationEditor : Scenario continue; } - int result = MessageBox.Query ( + int? result = MessageBox.Query (editor?.App, "Save Changes", - $"Save changes to {editor.FileInfo!.Name}", + $"Save changes to {editor?.FileInfo!.Name}", "_Yes", "_No", "_Cancel" @@ -164,7 +164,7 @@ public class ConfigurationEditor : Scenario switch (result) { case 0: - editor.Save (); + editor?.Save (); break; diff --git a/Examples/UICatalog/Scenarios/ContextMenus.cs b/Examples/UICatalog/Scenarios/ContextMenus.cs index 5baaabded..541b11943 100644 --- a/Examples/UICatalog/Scenarios/ContextMenus.cs +++ b/Examples/UICatalog/Scenarios/ContextMenus.cs @@ -49,7 +49,7 @@ public class ContextMenus : Scenario var text = "Context Menu"; var width = 20; - CreateWinContextMenu (); + CreateWinContextMenu (ApplicationImpl.Instance); var label = new Label { @@ -108,7 +108,7 @@ public class ContextMenus : Scenario } } - private void CreateWinContextMenu () + private void CreateWinContextMenu (IApplication? app) { _winContextMenu = new ( [ @@ -122,7 +122,7 @@ public class ContextMenus : Scenario { Title = "_Configuration...", HelpText = "Show configuration", - Action = () => MessageBox.Query ( + Action = () => MessageBox.Query (app, 50, 10, "Configuration", @@ -140,7 +140,7 @@ public class ContextMenus : Scenario Title = "_Setup...", HelpText = "Perform setup", Action = () => MessageBox - .Query ( + .Query (app, 50, 10, "Setup", @@ -154,7 +154,7 @@ public class ContextMenus : Scenario Title = "_Maintenance...", HelpText = "Maintenance mode", Action = () => MessageBox - .Query ( + .Query (app, 50, 10, "Maintenance", diff --git a/Examples/UICatalog/Scenarios/CsvEditor.cs b/Examples/UICatalog/Scenarios/CsvEditor.cs index 5690a7509..5831b8feb 100644 --- a/Examples/UICatalog/Scenarios/CsvEditor.cs +++ b/Examples/UICatalog/Scenarios/CsvEditor.cs @@ -215,7 +215,7 @@ public class CsvEditor : Scenario _tableView.Table.Columns ); - int result = MessageBox.Query ( + int? result = MessageBox.Query (ApplicationImpl.Instance, "Column Type", "Pick a data type for the column", "Date", @@ -225,7 +225,7 @@ public class CsvEditor : Scenario "Cancel" ); - if (result <= -1 || result >= 4) + if (result is null || result >= 4) { return; } @@ -308,7 +308,7 @@ public class CsvEditor : Scenario if (_tableView.SelectedColumn == -1) { - MessageBox.ErrorQuery ("No Column", "No column selected", "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "No Column", "No column selected", "Ok"); return; } @@ -320,7 +320,7 @@ public class CsvEditor : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery ("Could not remove column", ex.Message, "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Could not remove column", ex.Message, "Ok"); } } @@ -342,7 +342,7 @@ public class CsvEditor : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery (60, 20, "Failed to set text", ex.Message, "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, 60, 20, "Failed to set text", ex.Message, "Ok"); } _tableView.Update (); @@ -388,7 +388,7 @@ public class CsvEditor : Scenario if (_tableView.SelectedColumn == -1) { - MessageBox.ErrorQuery ("No Column", "No column selected", "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "No Column", "No column selected", "Ok"); return; } @@ -413,7 +413,7 @@ public class CsvEditor : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery ("Error moving column", ex.Message, "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Error moving column", ex.Message, "Ok"); } } @@ -426,7 +426,7 @@ public class CsvEditor : Scenario if (_tableView.SelectedRow == -1) { - MessageBox.ErrorQuery ("No Rows", "No row selected", "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "No Rows", "No row selected", "Ok"); return; } @@ -446,7 +446,7 @@ public class CsvEditor : Scenario return; } - object?[] arrayItems = currentRow.ItemArray; + object? [] arrayItems = currentRow.ItemArray; _currentTable.Rows.Remove (currentRow); // Removing and Inserting the same DataRow seems to result in it loosing its values so we have to create a new instance @@ -462,7 +462,7 @@ public class CsvEditor : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery ("Error moving column", ex.Message, "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Error moving column", ex.Message, "Ok"); } } @@ -470,7 +470,7 @@ public class CsvEditor : Scenario { if (_tableView?.Table is null) { - MessageBox.ErrorQuery ("No Table Loaded", "No table has currently be opened", "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "No Table Loaded", "No table has currently be opened", "Ok"); return true; } @@ -582,7 +582,7 @@ public class CsvEditor : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery ( + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Open Failed", $"Error on line {lineNumber}{Environment.NewLine}{ex.Message}", "Ok" @@ -612,7 +612,7 @@ public class CsvEditor : Scenario { if (_tableView?.Table is null || string.IsNullOrWhiteSpace (_currentFile) || _currentTable is null) { - MessageBox.ErrorQuery ("No file loaded", "No file is currently loaded", "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "No file loaded", "No file is currently loaded", "Ok"); return; } @@ -674,7 +674,7 @@ public class CsvEditor : Scenario if (col.DataType == typeof (string)) { - MessageBox.ErrorQuery ( + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Cannot Format Column", "String columns cannot be Formatted, try adding a new column to the table with a date/numerical Type", "Ok" @@ -711,7 +711,7 @@ public class CsvEditor : Scenario if (_tableView.SelectedColumn == -1) { - MessageBox.ErrorQuery ("No Column", "No column selected", "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "No Column", "No column selected", "Ok"); return; } diff --git a/Examples/UICatalog/Scenarios/Dialogs.cs b/Examples/UICatalog/Scenarios/Dialogs.cs index e7fd1ac77..8e8a6ec99 100644 --- a/Examples/UICatalog/Scenarios/Dialogs.cs +++ b/Examples/UICatalog/Scenarios/Dialogs.cs @@ -266,7 +266,7 @@ public class Dialogs : Scenario { Title = titleEdit.Text, Text = "Dialog Text", - ButtonAlignment = (Alignment)Enum.Parse (typeof (Alignment), alignmentGroup.Labels! [(int)alignmentGroup.Value!.Value] [1..]), + ButtonAlignment = (Alignment)Enum.Parse (typeof (Alignment), alignmentGroup.Labels! [(int)alignmentGroup.Value!.Value] [0..]), Buttons = buttons.ToArray () }; diff --git a/Examples/UICatalog/Scenarios/DynamicStatusBar.cs b/Examples/UICatalog/Scenarios/DynamicStatusBar.cs index 73dd3b802..a0cdb48e3 100644 --- a/Examples/UICatalog/Scenarios/DynamicStatusBar.cs +++ b/Examples/UICatalog/Scenarios/DynamicStatusBar.cs @@ -79,7 +79,7 @@ public class DynamicStatusBar : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery ("Binding Error", $"Binding failed: {ex}.", "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Binding Error", $"Binding failed: {ex}.", "Ok"); } } } @@ -140,7 +140,7 @@ public class DynamicStatusBar : Scenario public TextView TextAction { get; } public TextField TextShortcut { get; } public TextField TextTitle { get; } - public Action CreateAction (DynamicStatusItem item) { return () => MessageBox.ErrorQuery (item.Title, item.Action, "Ok"); } + public Action CreateAction (DynamicStatusItem item) { return () => MessageBox.ErrorQuery (ApplicationImpl.Instance, item.Title, item.Action, "Ok"); } public void EditStatusItem (Shortcut statusItem) { @@ -184,7 +184,7 @@ public class DynamicStatusBar : Scenario { if (string.IsNullOrEmpty (TextTitle.Text)) { - MessageBox.ErrorQuery ("Invalid title", "Must enter a valid title!.", "Ok"); + MessageBox.ErrorQuery (App, "Invalid title", "Must enter a valid title!.", "Ok"); } else { @@ -200,7 +200,7 @@ public class DynamicStatusBar : Scenario TextTitle.Text = string.Empty; Application.RequestStop (); }; - var dialog = new Dialog { Title = "Enter the menu details.", Buttons = [btnOk, btnCancel], Height = Dim.Auto (DimAutoStyle.Content, 17, Application.Screen.Height) }; + var dialog = new Dialog { Title = "Enter the menu details.", Buttons = [btnOk, btnCancel], Height = Dim.Auto (DimAutoStyle.Content, 17, App?.Screen.Height) }; Width = Dim.Fill (); Height = Dim.Fill () - 2; @@ -382,7 +382,7 @@ public class DynamicStatusBar : Scenario { if (string.IsNullOrEmpty (frmStatusBarDetails.TextTitle.Text) && _currentEditStatusItem != null) { - MessageBox.ErrorQuery ("Invalid title", "Must enter a valid title!.", "Ok"); + MessageBox.ErrorQuery (App, "Invalid title", "Must enter a valid title!.", "Ok"); } else if (_currentEditStatusItem != null) { diff --git a/Examples/UICatalog/Scenarios/Editor.cs b/Examples/UICatalog/Scenarios/Editor.cs index 3b2e13813..d2eac26dc 100644 --- a/Examples/UICatalog/Scenarios/Editor.cs +++ b/Examples/UICatalog/Scenarios/Editor.cs @@ -156,7 +156,7 @@ public class Editor : Scenario new (Key.F2, "Open", Open), new (Key.F3, "Save", () => Save ()), new (Key.F4, "Save As", () => SaveAs ()), - new (Key.Empty, $"OS Clipboard IsSupported : {Clipboard.IsSupported}", null), + new (Key.Empty, $"OS Clipboard IsSupported : {Application.Clipboard!.IsSupported}", null), siCursorPosition ] ) @@ -193,7 +193,8 @@ public class Editor : Scenario Debug.Assert (_textView.IsDirty); - int r = MessageBox.ErrorQuery ( + int? r = MessageBox.ErrorQuery ( + ApplicationImpl.Instance, "Save File", $"Do you want save changes in {_appWindow.Title}?", "Yes", @@ -228,7 +229,7 @@ public class Editor : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery ("Error", ex.Message, "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Error", ex.Message, "Ok"); } } @@ -307,11 +308,11 @@ public class Editor : Scenario if (!found) { - MessageBox.Query ("Find", $"The following specified text was not found: '{_textToFind}'", "Ok"); + MessageBox.Query (ApplicationImpl.Instance, "Find", $"The following specified text was not found: '{_textToFind}'", "Ok"); } else if (gaveFullTurn) { - MessageBox.Query ( + MessageBox.Query (ApplicationImpl.Instance, "Find", $"No more occurrences were found for the following specified text: '{_textToFind}'", "Ok" @@ -887,7 +888,7 @@ public class Editor : Scenario if (_textView.ReplaceAllText (_textToFind, _matchCase, _matchWholeWord, _textToReplace)) { - MessageBox.Query ( + MessageBox.Query (ApplicationImpl.Instance, "Replace All", $"All occurrences were replaced for the following specified text: '{_textToReplace}'", "Ok" @@ -895,7 +896,7 @@ public class Editor : Scenario } else { - MessageBox.Query ( + MessageBox.Query (ApplicationImpl.Instance, "Replace All", $"None of the following specified text was found: '{_textToFind}'", "Ok" @@ -1147,7 +1148,7 @@ public class Editor : Scenario { if (File.Exists (path)) { - if (MessageBox.Query ( + if (MessageBox.Query (ApplicationImpl.Instance, "Save File", "File already exists. Overwrite any way?", "No", @@ -1186,11 +1187,11 @@ public class Editor : Scenario _originalText = Encoding.Unicode.GetBytes (_textView.Text); _saved = true; _textView.ClearHistoryChanges (); - MessageBox.Query ("Save File", "File was successfully saved.", "Ok"); + MessageBox.Query (ApplicationImpl.Instance, "Save File", "File was successfully saved.", "Ok"); } catch (Exception ex) { - MessageBox.ErrorQuery ("Error", ex.Message, "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Error", ex.Message, "Ok"); return false; } diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs index ff372ecc7..7f1f795c9 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs @@ -157,7 +157,7 @@ public class DimEditor : EditorBase } catch (Exception e) { - MessageBox.ErrorQuery ("Exception", e.Message, "Ok"); + MessageBox.ErrorQuery (App, "Exception", e.Message, "Ok"); } } } diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs index 45f0ab950..467b54756 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs @@ -160,7 +160,7 @@ public class PosEditor : EditorBase } catch (Exception e) { - MessageBox.ErrorQuery ("Exception", e.Message, "Ok"); + MessageBox.ErrorQuery (App, "Exception", e.Message, "Ok"); } } } diff --git a/Examples/UICatalog/Scenarios/FileDialogExamples.cs b/Examples/UICatalog/Scenarios/FileDialogExamples.cs index 290e4a432..fd80d82f3 100644 --- a/Examples/UICatalog/Scenarios/FileDialogExamples.cs +++ b/Examples/UICatalog/Scenarios/FileDialogExamples.cs @@ -133,7 +133,7 @@ public class FileDialogExamples : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery ("Error", ex.ToString (), "_Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Error", ex.ToString (), "_Ok"); } finally { @@ -153,7 +153,7 @@ public class FileDialogExamples : Scenario { if (File.Exists (e.Dialog.Path)) { - int result = MessageBox.Query ("Overwrite?", "File already exists", "_Yes", "_No"); + int? result = MessageBox.Query (ApplicationImpl.Instance, "Overwrite?", "File already exists", "_Yes", "_No"); e.Cancel = result == 1; } } @@ -248,7 +248,7 @@ public class FileDialogExamples : Scenario if (canceled) { - MessageBox.Query ( + MessageBox.Query (ApplicationImpl.Instance, "Canceled", "You canceled navigation and did not pick anything", "Ok" @@ -256,7 +256,7 @@ public class FileDialogExamples : Scenario } else if (_cbAllowMultipleSelection.CheckedState == CheckState.Checked) { - MessageBox.Query ( + MessageBox.Query (ApplicationImpl.Instance, "Chosen!", "You chose:" + Environment.NewLine + string.Join (Environment.NewLine, multiSelected.Select (m => m)), "Ok" @@ -264,7 +264,7 @@ public class FileDialogExamples : Scenario } else { - MessageBox.Query ( + MessageBox.Query (ApplicationImpl.Instance, "Chosen!", "You chose:" + Environment.NewLine + path, "Ok" diff --git a/Examples/UICatalog/Scenarios/Generic.cs b/Examples/UICatalog/Scenarios/Generic.cs index f0da0dd53..a8c3c7266 100644 --- a/Examples/UICatalog/Scenarios/Generic.cs +++ b/Examples/UICatalog/Scenarios/Generic.cs @@ -29,7 +29,7 @@ public sealed class Generic : Scenario { // When Accepting is handled, set e.Handled to true to prevent further processing. e.Handled = true; - MessageBox.ErrorQuery ("Error", "You pressed the button!", "_Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Error", "You pressed the button!", "_Ok"); }; appWindow.Add (button); diff --git a/Examples/UICatalog/Scenarios/HexEditor.cs b/Examples/UICatalog/Scenarios/HexEditor.cs index 45abe08ac..fdd4b5e83 100644 --- a/Examples/UICatalog/Scenarios/HexEditor.cs +++ b/Examples/UICatalog/Scenarios/HexEditor.cs @@ -181,7 +181,7 @@ public class HexEditor : Scenario } } - private void Copy () { MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "Ok"); } + private void Copy () { MessageBox.ErrorQuery (ApplicationImpl.Instance, "Not Implemented", "Functionality not yet implemented.", "Ok"); } private void CreateDemoFile (string fileName) { @@ -208,7 +208,7 @@ public class HexEditor : Scenario ms.Close (); } - private void Cut () { MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "Ok"); } + private void Cut () { MessageBox.ErrorQuery (ApplicationImpl.Instance, "Not Implemented", "Functionality not yet implemented.", "Ok"); } private Stream LoadFile () { @@ -216,7 +216,7 @@ public class HexEditor : Scenario if (!_saved && _hexView!.Edits.Count > 0 && _hexView.Source is {}) { - if (MessageBox.ErrorQuery ( + if (MessageBox.ErrorQuery (ApplicationImpl.Instance, "Save", "The changes were not saved. Want to open without saving?", "_Yes", @@ -267,7 +267,7 @@ public class HexEditor : Scenario d.Dispose (); } - private void Paste () { MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "_Ok"); } + private void Paste () { MessageBox.ErrorQuery (ApplicationImpl.Instance, "Not Implemented", "Functionality not yet implemented.", "_Ok"); } private void Quit () { Application.RequestStop (); } private void Save () diff --git a/Examples/UICatalog/Scenarios/Images.cs b/Examples/UICatalog/Scenarios/Images.cs index a9854092d..5791166cb 100644 --- a/Examples/UICatalog/Scenarios/Images.cs +++ b/Examples/UICatalog/Scenarios/Images.cs @@ -183,7 +183,7 @@ public class Images : Scenario if (!_sixelSupportResult.SupportsTransparency) { - if (MessageBox.Query ( + if (MessageBox.Query (ApplicationImpl.Instance, "Transparency Not Supported", "It looks like your terminal does not support transparent sixel backgrounds. Do you want to try anyway?", "Yes", @@ -288,7 +288,7 @@ public class Images : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery ("Could not open file", ex.Message, "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Could not open file", ex.Message, "Ok"); return; } @@ -492,7 +492,7 @@ public class Images : Scenario { if (_imageView.FullResImage == null) { - MessageBox.Query ("No Image Loaded", "You must first open an image. Use the 'Open Image' button above.", "Ok"); + MessageBox.Query (ApplicationImpl.Instance, "No Image Loaded", "You must first open an image. Use the 'Open Image' button above.", "Ok"); return; } diff --git a/Examples/UICatalog/Scenarios/InteractiveTree.cs b/Examples/UICatalog/Scenarios/InteractiveTree.cs index a91448c10..d90af1fa4 100644 --- a/Examples/UICatalog/Scenarios/InteractiveTree.cs +++ b/Examples/UICatalog/Scenarios/InteractiveTree.cs @@ -173,7 +173,7 @@ public class InteractiveTree : Scenario if (parent is null) { - MessageBox.ErrorQuery ( + MessageBox.ErrorQuery (ApplicationImpl.Instance, "Could not delete", $"Parent of '{toDelete}' was unexpectedly null", "Ok" diff --git a/Examples/UICatalog/Scenarios/KeyBindings.cs b/Examples/UICatalog/Scenarios/KeyBindings.cs index f68e67f17..635aa6f6e 100644 --- a/Examples/UICatalog/Scenarios/KeyBindings.cs +++ b/Examples/UICatalog/Scenarios/KeyBindings.cs @@ -164,17 +164,17 @@ public class KeyBindingsDemo : View AddCommand (Command.Save, ctx => { - MessageBox.Query ($"{ctx.Command}", $"Ctx: {ctx}", buttons: "Ok"); + MessageBox.Query (ApplicationImpl.Instance, $"{ctx.Command}", $"Ctx: {ctx}", buttons: "Ok"); return true; }); AddCommand (Command.New, ctx => { - MessageBox.Query ($"{ctx.Command}", $"Ctx: {ctx}", buttons: "Ok"); + MessageBox.Query (ApplicationImpl.Instance, $"{ctx.Command}", $"Ctx: {ctx}", buttons: "Ok"); return true; }); AddCommand (Command.HotKey, ctx => { - MessageBox.Query ($"{ctx.Command}", $"Ctx: {ctx}\nCommand: {ctx.Command}", buttons: "Ok"); + MessageBox.Query (ApplicationImpl.Instance, $"{ctx.Command}", $"Ctx: {ctx}\nCommand: {ctx.Command}", buttons: "Ok"); SetFocus (); return true; }); @@ -189,7 +189,7 @@ public class KeyBindingsDemo : View { return false; } - MessageBox.Query ($"{keyCommandContext.Binding}", $"Key: {keyCommandContext.Binding.Key}\nCommand: {ctx.Command}", buttons: "Ok"); + MessageBox.Query (ApplicationImpl.Instance, $"{keyCommandContext.Binding}", $"Key: {keyCommandContext.Binding.Key}\nCommand: {ctx.Command}", buttons: "Ok"); Application.RequestStop (); return true; }); diff --git a/Examples/UICatalog/Scenarios/ListColumns.cs b/Examples/UICatalog/Scenarios/ListColumns.cs index 8ed35942e..d300b4163 100644 --- a/Examples/UICatalog/Scenarios/ListColumns.cs +++ b/Examples/UICatalog/Scenarios/ListColumns.cs @@ -336,7 +336,7 @@ public class ListColumns : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery (60, 20, "Failed to set", ex.Message, "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, 60, 20, "Failed to set", ex.Message, "Ok"); } } } diff --git a/Examples/UICatalog/Scenarios/MessageBoxes.cs b/Examples/UICatalog/Scenarios/MessageBoxes.cs index c8356a86a..fcb6488ee 100644 --- a/Examples/UICatalog/Scenarios/MessageBoxes.cs +++ b/Examples/UICatalog/Scenarios/MessageBoxes.cs @@ -251,7 +251,7 @@ public class MessageBoxes : Scenario { buttonPressedLabel.Text = $"{MessageBox.Query ( - width, + ApplicationImpl.Instance, width, height, titleEdit.Text, messageEdit.Text, @@ -263,14 +263,14 @@ public class MessageBoxes : Scenario else { buttonPressedLabel.Text = - $"{MessageBox.ErrorQuery ( - width, - height, - titleEdit.Text, - messageEdit.Text, - defaultButton, - ckbWrapMessage.CheckedState == CheckState.Checked, - btns.ToArray () + $"{MessageBox.ErrorQuery (ApplicationImpl.Instance, + width, + height, + titleEdit.Text, + messageEdit.Text, + defaultButton, + ckbWrapMessage.CheckedState == CheckState.Checked, + btns.ToArray () )}"; } } diff --git a/Examples/UICatalog/Scenarios/MultiColouredTable.cs b/Examples/UICatalog/Scenarios/MultiColouredTable.cs index 9b717d1ed..5bac4e125 100644 --- a/Examples/UICatalog/Scenarios/MultiColouredTable.cs +++ b/Examples/UICatalog/Scenarios/MultiColouredTable.cs @@ -99,7 +99,7 @@ public class MultiColouredTable : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery (60, 20, "Failed to set text", ex.Message, "Ok"); + MessageBox.ErrorQuery (ApplicationImpl.Instance, 60, 20, "Failed to set text", ex.Message, "Ok"); } _tableView.Update (); diff --git a/Examples/UICatalog/Scenarios/Navigation.cs b/Examples/UICatalog/Scenarios/Navigation.cs index 94e7dad8e..dfedfc4d1 100644 --- a/Examples/UICatalog/Scenarios/Navigation.cs +++ b/Examples/UICatalog/Scenarios/Navigation.cs @@ -59,7 +59,7 @@ public class Navigation : Scenario Y = 0, Title = $"TopButton _{GetNextHotKey ()}" }; - button.Accepting += (sender, args) => MessageBox.Query ("hi", button.Title, "_Ok"); + button.Accepting += (sender, args) => MessageBox.Query (ApplicationImpl.Instance, "hi", button.Title, "_Ok"); testFrame.Add (button); @@ -210,7 +210,7 @@ public class Navigation : Scenario return; - void OnApplicationIteration (object sender, IterationEventArgs args) + void OnApplicationIteration (object sender, EventArgs args) { if (progressBar.Fraction == 1.0) { diff --git a/Examples/UICatalog/Scenarios/Notepad.cs b/Examples/UICatalog/Scenarios/Notepad.cs index 4a2f7d2e6..6d3ac2c82 100644 --- a/Examples/UICatalog/Scenarios/Notepad.cs +++ b/Examples/UICatalog/Scenarios/Notepad.cs @@ -71,7 +71,7 @@ public class Notepad : Scenario new MenuItem { Title = "_About", - Action = () => MessageBox.Query ("Notepad", "About Notepad...", "Ok") + Action = () => MessageBox.Query (ApplicationImpl.Instance, "Notepad", "About Notepad...", "Ok") } ] ) @@ -193,15 +193,15 @@ public class Notepad : Scenario if (tab.UnsavedChanges) { - int result = MessageBox.Query ( - "Save Changes", - $"Save changes to {tab.Text.TrimEnd ('*')}", - "Yes", - "No", - "Cancel" + int? result = MessageBox.Query (ApplicationImpl.Instance, + "Save Changes", + $"Save changes to {tab.Text.TrimEnd ('*')}", + "Yes", + "No", + "Cancel" ); - if (result == -1 || result == 2) + if (result is null || result == 2) { // user cancelled return; diff --git a/Examples/UICatalog/Scenarios/RunTExample.cs b/Examples/UICatalog/Scenarios/RunTExample.cs index 6e4cfa1d7..7a66e54e5 100644 --- a/Examples/UICatalog/Scenarios/RunTExample.cs +++ b/Examples/UICatalog/Scenarios/RunTExample.cs @@ -63,12 +63,12 @@ public class RunTExample : Scenario { if (_usernameText.Text == "admin" && passwordText.Text == "password") { - MessageBox.Query ("Login Successful", $"Username: {_usernameText.Text}", "Ok"); - Application.RequestStop (); + MessageBox.Query (App, "Login Successful", $"Username: {_usernameText.Text}", "Ok"); + App?.RequestStop (); } else { - MessageBox.ErrorQuery ( + MessageBox.ErrorQuery (App, "Error Logging In", "Incorrect username or password (hint: admin/password)", "Ok" diff --git a/Examples/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs b/Examples/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs index 512a106f0..23f2e63fa 100644 --- a/Examples/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs +++ b/Examples/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs @@ -166,7 +166,7 @@ public class RuneWidthGreaterThanOne : Scenario { if (_text is { }) { - MessageBox.Query ("Say Hello 你", $"Hello {_text.Text}", "Ok"); + MessageBox.Query (ApplicationImpl.Instance, "Say Hello 你", $"Hello {_text.Text}", "Ok"); } } @@ -197,7 +197,7 @@ public class RuneWidthGreaterThanOne : Scenario { if (_text is { }) { - MessageBox.Query ("Say Hello", $"Hello {_text.Text}", "Ok"); + MessageBox.Query (ApplicationImpl.Instance, "Say Hello", $"Hello {_text.Text}", "Ok"); } } @@ -252,7 +252,7 @@ public class RuneWidthGreaterThanOne : Scenario { if (_text is { }) { - MessageBox.Query ("こんにちはと言う", $"こんにちは {_text.Text}", "Ok"); + MessageBox.Query (ApplicationImpl.Instance, "こんにちはと言う", $"こんにちは {_text.Text}", "Ok"); } } diff --git a/Examples/UICatalog/Scenarios/Shortcuts.cs b/Examples/UICatalog/Scenarios/Shortcuts.cs index 573834aa3..7af36ef92 100644 --- a/Examples/UICatalog/Scenarios/Shortcuts.cs +++ b/Examples/UICatalog/Scenarios/Shortcuts.cs @@ -566,6 +566,6 @@ public class Shortcuts : Scenario { e.Handled = true; var view = sender as View; - MessageBox.Query ("Hi", $"You clicked {view?.Text}", "_Ok"); + MessageBox.Query ((sender as View)?.App, "Hi", $"You clicked {view?.Text}", "_Ok"); } } diff --git a/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs b/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs index 5e795a9c0..e5b8c301f 100644 --- a/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs +++ b/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs @@ -224,7 +224,7 @@ public class SingleBackgroundWorker : Scenario bool Close () { - int n = MessageBox.Query ( + int? n = MessageBox.Query (App, 50, 7, "Close Window.", @@ -251,7 +251,7 @@ public class SingleBackgroundWorker : Scenario { if (Close ()) { - Application.RequestStop (); + App?.RequestStop (); } } } @@ -270,7 +270,7 @@ public class SingleBackgroundWorker : Scenario { if (Close ()) { - Application.RequestStop (); + App?.RequestStop (); } } ) @@ -304,7 +304,7 @@ public class SingleBackgroundWorker : Scenario { if (_top is { }) { - Application.Run (_top); + App?.Run (_top); _top.Dispose (); _top = null; } diff --git a/Examples/UICatalog/Scenarios/TableEditor.cs b/Examples/UICatalog/Scenarios/TableEditor.cs index 12ab5e9d8..7d130da78 100644 --- a/Examples/UICatalog/Scenarios/TableEditor.cs +++ b/Examples/UICatalog/Scenarios/TableEditor.cs @@ -1026,7 +1026,7 @@ public class TableEditor : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery (60, 20, "Failed to set text", ex.Message, "Ok"); + MessageBox.ErrorQuery ((sender as View)?.App, 60, 20, "Failed to set text", ex.Message, "Ok"); } _tableView!.Update (); @@ -1165,7 +1165,7 @@ public class TableEditor : Scenario } catch (Exception e) { - MessageBox.ErrorQuery ("Could not find local drives", e.Message, "Ok"); + MessageBox.ErrorQuery (_tableView?.App, "Could not find local drives", e.Message, "Ok"); } _tableView!.Table = source; @@ -1199,10 +1199,10 @@ public class TableEditor : Scenario ok.Accepting += (s, e) => { accepted = true; - Application.RequestStop (); + (s as View)?.App?.RequestStop (); }; var cancel = new Button { Text = "Cancel" }; - cancel.Accepting += (s, e) => { Application.RequestStop (); }; + cancel.Accepting += (s, e) => { (s as View)?.App?.RequestStop (); }; var d = new Dialog { @@ -1218,7 +1218,7 @@ public class TableEditor : Scenario d.Add (lbl, tf); tf.SetFocus (); - Application.Run (d); + _tableView.App?.Run (d); d.Dispose (); if (accepted) @@ -1229,7 +1229,7 @@ public class TableEditor : Scenario } catch (Exception ex) { - MessageBox.ErrorQuery (60, 20, "Failed to set", ex.Message, "Ok"); + MessageBox.ErrorQuery (_tableView.App, 60, 20, "Failed to set", ex.Message, "Ok"); } _tableView!.Update (); @@ -1512,7 +1512,7 @@ public class TableEditor : Scenario _checkedFileSystemInfos!.Contains, CheckOrUncheckFile ) - { UseRadioButtons = radio }; + { UseRadioButtons = radio }; } else { diff --git a/Examples/UICatalog/Scenarios/Transparent.cs b/Examples/UICatalog/Scenarios/Transparent.cs index 1b6f08df8..801372d2c 100644 --- a/Examples/UICatalog/Scenarios/Transparent.cs +++ b/Examples/UICatalog/Scenarios/Transparent.cs @@ -46,7 +46,7 @@ public sealed class Transparent : Scenario }; appButton.Accepting += (sender, args) => { - MessageBox.Query ("AppButton", "Transparency is cool!", "_Ok"); + MessageBox.Query ((sender as View)?.App, "AppButton", "Transparency is cool!", "_Ok"); args.Handled = true; }; appWindow.Add (appButton); @@ -106,7 +106,7 @@ public sealed class Transparent : Scenario }; button.Accepting += (sender, args) => { - MessageBox.Query ("Clicked!", "Button in Transparent View", "_Ok"); + MessageBox.Query (App, "Clicked!", "Button in Transparent View", "_Ok"); args.Handled = true; }; //button.Visible = false; diff --git a/Examples/UICatalog/Scenarios/ViewportSettings.cs b/Examples/UICatalog/Scenarios/ViewportSettings.cs index 4e430171c..e687a078f 100644 --- a/Examples/UICatalog/Scenarios/ViewportSettings.cs +++ b/Examples/UICatalog/Scenarios/ViewportSettings.cs @@ -169,13 +169,13 @@ public class ViewportSettings : Scenario }; charMap.Accepting += (s, e) => - MessageBox.Query (20, 7, "Hi", $"Am I a {view.GetType ().Name}?", "Yes", "No"); + MessageBox.Query ((s as View)?.App, 20, 7, "Hi", $"Am I a {view.GetType ().Name}?", "Yes", "No"); var buttonAnchored = new Button { X = Pos.AnchorEnd () - 10, Y = Pos.AnchorEnd () - 4, Text = "Bottom Rig_ht" }; - buttonAnchored.Accepting += (sender, args) => MessageBox.Query ("Hi", $"You pressed {((Button)sender)?.Text}", "_Ok"); + buttonAnchored.Accepting += (sender, args) => MessageBox.Query ((sender as View)?.App, "Hi", $"You pressed {((Button)sender)?.Text}", "_Ok"); view.Margin!.Data = "Margin"; view.Margin!.Thickness = new (0); diff --git a/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs b/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs index d2b58d847..4404f8008 100644 --- a/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs +++ b/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs @@ -16,9 +16,9 @@ public class WindowsAndFrameViews : Scenario Title = GetQuitKeyAndName () }; - static int About () + static int? About () { - return MessageBox.Query ( + return MessageBox.Query (ApplicationImpl.Instance, "About UI Catalog", "UI Catalog is a comprehensive sample library for Terminal.Gui", "Ok" @@ -99,7 +99,7 @@ public class WindowsAndFrameViews : Scenario }; pressMeButton.Accepting += (s, e) => - MessageBox.ErrorQuery (loopWin.Title, "Neat?", "Yes", "No"); + MessageBox.ErrorQuery ((s as View)?.App, loopWin.Title, "Neat?", "Yes", "No"); loopWin.Add (pressMeButton); var subWin = new Window diff --git a/Examples/UICatalog/Scenarios/WizardAsView.cs b/Examples/UICatalog/Scenarios/WizardAsView.cs index 9df72b835..67e23685f 100644 --- a/Examples/UICatalog/Scenarios/WizardAsView.cs +++ b/Examples/UICatalog/Scenarios/WizardAsView.cs @@ -21,6 +21,7 @@ public class WizardAsView : Scenario { Title = "_Restart Configuration...", Action = () => MessageBox.Query ( + ApplicationImpl.Instance, "Wizard", "Are you sure you want to reset the Wizard and start over?", "Ok", @@ -31,6 +32,7 @@ public class WizardAsView : Scenario { Title = "Re_boot Server...", Action = () => MessageBox.Query ( + ApplicationImpl.Instance, "Wizard", "Are you sure you want to reboot the server start over?", "Ok", @@ -41,6 +43,7 @@ public class WizardAsView : Scenario { Title = "_Shutdown Server...", Action = () => MessageBox.Query ( + ApplicationImpl.Instance, "Wizard", "Are you sure you want to cancel setup and shutdown?", "Ok", @@ -80,13 +83,13 @@ public class WizardAsView : Scenario wizard.Finished += (s, args) => { //args.Cancel = true; - MessageBox.Query ("Setup Wizard", "Finished", "Ok"); + MessageBox.Query ((s as View)?.App, "Setup Wizard", "Finished", "Ok"); Application.RequestStop (); }; wizard.Cancelled += (s, args) => { - int btn = MessageBox.Query ("Setup Wizard", "Are you sure you want to cancel?", "Yes", "No"); + int? btn = MessageBox.Query ((s as View)?.App, "Setup Wizard", "Are you sure you want to cancel?", "Yes", "No"); args.Cancel = btn == 1; if (btn == 0) @@ -123,7 +126,7 @@ public class WizardAsView : Scenario { secondStep.Title = "2nd Step"; - MessageBox.Query ( + MessageBox.Query ((s as View)?.App, "Wizard Scenario", "This Wizard Step's title was changed to '2nd Step'", "Ok" diff --git a/Examples/UICatalog/Scenarios/Wizards.cs b/Examples/UICatalog/Scenarios/Wizards.cs index 6263093a4..2f90c3420 100644 --- a/Examples/UICatalog/Scenarios/Wizards.cs +++ b/Examples/UICatalog/Scenarios/Wizards.cs @@ -1,13 +1,9 @@ -using System; -using System.Linq; - -namespace UICatalog.Scenarios; +namespace UICatalog.Scenarios; [ScenarioMetadata ("Wizards", "Demonstrates the Wizard class")] [ScenarioCategory ("Dialogs")] [ScenarioCategory ("Wizards")] [ScenarioCategory ("Runnable")] - public class Wizards : Scenario { public override void Main () @@ -108,267 +104,277 @@ public class Wizards : Scenario }; showWizardButton.Accepting += (s, e) => - { - try - { - var width = 0; - int.TryParse (widthEdit.Text, out width); - var height = 0; - int.TryParse (heightEdit.Text, out height); + { + try + { + var width = 0; + int.TryParse (widthEdit.Text, out width); + var height = 0; + int.TryParse (heightEdit.Text, out height); - if (width < 1 || height < 1) - { - MessageBox.ErrorQuery ( - "Nope", - "Height and width must be greater than 0 (much bigger)", - "Ok" - ); + if (width < 1 || height < 1) + { + MessageBox.ErrorQuery ( + (s as View)?.App, + "Nope", + "Height and width must be greater than 0 (much bigger)", + "Ok" + ); - return; - } + return; + } - actionLabel.Text = string.Empty; + actionLabel.Text = string.Empty; - var wizard = new Wizard { Title = titleEdit.Text, Width = width, Height = height }; + var wizard = new Wizard { Title = titleEdit.Text, Width = width, Height = height }; - wizard.MovingBack += (s, args) => - { - //args.Cancel = true; - actionLabel.Text = "Moving Back"; - }; + wizard.MovingBack += (s, args) => + { + //args.Cancel = true; + actionLabel.Text = "Moving Back"; + }; - wizard.MovingNext += (s, args) => - { - //args.Cancel = true; - actionLabel.Text = "Moving Next"; - }; + wizard.MovingNext += (s, args) => + { + //args.Cancel = true; + actionLabel.Text = "Moving Next"; + }; - wizard.Finished += (s, args) => - { - //args.Cancel = true; - actionLabel.Text = "Finished"; - }; + wizard.Finished += (s, args) => + { + //args.Cancel = true; + actionLabel.Text = "Finished"; + }; - wizard.Cancelled += (s, args) => - { - //args.Cancel = true; - actionLabel.Text = "Cancelled"; - }; - - // Add 1st step - var firstStep = new WizardStep { Title = "End User License Agreement" }; - firstStep.NextButtonText = "Accept!"; - - firstStep.HelpText = - "This is the End User License Agreement.\n\n\n\n\n\nThis is a test of the emergency broadcast system. This is a test of the emergency broadcast system.\nThis is a test of the emergency broadcast system.\n\n\nThis is a test of the emergency broadcast system.\n\nThis is a test of the emergency broadcast system.\n\n\n\nThe end of the EULA."; - - OptionSelector optionSelector = new () - { - Labels = ["_One", "_Two", "_3"] - }; - firstStep.Add (optionSelector); - - wizard.AddStep (firstStep); - - // Add 2nd step - var secondStep = new WizardStep { Title = "Second Step" }; - wizard.AddStep (secondStep); - - secondStep.HelpText = - "This is the help text for the Second Step.\n\nPress the button to change the Title.\n\nIf First Name is empty the step will prevent moving to the next step."; - - var buttonLbl = new Label { Text = "Second Step Button: ", X = 1, Y = 1 }; - - var button = new Button - { - Text = "Press Me to Rename Step", X = Pos.Right (buttonLbl), Y = Pos.Top (buttonLbl) - }; - - OptionSelector optionSelecor2 = new () - { - Labels = ["_A", "_B", "_C"], - Orientation = Orientation.Horizontal - }; - secondStep.Add (optionSelecor2); - - button.Accepting += (s, e) => - { - secondStep.Title = "2nd Step"; - - MessageBox.Query ( - "Wizard Scenario", - "This Wizard Step's title was changed to '2nd Step'" - ); - }; - secondStep.Add (buttonLbl, button); - var lbl = new Label { Text = "First Name: ", X = 1, Y = Pos.Bottom (buttonLbl) }; - - var firstNameField = - new TextField { Text = "Number", Width = 30, X = Pos.Right (lbl), Y = Pos.Top (lbl) }; - secondStep.Add (lbl, firstNameField); - lbl = new () { Text = "Last Name: ", X = 1, Y = Pos.Bottom (lbl) }; - var lastNameField = new TextField { Text = "Six", Width = 30, X = Pos.Right (lbl), Y = Pos.Top (lbl) }; - secondStep.Add (lbl, lastNameField); - - var thirdStepEnabledCeckBox = new CheckBox - { - Text = "Enable Step _3", - CheckedState = CheckState.UnChecked, - X = Pos.Left (lastNameField), - Y = Pos.Bottom (lastNameField) - }; - secondStep.Add (thirdStepEnabledCeckBox); - - // Add a frame - var frame = new FrameView - { - X = 0, - Y = Pos.Bottom (thirdStepEnabledCeckBox) + 2, - Width = Dim.Fill (), - Height = 4, - Title = "A Broken Frame (by Depeche Mode)", - TabStop = TabBehavior.NoStop - }; - frame.Add (new TextField { Text = "This is a TextField inside of the frame." }); - secondStep.Add (frame); - - wizard.StepChanging += (s, args) => + wizard.Cancelled += (s, args) => { - if (args.OldStep == secondStep && string.IsNullOrEmpty (firstNameField.Text)) - { - args.Cancel = true; - - int btn = MessageBox.ErrorQuery ( - "Second Step", - "You must enter a First Name to continue", - "Ok" - ); - } + //args.Cancel = true; + actionLabel.Text = "Cancelled"; }; - // Add 3rd (optional) step - var thirdStep = new WizardStep { Title = "Third Step (Optional)" }; - wizard.AddStep (thirdStep); + // Add 1st step + var firstStep = new WizardStep { Title = "End User License Agreement" }; + firstStep.NextButtonText = "Accept!"; - thirdStep.HelpText = - "This is step is optional (WizardStep.Enabled = false). Enable it with the checkbox in Step 2."; - var step3Label = new Label { Text = "This step is optional.", X = 0, Y = 0 }; - thirdStep.Add (step3Label); - var progLbl = new Label { Text = "Third Step ProgressBar: ", X = 1, Y = 10 }; + firstStep.HelpText = + "This is the End User License Agreement.\n\n\n\n\n\nThis is a test of the emergency broadcast system. This is a test of the emergency broadcast system.\nThis is a test of the emergency broadcast system.\n\n\nThis is a test of the emergency broadcast system.\n\nThis is a test of the emergency broadcast system.\n\n\n\nThe end of the EULA."; - var progressBar = new ProgressBar - { - X = Pos.Right (progLbl), Y = Pos.Top (progLbl), Width = 40, Fraction = 0.42F - }; - thirdStep.Add (progLbl, progressBar); - thirdStep.Enabled = thirdStepEnabledCeckBox.CheckedState == CheckState.Checked; - thirdStepEnabledCeckBox.CheckedStateChanged += (s, e) => { thirdStep.Enabled = thirdStepEnabledCeckBox.CheckedState == CheckState.Checked; }; + OptionSelector optionSelector = new () + { + Labels = ["_One", "_Two", "_3"] + }; + firstStep.Add (optionSelector); - // Add 4th step - var fourthStep = new WizardStep { Title = "Step Four" }; - wizard.AddStep (fourthStep); + wizard.AddStep (firstStep); - var someText = new TextView - { - Text = - "This step (Step Four) shows how to show/hide the Help pane. The step contains this TextView (but it's hard to tell it's a TextView because of Issue #1800).", - X = 0, - Y = 0, - Width = Dim.Fill (), - WordWrap = true, - AllowsTab = false, - SchemeName = "Base" - }; + // Add 2nd step + var secondStep = new WizardStep { Title = "Second Step" }; + wizard.AddStep (secondStep); - someText.Height = Dim.Fill ( - Dim.Func ( - v => someText.SuperView is { IsInitialized: true } - ? someText.SuperView.SubViews - .First (view => view.Y.Has (out _)) - .Frame.Height - : 1)); - var help = "This is helpful."; - fourthStep.Add (someText); + secondStep.HelpText = + "This is the help text for the Second Step.\n\nPress the button to change the Title.\n\nIf First Name is empty the step will prevent moving to the next step."; - var hideHelpBtn = new Button - { - Text = "Press me to show/hide help", - X = Pos.Center (), - Y = Pos.AnchorEnd () - }; + var buttonLbl = new Label { Text = "Second Step Button: ", X = 1, Y = 1 }; - hideHelpBtn.Accepting += (s, e) => - { - if (fourthStep.HelpText.Length > 0) + var button = new Button + { + Text = "Press Me to Rename Step", X = Pos.Right (buttonLbl), Y = Pos.Top (buttonLbl) + }; + + OptionSelector optionSelecor2 = new () + { + Labels = ["_A", "_B", "_C"], + Orientation = Orientation.Horizontal + }; + secondStep.Add (optionSelecor2); + + button.Accepting += (s, e) => + { + secondStep.Title = "2nd Step"; + + MessageBox.Query ( + (s as View)?.App, + "Wizard Scenario", + "This Wizard Step's title was changed to '2nd Step'" + ); + }; + secondStep.Add (buttonLbl, button); + var lbl = new Label { Text = "First Name: ", X = 1, Y = Pos.Bottom (buttonLbl) }; + + var firstNameField = + new TextField { Text = "Number", Width = 30, X = Pos.Right (lbl), Y = Pos.Top (lbl) }; + secondStep.Add (lbl, firstNameField); + lbl = new () { Text = "Last Name: ", X = 1, Y = Pos.Bottom (lbl) }; + var lastNameField = new TextField { Text = "Six", Width = 30, X = Pos.Right (lbl), Y = Pos.Top (lbl) }; + secondStep.Add (lbl, lastNameField); + + var thirdStepEnabledCeckBox = new CheckBox + { + Text = "Enable Step _3", + CheckedState = CheckState.UnChecked, + X = Pos.Left (lastNameField), + Y = Pos.Bottom (lastNameField) + }; + secondStep.Add (thirdStepEnabledCeckBox); + + // Add a frame + var frame = new FrameView + { + X = 0, + Y = Pos.Bottom (thirdStepEnabledCeckBox) + 2, + Width = Dim.Fill (), + Height = 4, + Title = "A Broken Frame (by Depeche Mode)", + TabStop = TabBehavior.NoStop + }; + frame.Add (new TextField { Text = "This is a TextField inside of the frame." }); + secondStep.Add (frame); + + wizard.StepChanging += (s, args) => { - fourthStep.HelpText = string.Empty; - } - else - { - fourthStep.HelpText = help; - } - }; - fourthStep.Add (hideHelpBtn); - fourthStep.NextButtonText = "_Go To Last Step"; - //var scrollBar = new ScrollBarView (someText, true); + if (args.OldStep == secondStep && string.IsNullOrEmpty (firstNameField.Text)) + { + args.Cancel = true; - //scrollBar.ChangedPosition += (s, e) => - // { - // someText.TopRow = scrollBar.Position; + int? btn = MessageBox.ErrorQuery ( + (s as View)?.App, + "Second Step", + "You must enter a First Name to continue", + "Ok" + ); + } + }; - // if (someText.TopRow != scrollBar.Position) - // { - // scrollBar.Position = someText.TopRow; - // } + // Add 3rd (optional) step + var thirdStep = new WizardStep { Title = "Third Step (Optional)" }; + wizard.AddStep (thirdStep); - // someText.SetNeedsDraw (); - // }; + thirdStep.HelpText = + "This is step is optional (WizardStep.Enabled = false). Enable it with the checkbox in Step 2."; + var step3Label = new Label { Text = "This step is optional.", X = 0, Y = 0 }; + thirdStep.Add (step3Label); + var progLbl = new Label { Text = "Third Step ProgressBar: ", X = 1, Y = 10 }; - //someText.DrawingContent += (s, e) => - // { - // scrollBar.Size = someText.Lines; - // scrollBar.Position = someText.TopRow; + var progressBar = new ProgressBar + { + X = Pos.Right (progLbl), Y = Pos.Top (progLbl), Width = 40, Fraction = 0.42F + }; + thirdStep.Add (progLbl, progressBar); + thirdStep.Enabled = thirdStepEnabledCeckBox.CheckedState == CheckState.Checked; - // if (scrollBar.OtherScrollBarView != null) - // { - // scrollBar.OtherScrollBarView.Size = someText.Maxlength; - // scrollBar.OtherScrollBarView.Position = someText.LeftColumn; - // } - // }; - //fourthStep.Add (scrollBar); + thirdStepEnabledCeckBox.CheckedStateChanged += (s, e) => + { + thirdStep.Enabled = + thirdStepEnabledCeckBox.CheckedState == CheckState.Checked; + }; - // Add last step - var lastStep = new WizardStep { Title = "The last step" }; - wizard.AddStep (lastStep); + // Add 4th step + var fourthStep = new WizardStep { Title = "Step Four" }; + wizard.AddStep (fourthStep); - lastStep.HelpText = - "The wizard is complete!\n\nPress the Finish button to continue.\n\nPressing ESC will cancel the wizard."; + var someText = new TextView + { + Text = + "This step (Step Four) shows how to show/hide the Help pane. The step contains this TextView (but it's hard to tell it's a TextView because of Issue #1800).", + X = 0, + Y = 0, + Width = Dim.Fill (), + WordWrap = true, + AllowsTab = false, + SchemeName = "Base" + }; - var finalFinalStepEnabledCeckBox = - new CheckBox { Text = "Enable _Final Final Step", CheckedState = CheckState.UnChecked, X = 0, Y = 1 }; - lastStep.Add (finalFinalStepEnabledCeckBox); + someText.Height = Dim.Fill ( + Dim.Func (v => someText.SuperView is { IsInitialized: true } + ? someText.SuperView.SubViews + .First (view => view.Y.Has (out _)) + .Frame.Height + : 1)); + var help = "This is helpful."; + fourthStep.Add (someText); - // Add an optional FINAL last step - var finalFinalStep = new WizardStep { Title = "The VERY last step" }; - wizard.AddStep (finalFinalStep); + var hideHelpBtn = new Button + { + Text = "Press me to show/hide help", + X = Pos.Center (), + Y = Pos.AnchorEnd () + }; - finalFinalStep.HelpText = - "This step only shows if it was enabled on the other last step."; - finalFinalStep.Enabled = thirdStepEnabledCeckBox.CheckedState == CheckState.Checked; + hideHelpBtn.Accepting += (s, e) => + { + if (fourthStep.HelpText.Length > 0) + { + fourthStep.HelpText = string.Empty; + } + else + { + fourthStep.HelpText = help; + } + }; + fourthStep.Add (hideHelpBtn); + fourthStep.NextButtonText = "_Go To Last Step"; - finalFinalStepEnabledCeckBox.CheckedStateChanged += (s, e) => - { - finalFinalStep.Enabled = finalFinalStepEnabledCeckBox.CheckedState == CheckState.Checked; - }; + //var scrollBar = new ScrollBarView (someText, true); - Application.Run (wizard); - wizard.Dispose (); - } - catch (FormatException) - { - actionLabel.Text = "Invalid Options"; - } - }; + //scrollBar.ChangedPosition += (s, e) => + // { + // someText.TopRow = scrollBar.Position; + + // if (someText.TopRow != scrollBar.Position) + // { + // scrollBar.Position = someText.TopRow; + // } + + // someText.SetNeedsDraw (); + // }; + + //someText.DrawingContent += (s, e) => + // { + // scrollBar.Size = someText.Lines; + // scrollBar.Position = someText.TopRow; + + // if (scrollBar.OtherScrollBarView != null) + // { + // scrollBar.OtherScrollBarView.Size = someText.Maxlength; + // scrollBar.OtherScrollBarView.Position = someText.LeftColumn; + // } + // }; + //fourthStep.Add (scrollBar); + + // Add last step + var lastStep = new WizardStep { Title = "The last step" }; + wizard.AddStep (lastStep); + + lastStep.HelpText = + "The wizard is complete!\n\nPress the Finish button to continue.\n\nPressing ESC will cancel the wizard."; + + var finalFinalStepEnabledCeckBox = + new CheckBox { Text = "Enable _Final Final Step", CheckedState = CheckState.UnChecked, X = 0, Y = 1 }; + lastStep.Add (finalFinalStepEnabledCeckBox); + + // Add an optional FINAL last step + var finalFinalStep = new WizardStep { Title = "The VERY last step" }; + wizard.AddStep (finalFinalStep); + + finalFinalStep.HelpText = + "This step only shows if it was enabled on the other last step."; + finalFinalStep.Enabled = thirdStepEnabledCeckBox.CheckedState == CheckState.Checked; + + finalFinalStepEnabledCeckBox.CheckedStateChanged += (s, e) => + { + finalFinalStep.Enabled = + finalFinalStepEnabledCeckBox.CheckedState + == CheckState.Checked; + }; + + Application.Run (wizard); + wizard.Dispose (); + } + catch (FormatException) + { + actionLabel.Text = "Invalid Options"; + } + }; win.Add (showWizardButton); Application.Run (win); @@ -376,8 +382,5 @@ public class Wizards : Scenario Application.Shutdown (); } - private void Wizard_StepChanged (object sender, StepChangeEventArgs e) - { - throw new NotImplementedException (); - } + private void Wizard_StepChanged (object sender, StepChangeEventArgs e) { throw new NotImplementedException (); } } diff --git a/Examples/UICatalog/UICatalogTop.cs b/Examples/UICatalog/UICatalogTop.cs index d02a73919..0b46293bc 100644 --- a/Examples/UICatalog/UICatalogTop.cs +++ b/Examples/UICatalog/UICatalogTop.cs @@ -144,6 +144,7 @@ public class UICatalogTop : Toplevel "_About...", "About UI Catalog", () => MessageBox.Query ( + App, "", GetAboutBoxMessage (), wrapMessage: false, diff --git a/Terminal.Gui/App/Application.Clipboard.cs b/Terminal.Gui/App/Application.Clipboard.cs new file mode 100644 index 000000000..22cc85907 --- /dev/null +++ b/Terminal.Gui/App/Application.Clipboard.cs @@ -0,0 +1,15 @@ +namespace Terminal.Gui.App; + +public static partial class Application // Clipboard handling +{ + /// + /// Gets the clipboard for the application. + /// + /// + /// + /// Provides access to the OS clipboard through the driver. + /// + /// + [Obsolete ("The legacy static Application object is going away. Use IApplication.Clipboard instead.")] + public static IClipboard? Clipboard => ApplicationImpl.Instance.Clipboard; +} diff --git a/Terminal.Gui/App/Application.Driver.cs b/Terminal.Gui/App/Application.Driver.cs index 635ff854b..427ba4de5 100644 --- a/Terminal.Gui/App/Application.Driver.cs +++ b/Terminal.Gui/App/Application.Driver.cs @@ -13,38 +13,44 @@ public static partial class Application // Driver abstractions internal set => ApplicationImpl.Instance.Driver = value; } + private static bool _force16Colors = false; // Resources/config.json overrides + /// [ConfigurationProperty (Scope = typeof (SettingsScope))] [Obsolete ("The legacy static Application object is going away.")] public static bool Force16Colors { - get => ApplicationImpl.Instance.Force16Colors; - set => ApplicationImpl.Instance.Force16Colors = value; + get => _force16Colors; + set + { + bool oldValue = _force16Colors; + _force16Colors = value; + Force16ColorsChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _force16Colors)); + } } + /// Raised when changes. + public static event EventHandler>? Force16ColorsChanged; + + private static string _forceDriver = string.Empty; // Resources/config.json overrides + /// [ConfigurationProperty (Scope = typeof (SettingsScope))] [Obsolete ("The legacy static Application object is going away.")] public static string ForceDriver { - get => ApplicationImpl.Instance.ForceDriver; + get => _forceDriver; set { - if (!string.IsNullOrEmpty (ApplicationImpl.Instance.ForceDriver) && value != Driver?.GetName ()) - { - // ForceDriver cannot be changed if it has a valid value - return; - } - - if (ApplicationImpl.Instance.Initialized && value != Driver?.GetName ()) - { - throw new InvalidOperationException ($"The {nameof (ForceDriver)} can only be set before initialized."); - } - - ApplicationImpl.Instance.ForceDriver = value; + string oldValue = _forceDriver; + _forceDriver = value; + ForceDriverChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _forceDriver)); } } + /// Raised when changes. + public static event EventHandler>? ForceDriverChanged; + /// [Obsolete ("The legacy static Application object is going away.")] public static List Sixel => ApplicationImpl.Instance.Sixel; diff --git a/Terminal.Gui/App/Application.Lifecycle.cs b/Terminal.Gui/App/Application.Lifecycle.cs index 802ba7a53..c3c3cdf16 100644 --- a/Terminal.Gui/App/Application.Lifecycle.cs +++ b/Terminal.Gui/App/Application.Lifecycle.cs @@ -18,7 +18,15 @@ public static partial class Application // Lifecycle (Init/Shutdown) /// instance for all subsequent application operations. /// /// A new instance. - public static IApplication Create () { return new ApplicationImpl (); } + /// + /// Thrown if the legacy static Application model has already been used in this process. + /// + public static IApplication Create () + { + ApplicationImpl.MarkInstanceBasedModelUsed (); + + return new ApplicationImpl (); + } /// [RequiresUnreferencedCode ("AOT")] @@ -26,6 +34,7 @@ public static partial class Application // Lifecycle (Init/Shutdown) [Obsolete ("The legacy static Application object is going away.")] public static void Init (string? driverName = null) { + //Debug.Fail ("Application.Init() called - parallelizable tests should not use legacy static Application model"); ApplicationImpl.Instance.Init (driverName ?? ForceDriver); } @@ -35,8 +44,8 @@ public static partial class Application // Lifecycle (Init/Shutdown) [Obsolete ("The legacy static Application object is going away.")] public static int? MainThreadId { - get => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId; - set => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId = value; + get => ApplicationImpl.Instance.MainThreadId; + internal set => ApplicationImpl.Instance.MainThreadId = value; } /// @@ -65,5 +74,9 @@ public static partial class Application // Lifecycle (Init/Shutdown) // guaranteeing that the state of this singleton is deterministic when Init // starts running and after Shutdown returns. [Obsolete ("The legacy static Application object is going away.")] - internal static void ResetState (bool ignoreDisposed = false) => ApplicationImpl.Instance?.ResetState (ignoreDisposed); + internal static void ResetState (bool ignoreDisposed = false) + { + // Use the static reset method to bypass the fence check + ApplicationImpl.ResetStateStatic (ignoreDisposed); + } } diff --git a/Terminal.Gui/App/Application.Mouse.cs b/Terminal.Gui/App/Application.Mouse.cs index 5e6ec118f..2ea9bb650 100644 --- a/Terminal.Gui/App/Application.Mouse.cs +++ b/Terminal.Gui/App/Application.Mouse.cs @@ -4,15 +4,25 @@ namespace Terminal.Gui.App; public static partial class Application // Mouse handling { + private static bool _isMouseDisabled = false; // Resources/config.json overrides + /// Disable or enable the mouse. The mouse is enabled by default. [ConfigurationProperty (Scope = typeof (SettingsScope))] [Obsolete ("The legacy static Application object is going away.")] public static bool IsMouseDisabled { - get => Mouse.IsMouseDisabled; - set => Mouse.IsMouseDisabled = value; + get => _isMouseDisabled; + set + { + bool oldValue = _isMouseDisabled; + _isMouseDisabled = value; + IsMouseDisabledChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _isMouseDisabled)); + } } + /// Raised when changes. + public static event EventHandler>? IsMouseDisabledChanged; + /// /// Gets the instance that manages mouse event handling and state. /// diff --git a/Terminal.Gui/App/Application.Navigation.cs b/Terminal.Gui/App/Application.Navigation.cs index b1053ae42..031ebac1c 100644 --- a/Terminal.Gui/App/Application.Navigation.cs +++ b/Terminal.Gui/App/Application.Navigation.cs @@ -13,22 +13,42 @@ public static partial class Application // Navigation stuff internal set => ApplicationImpl.Instance.Navigation = value; } + private static Key _nextTabGroupKey = Key.F6; // Resources/config.json overrides + /// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key. [ConfigurationProperty (Scope = typeof (SettingsScope))] - [Obsolete ("The legacy static Application object is going away.")]public static Key NextTabGroupKey + public static Key NextTabGroupKey { - get => ApplicationImpl.Instance.Keyboard.NextTabGroupKey; - set => ApplicationImpl.Instance.Keyboard.NextTabGroupKey = value; + get => _nextTabGroupKey; + set + { + Key oldValue = _nextTabGroupKey; + _nextTabGroupKey = value; + NextTabGroupKeyChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _nextTabGroupKey)); + } } + /// Raised when changes. + public static event EventHandler>? NextTabGroupKeyChanged; + + private static Key _nextTabKey = Key.Tab; // Resources/config.json overrides + /// Alternative key to navigate forwards through views. Tab is the primary key. [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Key NextTabKey { - get => ApplicationImpl.Instance.Keyboard.NextTabKey; - set => ApplicationImpl.Instance.Keyboard.NextTabKey = value; + get => _nextTabKey; + set + { + Key oldValue = _nextTabKey; + _nextTabKey = value; + NextTabKeyChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _nextTabKey)); + } } + /// Raised when changes. + public static event EventHandler>? NextTabKeyChanged; + /// /// Raised when the user releases a key. /// @@ -48,19 +68,39 @@ public static partial class Application // Navigation stuff remove => ApplicationImpl.Instance.Keyboard.KeyUp -= value; } + private static Key _prevTabGroupKey = Key.F6.WithShift; // Resources/config.json overrides + /// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key. [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Key PrevTabGroupKey { - get => ApplicationImpl.Instance.Keyboard.PrevTabGroupKey; - set => ApplicationImpl.Instance.Keyboard.PrevTabGroupKey = value; + get => _prevTabGroupKey; + set + { + Key oldValue = _prevTabGroupKey; + _prevTabGroupKey = value; + PrevTabGroupKeyChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _prevTabGroupKey)); + } } + /// Raised when changes. + public static event EventHandler>? PrevTabGroupKeyChanged; + + private static Key _prevTabKey = Key.Tab.WithShift; // Resources/config.json overrides + /// Alternative key to navigate backwards through views. Shift+Tab is the primary key. [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Key PrevTabKey { - get => ApplicationImpl.Instance.Keyboard.PrevTabKey; - set => ApplicationImpl.Instance.Keyboard.PrevTabKey = value; + get => _prevTabKey; + set + { + Key oldValue = _prevTabKey; + _prevTabKey = value; + PrevTabKeyChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _prevTabKey)); + } } + + /// Raised when changes. + public static event EventHandler>? PrevTabKeyChanged; } diff --git a/Terminal.Gui/App/Application.Run.cs b/Terminal.Gui/App/Application.Run.cs index 9e6b2e064..81cea2171 100644 --- a/Terminal.Gui/App/Application.Run.cs +++ b/Terminal.Gui/App/Application.Run.cs @@ -4,22 +4,42 @@ namespace Terminal.Gui.App; public static partial class Application // Run (Begin -> Run -> Layout/Draw -> End -> Stop) { + private static Key _quitKey = Key.Esc; // Resources/config.json overrides + /// Gets or sets the key to quit the application. [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Key QuitKey { - get => ApplicationImpl.Instance.Keyboard.QuitKey; - set => ApplicationImpl.Instance.Keyboard.QuitKey = value; + get => _quitKey; + set + { + Key oldValue = _quitKey; + _quitKey = value; + QuitKeyChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _quitKey)); + } } + /// Raised when changes. + public static event EventHandler>? QuitKeyChanged; + + private static Key _arrangeKey = Key.F5.WithCtrl; // Resources/config.json overrides + /// Gets or sets the key to activate arranging views using the keyboard. [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Key ArrangeKey { - get => ApplicationImpl.Instance.Keyboard.ArrangeKey; - set => ApplicationImpl.Instance.Keyboard.ArrangeKey = value; + get => _arrangeKey; + set + { + Key oldValue = _arrangeKey; + _arrangeKey = value; + ArrangeKeyChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _arrangeKey)); + } } + /// Raised when changes. + public static event EventHandler>? ArrangeKeyChanged; + /// [Obsolete ("The legacy static Application object is going away.")] public static SessionToken Begin (Toplevel toplevel) => ApplicationImpl.Instance.Begin (toplevel); @@ -88,7 +108,7 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E /// [Obsolete ("The legacy static Application object is going away.")] - public static event EventHandler? Iteration + public static event EventHandler>? Iteration { add => ApplicationImpl.Instance.Iteration += value; remove => ApplicationImpl.Instance.Iteration -= value; diff --git a/Terminal.Gui/App/Application.Current.cs b/Terminal.Gui/App/Application.TopRunnable.cs similarity index 91% rename from Terminal.Gui/App/Application.Current.cs rename to Terminal.Gui/App/Application.TopRunnable.cs index 1b91a45ff..85a25cd06 100644 --- a/Terminal.Gui/App/Application.Current.cs +++ b/Terminal.Gui/App/Application.TopRunnable.cs @@ -2,7 +2,7 @@ using System.Collections.Concurrent; namespace Terminal.Gui.App; -public static partial class Application // Current handling +public static partial class Application // TopRunnable handling { /// [Obsolete ("The legacy static Application object is going away.")] public static ConcurrentStack SessionStack => ApplicationImpl.Instance.SessionStack; diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs index ed1e98741..e0a7390b7 100644 --- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs +++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs @@ -1,10 +1,14 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Reflection; namespace Terminal.Gui.App; public partial class ApplicationImpl { + /// + public int? MainThreadId { get; set; } + /// public bool Initialized { get; set; } @@ -23,6 +27,29 @@ public partial class ApplicationImpl throw new InvalidOperationException ("Init called multiple times without Shutdown"); } + // Thread-safe fence check: Ensure we're not mixing application models + // Use lock to make check-and-set atomic + lock (_modelUsageLock) + { + // If this is a legacy static instance and instance-based model was used, throw + if (this == _instance && ModelUsage == ApplicationModelUsage.InstanceBased) + { + throw new InvalidOperationException (ERROR_LEGACY_AFTER_MODERN); + } + + // If this is an instance-based instance and legacy static model was used, throw + if (this != _instance && ModelUsage == ApplicationModelUsage.LegacyStatic) + { + throw new InvalidOperationException (ERROR_MODERN_AFTER_LEGACY); + } + + // If no model has been set yet, set it now based on which instance this is + if (ModelUsage == ApplicationModelUsage.None) + { + ModelUsage = this == _instance ? ApplicationModelUsage.LegacyStatic : ApplicationModelUsage.InstanceBased; + } + } + if (!string.IsNullOrWhiteSpace (driverName)) { _driverName = driverName; @@ -41,26 +68,24 @@ public partial class ApplicationImpl // Preserve existing keyboard settings if they exist bool hasExistingKeyboard = _keyboard is { }; - Key existingQuitKey = _keyboard?.QuitKey ?? Key.Esc; - Key existingArrangeKey = _keyboard?.ArrangeKey ?? Key.F5.WithCtrl; - Key existingNextTabKey = _keyboard?.NextTabKey ?? Key.Tab; - Key existingPrevTabKey = _keyboard?.PrevTabKey ?? Key.Tab.WithShift; - Key existingNextTabGroupKey = _keyboard?.NextTabGroupKey ?? Key.F6; - Key existingPrevTabGroupKey = _keyboard?.PrevTabGroupKey ?? Key.F6.WithShift; + Key existingQuitKey = _keyboard?.QuitKey ?? Application.QuitKey; + Key existingArrangeKey = _keyboard?.ArrangeKey ?? Application.ArrangeKey; + Key existingNextTabKey = _keyboard?.NextTabKey ?? Application.NextTabKey; + Key existingPrevTabKey = _keyboard?.PrevTabKey ?? Application.PrevTabKey; + Key existingNextTabGroupKey = _keyboard?.NextTabGroupKey ?? Application.NextTabGroupKey; + Key existingPrevTabGroupKey = _keyboard?.PrevTabGroupKey ?? Application.PrevTabGroupKey; // Reset keyboard to ensure fresh state with default bindings _keyboard = new KeyboardImpl { App = this }; - // Restore previously set keys if they existed and were different from defaults - if (hasExistingKeyboard) - { - _keyboard.QuitKey = existingQuitKey; - _keyboard.ArrangeKey = existingArrangeKey; - _keyboard.NextTabKey = existingNextTabKey; - _keyboard.PrevTabKey = existingPrevTabKey; - _keyboard.NextTabGroupKey = existingNextTabGroupKey; - _keyboard.PrevTabGroupKey = existingPrevTabGroupKey; - } + // Sync keys from Application static properties (or existing keyboard if it had custom values) + // This ensures we respect any Application.QuitKey etc changes made before Init() + _keyboard.QuitKey = existingQuitKey; + _keyboard.ArrangeKey = existingArrangeKey; + _keyboard.NextTabKey = existingNextTabKey; + _keyboard.PrevTabKey = existingPrevTabKey; + _keyboard.NextTabGroupKey = existingNextTabGroupKey; + _keyboard.PrevTabGroupKey = existingPrevTabGroupKey; CreateDriver (_driverName); Screen = Driver!.Screen; @@ -85,8 +110,8 @@ public partial class ApplicationImpl if (runnableToDispose is { }) { // Extract the result using reflection to get the Result property value - var resultProperty = runnableToDispose.GetType().GetProperty("Result"); - result = resultProperty?.GetValue(runnableToDispose); + PropertyInfo? resultProperty = runnableToDispose.GetType ().GetProperty ("Result"); + result = resultProperty?.GetValue (runnableToDispose); } // Stop the coordinator if running @@ -115,8 +140,9 @@ public partial class ApplicationImpl { if (runnableToDispose is IDisposable disposable) { - disposable.Dispose(); + disposable.Dispose (); } + FrameworkOwnedRunnable = null; } @@ -140,36 +166,6 @@ public partial class ApplicationImpl return result; } -#if DEBUG - /// - /// DEBUG ONLY: Asserts that an event has no remaining subscribers. - /// - /// The name of the event for diagnostic purposes. - /// The event delegate to check. - private static void AssertNoEventSubscribers (string eventName, Delegate? eventDelegate) - { - if (eventDelegate is null) - { - return; - } - - Delegate [] subscribers = eventDelegate.GetInvocationList (); - - if (subscribers.Length > 0) - { - string subscriberInfo = string.Join ( - ", ", - subscribers.Select (d => $"{d.Method.DeclaringType?.Name}.{d.Method.Name}" - ) - ); - - Debug.Fail ( - $"Application.{eventName} has {subscribers.Length} remaining subscriber(s) after Shutdown: {subscriberInfo}" - ); - } - } -#endif - /// public void ResetState (bool ignoreDisposed = false) { @@ -241,6 +237,17 @@ public partial class ApplicationImpl ClearScreenNextIteration = false; // === 6. Reset input systems === + // Dispose keyboard and mouse to unsubscribe from events + if (_keyboard is IDisposable keyboardDisposable) + { + keyboardDisposable.Dispose (); + } + + if (_mouse is IDisposable mouseDisposable) + { + mouseDisposable.Dispose (); + } + // Mouse and Keyboard will be lazy-initialized on next access _mouse = null; _keyboard = null; @@ -273,10 +280,57 @@ public partial class ApplicationImpl // gui.cs does no longer process any callbacks. See #1084 for more details: // (https://github.com/gui-cs/Terminal.Gui/issues/1084). SynchronizationContext.SetSynchronizationContext (null); + + // === 12. Unsubscribe from Application static property change events === + UnsubscribeApplicationEvents (); } /// /// Raises the event. /// internal void RaiseInitializedChanged (object sender, EventArgs e) { InitializedChanged?.Invoke (sender, e); } + +#if DEBUG + /// + /// DEBUG ONLY: Asserts that an event has no remaining subscribers. + /// + /// The name of the event for diagnostic purposes. + /// The event delegate to check. + private static void AssertNoEventSubscribers (string eventName, Delegate? eventDelegate) + { + if (eventDelegate is null) + { + return; + } + + Delegate [] subscribers = eventDelegate.GetInvocationList (); + + if (subscribers.Length > 0) + { + string subscriberInfo = string.Join ( + ", ", + subscribers.Select (d => $"{d.Method.DeclaringType?.Name}.{d.Method.Name}" + ) + ); + + Debug.Fail ( + $"Application.{eventName} has {subscribers.Length} remaining subscriber(s) after Shutdown: {subscriberInfo}" + ); + } + } +#endif + + // Event handlers for Application static property changes + private void OnForce16ColorsChanged (object? sender, ValueChangedEventArgs e) { Force16Colors = e.NewValue; } + + private void OnForceDriverChanged (object? sender, ValueChangedEventArgs e) { ForceDriver = e.NewValue; } + + /// + /// Unsubscribes from Application static property change events. + /// + private void UnsubscribeApplicationEvents () + { + Application.Force16ColorsChanged -= OnForce16ColorsChanged; + Application.ForceDriverChanged -= OnForceDriverChanged; + } } diff --git a/Terminal.Gui/App/ApplicationImpl.Run.cs b/Terminal.Gui/App/ApplicationImpl.Run.cs index 944c64e09..e790e3ca0 100644 --- a/Terminal.Gui/App/ApplicationImpl.Run.cs +++ b/Terminal.Gui/App/ApplicationImpl.Run.cs @@ -5,15 +5,6 @@ namespace Terminal.Gui.App; public partial class ApplicationImpl { - /// - /// INTERNAL: Gets or sets the managed thread ID of the application's main UI thread, which is set during - /// and used to determine if code is executing on the main thread. - /// - /// - /// The managed thread ID of the main UI thread, or if the application is not initialized. - /// - internal int? MainThreadId { get; set; } - #region Begin->Run->Stop->End // TODO: This API is not used anywhere; it can be deleted @@ -156,11 +147,11 @@ public partial class ApplicationImpl /// public void RaiseIteration () { - Iteration?.Invoke (null, new ()); + Iteration?.Invoke (null, new (this)); } /// - public event EventHandler? Iteration; + public event EventHandler>? Iteration; /// [RequiresUnreferencedCode ("AOT")] diff --git a/Terminal.Gui/App/ApplicationImpl.cs b/Terminal.Gui/App/ApplicationImpl.cs index 2dc54cbda..6e968d0d3 100644 --- a/Terminal.Gui/App/ApplicationImpl.cs +++ b/Terminal.Gui/App/ApplicationImpl.cs @@ -9,23 +9,72 @@ namespace Terminal.Gui.App; public partial class ApplicationImpl : IApplication { /// - /// INTERNAL: Creates a new instance of the Application backend. + /// INTERNAL: Creates a new instance of the Application backend and subscribes to Application configuration property + /// events. /// - internal ApplicationImpl () { } + internal ApplicationImpl () + { + // Subscribe to Application static property change events + Application.Force16ColorsChanged += OnForce16ColorsChanged; + Application.ForceDriverChanged += OnForceDriverChanged; + } /// /// INTERNAL: Creates a new instance of the Application backend. /// /// - internal ApplicationImpl (IComponentFactory componentFactory) { _componentFactory = componentFactory; } + internal ApplicationImpl (IComponentFactory componentFactory) : this () { _componentFactory = componentFactory; } + + private string? _driverName; + + #region Clipboard + + /// + public IClipboard? Clipboard => Driver?.Clipboard; + + #endregion Clipboard + + /// + public new string ToString () => Driver?.ToString () ?? string.Empty; #region Singleton + /// + /// Lock object for synchronizing access to ModelUsage and _instance. + /// + private static readonly object _modelUsageLock = new (); + + /// + /// Tracks which application model has been used in this process. + /// + public static ApplicationModelUsage ModelUsage { get; private set; } = ApplicationModelUsage.None; + + /// + /// Error message for when trying to use modern model after legacy static model. + /// + internal const string ERROR_MODERN_AFTER_LEGACY = + "Cannot use modern instance-based model (Application.Create) after using legacy static Application model (Application.Init/ApplicationImpl.Instance). " + + "Use only one model per process."; + + /// + /// Error message for when trying to use legacy static model after modern model. + /// + internal const string ERROR_LEGACY_AFTER_MODERN = + "Cannot use legacy static Application model (Application.Init/ApplicationImpl.Instance) after using modern instance-based model (Application.Create). " + + "Use only one model per process."; + /// /// Configures the singleton instance of to use the specified backend implementation. /// /// - public static void SetInstance (IApplication? app) { _instance = app; } + public static void SetInstance (IApplication? app) + { + lock (_modelUsageLock) + { + ModelUsage = ApplicationModelUsage.LegacyStatic; + _instance = app; + } + } // Private static readonly Lazy instance of Application private static IApplication? _instance; @@ -33,12 +82,92 @@ public partial class ApplicationImpl : IApplication /// /// Gets the currently configured backend implementation of gateway methods. /// - public static IApplication Instance => _instance ??= new ApplicationImpl (); + public static IApplication Instance + { + get + { + //Debug.Fail ("ApplicationImpl.Instance accessed - parallelizable tests should not use legacy static Application model"); + + // Thread-safe: Use lock to make check-and-create atomic + lock (_modelUsageLock) + { + // If an instance already exists, return it without fence checking + // This allows for cleanup/reset operations + if (_instance is { }) + { + return _instance; + } + + // Check if the instance-based model has already been used + if (ModelUsage == ApplicationModelUsage.InstanceBased) + { + throw new InvalidOperationException (ERROR_LEGACY_AFTER_MODERN); + } + + // Mark the usage and create the instance + ModelUsage = ApplicationModelUsage.LegacyStatic; + + return _instance = new ApplicationImpl (); + } + } + } + + /// + /// INTERNAL: Marks that the instance-based model has been used. Called by Application.Create(). + /// + internal static void MarkInstanceBasedModelUsed () + { + lock (_modelUsageLock) + { + // Check if the legacy static model has already been initialized + if (ModelUsage == ApplicationModelUsage.LegacyStatic && _instance?.Initialized == true) + { + throw new InvalidOperationException (ERROR_MODERN_AFTER_LEGACY); + } + + ModelUsage = ApplicationModelUsage.InstanceBased; + } + } + + /// + /// INTERNAL: Resets the model usage tracking. Only for testing purposes. + /// + internal static void ResetModelUsageTracking () + { + lock (_modelUsageLock) + { + ModelUsage = ApplicationModelUsage.None; + _instance = null; + } + } + + /// + /// INTERNAL: Resets state without going through the fence-checked Instance property. + /// Used by Application.ResetState() to allow cleanup regardless of which model was used. + /// + internal static void ResetStateStatic (bool ignoreDisposed = false) + { + // If an instance exists, reset it + _instance?.ResetState (ignoreDisposed); + + // Reset Application static properties to their defaults + // This ensures tests start with clean state + Application.ForceDriver = string.Empty; + Application.Force16Colors = false; + Application.IsMouseDisabled = false; + Application.QuitKey = Key.Esc; + Application.ArrangeKey = Key.F5.WithCtrl; + Application.NextTabGroupKey = Key.F6; + Application.NextTabKey = Key.Tab; + Application.PrevTabGroupKey = Key.F6.WithShift; + Application.PrevTabKey = Key.Tab.WithShift; + + // Always reset the model tracking to allow tests to use either model after reset + ResetModelUsageTracking (); + } #endregion Singleton - private string? _driverName; - #region Input private IMouse? _mouse; @@ -122,8 +251,6 @@ public partial class ApplicationImpl : IApplication } } - // BUGBUG: Technically, this is not the full lst of sessions. There be dragons here, e.g. see how Toplevel.Id is used. What - /// public ConcurrentStack SessionStack { get; } = new (); @@ -137,7 +264,4 @@ public partial class ApplicationImpl : IApplication public IRunnable? FrameworkOwnedRunnable { get; set; } #endregion View Management - - /// - public new string ToString () => Driver?.ToString () ?? string.Empty; } diff --git a/Terminal.Gui/App/ApplicationModelUsage.cs b/Terminal.Gui/App/ApplicationModelUsage.cs new file mode 100644 index 000000000..909291d70 --- /dev/null +++ b/Terminal.Gui/App/ApplicationModelUsage.cs @@ -0,0 +1,16 @@ +namespace Terminal.Gui.App; + +/// +/// Defines the different application usage models. +/// +public enum ApplicationModelUsage +{ + /// No model has been used yet. + None, + + /// Legacy static model (Application.Init/ApplicationImpl.Instance). + LegacyStatic, + + /// Modern instance-based model (Application.Create). + InstanceBased +} diff --git a/Terminal.Gui/App/ApplicationNavigation.cs b/Terminal.Gui/App/ApplicationNavigation.cs index 1149c3ad6..6012d4629 100644 --- a/Terminal.Gui/App/ApplicationNavigation.cs +++ b/Terminal.Gui/App/ApplicationNavigation.cs @@ -13,7 +13,7 @@ public class ApplicationNavigation /// public ApplicationNavigation () { - // TODO: Move navigation key bindings here from AddApplicationKeyBindings + // TODO: Move navigation key bindings here from KeyboardImpl } /// diff --git a/Terminal.Gui/App/ApplicationRunnableExtensions.cs b/Terminal.Gui/App/ApplicationRunnableExtensions.cs index 7e706e9d5..3eb03c081 100644 --- a/Terminal.Gui/App/ApplicationRunnableExtensions.cs +++ b/Terminal.Gui/App/ApplicationRunnableExtensions.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui.App; /// /// /// These extensions provide convenience methods for wrapping views in -/// and running them in a single call, similar to how works. +/// and running them in a single call, similar to how works. /// public static class ApplicationRunnableExtensions { diff --git a/Terminal.Gui/App/Clipboard/Clipboard.cs b/Terminal.Gui/App/Clipboard/Clipboard.cs index 0db013351..42472e414 100644 --- a/Terminal.Gui/App/Clipboard/Clipboard.cs +++ b/Terminal.Gui/App/Clipboard/Clipboard.cs @@ -2,6 +2,9 @@ namespace Terminal.Gui.App; /// Provides cut, copy, and paste support for the OS clipboard. /// +/// +/// DEPRECATED: This static class is obsolete. Use instead. +/// /// On Windows, the class uses the Windows Clipboard APIs via P/Invoke. /// /// On Linux, when not running under Windows Subsystem for Linux (WSL), the class uses @@ -16,6 +19,7 @@ namespace Terminal.Gui.App; /// the Mac clipboard APIs vai P/Invoke. /// /// +[Obsolete ("Use IApplication.Clipboard instead. The static Clipboard class will be removed in a future release.")] public static class Clipboard { private static string? _contents = string.Empty; @@ -65,4 +69,32 @@ public static class Clipboard /// Returns true if the environmental dependencies are in place to interact with the OS clipboard. /// public static bool IsSupported => Application.Driver?.Clipboard?.IsSupported ?? false; + + /// Gets the OS clipboard data if possible. + /// The clipboard data if successful. + /// if the clipboard data was retrieved successfully; otherwise, . + public static bool TryGetClipboardData (out string result) + { + result = string.Empty; + + if (IsSupported && Application.Driver?.Clipboard is { }) + { + return Application.Driver.Clipboard.TryGetClipboardData (out result); + } + + return false; + } + + /// Sets the OS clipboard data if possible. + /// The text to set. + /// if the clipboard data was set successfully; otherwise, . + public static bool TrySetClipboardData (string text) + { + if (IsSupported && Application.Driver?.Clipboard is { }) + { + return Application.Driver.Clipboard.TrySetClipboardData (text); + } + + return false; + } } diff --git a/Terminal.Gui/App/IApplication.cs b/Terminal.Gui/App/IApplication.cs index f663a351f..a4a8c902e 100644 --- a/Terminal.Gui/App/IApplication.cs +++ b/Terminal.Gui/App/IApplication.cs @@ -39,6 +39,16 @@ public interface IApplication #region Initialization and Shutdown + /// + /// Gets or sets the managed thread ID of the application's main UI thread, which is set during + /// and used to determine if code is executing on the main thread. + /// + /// + /// The managed thread ID of the main UI thread, or if the application is not initialized. + /// + public int? MainThreadId { get; internal set; } + + /// Initializes a new instance of Application. /// /// The short name (e.g. "dotnet", "windows", "unix", or "fake") of the @@ -218,7 +228,7 @@ public interface IApplication [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] public TView Run (Func? errorHandler = null, string? driverName = null) - where TView : Toplevel, new (); + where TView : Toplevel, new(); /// /// Runs a new Session using the provided view and calling @@ -273,9 +283,11 @@ public interface IApplication /// /// This event is raised before input processing, timeout callbacks, and rendering occur each iteration. /// - /// See also and . + /// The event args contain the current application instance. /// - public event EventHandler? Iteration; + /// + /// . + public event EventHandler>? Iteration; /// Runs on the main UI loop thread. /// The action to be invoked on the main processing thread. @@ -523,7 +535,7 @@ public interface IApplication /// Supports fluent API: var result = Application.Create().Init().Run<MyView>().Shutdown() as MyResultType /// /// - IApplication Run (Func? errorHandler = null) where TRunnable : IRunnable, new (); + IApplication Run (Func? errorHandler = null) where TRunnable : IRunnable, new(); /// /// Requests that the specified runnable session stop. @@ -574,6 +586,17 @@ public interface IApplication /// IDriver? Driver { get; set; } + /// + /// Gets the clipboard for this application instance. + /// + /// + /// + /// Provides access to the OS clipboard through the driver. Returns if + /// is not initialized. + /// + /// + IClipboard? Clipboard { get; } + /// /// Gets or sets whether will be forced to output only the 16 colors defined in /// . The default is , meaning 24-bit (TrueColor) colors will be diff --git a/Terminal.Gui/App/IterationEventArgs.cs b/Terminal.Gui/App/IterationEventArgs.cs deleted file mode 100644 index e0c98d2ab..000000000 --- a/Terminal.Gui/App/IterationEventArgs.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Terminal.Gui.App; - -/// Event arguments for the event. -public class IterationEventArgs : EventArgs -{ } diff --git a/Terminal.Gui/App/Keyboard/KeyboardImpl.cs b/Terminal.Gui/App/Keyboard/KeyboardImpl.cs index 75c37d4b8..7b4d26e2d 100644 --- a/Terminal.Gui/App/Keyboard/KeyboardImpl.cs +++ b/Terminal.Gui/App/Keyboard/KeyboardImpl.cs @@ -1,7 +1,10 @@ +using System.Collections.Concurrent; + namespace Terminal.Gui.App; /// /// INTERNAL: Implements to manage keyboard input and key bindings at the Application level. +/// This implementation is thread-safe for all public operations. /// /// This implementation decouples keyboard handling state from the static class, /// enabling parallelizable unit tests and better testability. @@ -10,19 +13,61 @@ namespace Terminal.Gui.App; /// See for usage details. /// /// -internal class KeyboardImpl : IKeyboard +internal class KeyboardImpl : IKeyboard, IDisposable { - private Key _quitKey = Key.Esc; // Resources/config.json overrides - private Key _arrangeKey = Key.F5.WithCtrl; // Resources/config.json overrides - private Key _nextTabGroupKey = Key.F6; // Resources/config.json overrides - private Key _nextTabKey = Key.Tab; // Resources/config.json overrides - private Key _prevTabGroupKey = Key.F6.WithShift; // Resources/config.json overrides - private Key _prevTabKey = Key.Tab.WithShift; // Resources/config.json overrides + /// + /// Initializes keyboard bindings and subscribes to Application configuration property events. + /// + public KeyboardImpl () + { + // DON'T access Application static properties here - they trigger ApplicationImpl.Instance + // which sets ModelUsage to LegacyStatic, breaking parallel tests. + // These will be initialized from Application static properties in Init() or when accessed. + + // Initialize to reasonable defaults that match Application defaults + // These will be updated by property change events if Application properties change + _quitKey = Key.Esc; + _arrangeKey = Key.F5.WithCtrl; + _nextTabGroupKey = Key.F6; + _nextTabKey = Key.Tab; + _prevTabGroupKey = Key.F6.WithShift; + _prevTabKey = Key.Tab.WithShift; + + // Subscribe to Application static property change events + // so we get updated if they change + Application.QuitKeyChanged += OnQuitKeyChanged; + Application.ArrangeKeyChanged += OnArrangeKeyChanged; + Application.NextTabGroupKeyChanged += OnNextTabGroupKeyChanged; + Application.NextTabKeyChanged += OnNextTabKeyChanged; + Application.PrevTabGroupKeyChanged += OnPrevTabGroupKeyChanged; + Application.PrevTabKeyChanged += OnPrevTabKeyChanged; + + AddKeyBindings (); + } /// - /// Commands for Application. + /// Commands for Application. Thread-safe for concurrent access. /// - private readonly Dictionary _commandImplementations = new (); + private readonly ConcurrentDictionary _commandImplementations = new (); + + private Key _quitKey; + private Key _arrangeKey; + private Key _nextTabGroupKey; + private Key _nextTabKey; + private Key _prevTabGroupKey; + private Key _prevTabKey; + + /// + public void Dispose () + { + // Unsubscribe from Application static property change events + Application.QuitKeyChanged -= OnQuitKeyChanged; + Application.ArrangeKeyChanged -= OnArrangeKeyChanged; + Application.NextTabGroupKeyChanged -= OnNextTabGroupKeyChanged; + Application.NextTabKeyChanged -= OnNextTabKeyChanged; + Application.PrevTabGroupKeyChanged -= OnPrevTabGroupKeyChanged; + Application.PrevTabKeyChanged -= OnPrevTabKeyChanged; + } /// public IApplication? App { get; set; } @@ -102,14 +147,6 @@ internal class KeyboardImpl : IKeyboard /// public event EventHandler? KeyUp; - /// - /// Initializes keyboard bindings. - /// - public KeyboardImpl () - { - AddKeyBindings (); - } - /// public bool RaiseKeyDownEvent (Key key) { @@ -165,7 +202,8 @@ internal class KeyboardImpl : IKeyboard } bool? commandHandled = InvokeCommandsBoundToKey (key); - if(commandHandled is true) + + if (commandHandled is true) { return true; } @@ -188,7 +226,6 @@ internal class KeyboardImpl : IKeyboard return true; } - // TODO: Add Popover support if (App?.SessionStack is { }) @@ -214,6 +251,7 @@ internal class KeyboardImpl : IKeyboard public bool? InvokeCommandsBoundToKey (Key key) { bool? handled = null; + // Invoke any Application-scoped KeyBindings. // The first view that handles the key will stop the loop. // foreach (KeyValuePair binding in KeyBindings.GetBindings (key)) @@ -264,24 +302,6 @@ internal class KeyboardImpl : IKeyboard return null; } - /// - /// - /// Sets the function that will be invoked for a . - /// - /// - /// If AddCommand has already been called for will - /// replace the old one. - /// - /// - /// - /// - /// This version of AddCommand is for commands that do not require a . - /// - /// - /// The command. - /// The function. - private void AddCommand (Command command, Func f) { _commandImplementations [command] = ctx => f (); } - internal void AddKeyBindings () { _commandImplementations.Clear (); @@ -296,6 +316,7 @@ internal class KeyboardImpl : IKeyboard return true; } ); + AddCommand ( Command.Suspend, () => @@ -305,6 +326,7 @@ internal class KeyboardImpl : IKeyboard return true; } ); + AddCommand ( Command.NextTabStop, () => App?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop)); @@ -351,31 +373,64 @@ internal class KeyboardImpl : IKeyboard return false; }); - //SetKeysToHardCodedDefaults (); - // Need to clear after setting the above to ensure actually clear - // because set_QuitKey etc.. may call Add - KeyBindings.Clear (); + // because set_QuitKey etc. may call Add + //KeyBindings.Clear (); - KeyBindings.Add (QuitKey, Command.Quit); - KeyBindings.Add (NextTabKey, Command.NextTabStop); - KeyBindings.Add (PrevTabKey, Command.PreviousTabStop); - KeyBindings.Add (NextTabGroupKey, Command.NextTabGroup); - KeyBindings.Add (PrevTabGroupKey, Command.PreviousTabGroup); - KeyBindings.Add (ArrangeKey, Command.Arrange); + // Use ReplaceCommands instead of Add, because it's possible that + // during construction the Application static properties changed, and + // we added those keys already. + KeyBindings.ReplaceCommands (QuitKey, Command.Quit); + KeyBindings.ReplaceCommands (NextTabKey, Command.NextTabStop); + KeyBindings.ReplaceCommands (PrevTabKey, Command.PreviousTabStop); + KeyBindings.ReplaceCommands (NextTabGroupKey, Command.NextTabGroup); + KeyBindings.ReplaceCommands (PrevTabGroupKey, Command.PreviousTabGroup); + KeyBindings.ReplaceCommands (ArrangeKey, Command.Arrange); - KeyBindings.Add (Key.CursorRight, Command.NextTabStop); - KeyBindings.Add (Key.CursorDown, Command.NextTabStop); - KeyBindings.Add (Key.CursorLeft, Command.PreviousTabStop); - KeyBindings.Add (Key.CursorUp, Command.PreviousTabStop); + // TODO: Should these be configurable? + KeyBindings.ReplaceCommands (Key.CursorRight, Command.NextTabStop); + KeyBindings.ReplaceCommands (Key.CursorDown, Command.NextTabStop); + KeyBindings.ReplaceCommands (Key.CursorLeft, Command.PreviousTabStop); + KeyBindings.ReplaceCommands (Key.CursorUp, Command.PreviousTabStop); // TODO: Refresh Key should be configurable - KeyBindings.Add (Key.F5, Command.Refresh); + KeyBindings.ReplaceCommands (Key.F5, Command.Refresh); // TODO: Suspend Key should be configurable if (Environment.OSVersion.Platform == PlatformID.Unix) { - KeyBindings.Add (Key.Z.WithCtrl, Command.Suspend); + KeyBindings.ReplaceCommands (Key.Z.WithCtrl, Command.Suspend); } } + + /// + /// + /// Sets the function that will be invoked for a . + /// + /// + /// If AddCommand has already been called for will + /// replace the old one. + /// + /// + /// + /// + /// This version of AddCommand is for commands that do not require a . + /// + /// + /// The command. + /// The function. + private void AddCommand (Command command, Func f) { _commandImplementations [command] = ctx => f (); } + + private void OnArrangeKeyChanged (object? sender, ValueChangedEventArgs e) { ArrangeKey = e.NewValue; } + + private void OnNextTabGroupKeyChanged (object? sender, ValueChangedEventArgs e) { NextTabGroupKey = e.NewValue; } + + private void OnNextTabKeyChanged (object? sender, ValueChangedEventArgs e) { NextTabKey = e.NewValue; } + + private void OnPrevTabGroupKeyChanged (object? sender, ValueChangedEventArgs e) { PrevTabGroupKey = e.NewValue; } + + private void OnPrevTabKeyChanged (object? sender, ValueChangedEventArgs e) { PrevTabKey = e.NewValue; } + + // Event handlers for Application static property changes + private void OnQuitKeyChanged (object? sender, ValueChangedEventArgs e) { QuitKey = e.NewValue; } } diff --git a/Terminal.Gui/App/Mouse/MouseImpl.cs b/Terminal.Gui/App/Mouse/MouseImpl.cs index 12aa4ada9..42dc1c388 100644 --- a/Terminal.Gui/App/Mouse/MouseImpl.cs +++ b/Terminal.Gui/App/Mouse/MouseImpl.cs @@ -9,15 +9,25 @@ namespace Terminal.Gui.App; /// enabling better testability and parallel test execution. /// /// -internal class MouseImpl : IMouse +internal class MouseImpl : IMouse, IDisposable { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class and subscribes to Application configuration property events. /// - public MouseImpl () { } + public MouseImpl () + { + // Subscribe to Application static property change events + Application.IsMouseDisabledChanged += OnIsMouseDisabledChanged; + } + + private IApplication? _app; /// - public IApplication? App { get; set; } + public IApplication? App + { + get => _app; + set => _app = value; + } /// public Point? LastMousePosition { get; set; } @@ -391,4 +401,17 @@ internal class MouseImpl : IMouse return false; } + + // Event handler for Application static property changes + private void OnIsMouseDisabledChanged (object? sender, ValueChangedEventArgs e) + { + IsMouseDisabled = e.NewValue; + } + + /// + public void Dispose () + { + // Unsubscribe from Application static property change events + Application.IsMouseDisabledChanged -= OnIsMouseDisabledChanged; + } } diff --git a/Terminal.Gui/App/Runnable/IRunnable.cs b/Terminal.Gui/App/Runnable/IRunnable.cs index 2e6711d0c..75ef00b1b 100644 --- a/Terminal.Gui/App/Runnable/IRunnable.cs +++ b/Terminal.Gui/App/Runnable/IRunnable.cs @@ -66,7 +66,7 @@ public interface IRunnable /// /// Raised when is changing (e.g., when or /// is called). - /// Can be canceled by setting to . + /// Can be canceled by setting `args.Cancel` to . /// /// /// @@ -140,7 +140,7 @@ public interface IRunnable /// /// Raised when this runnable is about to become modal (top of stack) or cease being modal. - /// Can be canceled by setting to . + /// Can be canceled by setting `args.Cancel` to . /// /// /// diff --git a/Terminal.Gui/Configuration/ConfigurationManager.cs b/Terminal.Gui/Configuration/ConfigurationManager.cs index b8d3cdf5d..aa79c26e9 100644 --- a/Terminal.Gui/Configuration/ConfigurationManager.cs +++ b/Terminal.Gui/Configuration/ConfigurationManager.cs @@ -595,15 +595,53 @@ public static class ConfigurationManager TypeInfoResolver = SourceGenerationContext.Default }); + private static SourcesManager? _sourcesManager = new (); + private static readonly object _sourcesManagerLock = new (); + /// /// Gets the Sources Manager - manages the loading of configuration sources from files and resources. /// - public static SourcesManager? SourcesManager { get; internal set; } = new (); + public static SourcesManager? SourcesManager + { + get + { + lock (_sourcesManagerLock) + { + return _sourcesManager; + } + } + internal set + { + lock (_sourcesManagerLock) + { + _sourcesManager = value; + } + } + } + + private static string? _runtimeConfig = """{ }"""; + private static readonly object _runtimeConfigLock = new (); /// /// Gets or sets the in-memory config.json. See . /// - public static string? RuntimeConfig { get; set; } = """{ }"""; + public static string? RuntimeConfig + { + get + { + lock (_runtimeConfigLock) + { + return _runtimeConfig; + } + } + set + { + lock (_runtimeConfigLock) + { + _runtimeConfig = value; + } + } + } [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] private static readonly string _configFilename = "config.json"; @@ -678,13 +716,32 @@ public static class ConfigurationManager [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] internal static StringBuilder _jsonErrors = new (); + private static bool? _throwOnJsonErrors = false; + private static readonly object _throwOnJsonErrorsLock = new (); + /// /// Gets or sets whether the should throw an exception if it encounters an /// error on deserialization. If (the default), the error is logged and printed to the console /// when is called. /// [ConfigurationProperty (Scope = typeof (SettingsScope))] - public static bool? ThrowOnJsonErrors { get; set; } = false; + public static bool? ThrowOnJsonErrors + { + get + { + lock (_throwOnJsonErrorsLock) + { + return _throwOnJsonErrors; + } + } + set + { + lock (_throwOnJsonErrorsLock) + { + _throwOnJsonErrors = value; + } + } + } #pragma warning disable IDE1006 // Naming Styles private static readonly object _jsonErrorsLock = new (); @@ -758,8 +815,27 @@ public static class ConfigurationManager return JsonSerializer.Serialize (emptyScope, typeof (SettingsScope), SerializerContext!); } + private static string _appName = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!; + private static readonly object _appNameLock = new (); + /// Name of the running application. By default, this property is set to the application's assembly name. - public static string AppName { get; set; } = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!; + public static string AppName + { + get + { + lock (_appNameLock) + { + return _appName; + } + } + set + { + lock (_appNameLock) + { + _appName = value; + } + } + } /// /// INTERNAL: Retrieves all uninitialized configuration properties that belong to a specific scope from the cache. diff --git a/Terminal.Gui/Configuration/SourcesManager.cs b/Terminal.Gui/Configuration/SourcesManager.cs index 541d452a2..71c32ed36 100644 --- a/Terminal.Gui/Configuration/SourcesManager.cs +++ b/Terminal.Gui/Configuration/SourcesManager.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System.Collections.Concurrent; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json; @@ -13,7 +14,7 @@ public class SourcesManager /// /// Provides a map from each of the to file system and resource paths that have been loaded by . /// - public Dictionary Sources { get; } = new (); + public ConcurrentDictionary Sources { get; } = new (); /// INTERNAL: Loads into the specified . /// The Settings Scope object that will be loaded into. @@ -62,11 +63,8 @@ public class SourcesManager internal void AddSource (ConfigLocations location, string source) { - if (!Sources.TryAdd (location, source)) - { - //Logging.Warning ($"{location} has already been added to Sources."); - Sources [location] = source; - } + // ConcurrentDictionary's AddOrUpdate is thread-safe + Sources.AddOrUpdate (location, source, (key, oldValue) => source); } diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeInputProcessor.cs b/Terminal.Gui/Drivers/FakeDriver/FakeInputProcessor.cs index 83b0739a7..f6c777e33 100644 --- a/Terminal.Gui/Drivers/FakeDriver/FakeInputProcessor.cs +++ b/Terminal.Gui/Drivers/FakeDriver/FakeInputProcessor.cs @@ -1,4 +1,3 @@ -#nullable disable using System.Collections.Concurrent; namespace Terminal.Gui.Drivers; @@ -27,17 +26,18 @@ public class FakeInputProcessor : InputProcessorImpl } /// - public override void EnqueueMouseEvent (MouseEventArgs mouseEvent) + public override void EnqueueMouseEvent (IApplication? app, MouseEventArgs mouseEvent) { // FakeDriver uses ConsoleKeyInfo as its input record type, which cannot represent mouse events. + // TODO: Verify this is correct. This didn't check the threadId before. // If Application.Invoke is available (running in Application context), defer to next iteration // to ensure proper timing - the event is raised after views are laid out. // Otherwise (unit tests), raise immediately so tests can verify synchronously. - if (Application.MainThreadId is { }) + if (app is {} && app.MainThreadId != Thread.CurrentThread.ManagedThreadId) { // Application is running - use Invoke to defer to next iteration - ApplicationImpl.Instance.Invoke ((_) => RaiseMouseEvent (mouseEvent)); + app?.Invoke ((_) => RaiseMouseEvent (mouseEvent)); } else { diff --git a/Terminal.Gui/Drivers/IInputProcessor.cs b/Terminal.Gui/Drivers/IInputProcessor.cs index 9c800946c..b10ab0842 100644 --- a/Terminal.Gui/Drivers/IInputProcessor.cs +++ b/Terminal.Gui/Drivers/IInputProcessor.cs @@ -1,5 +1,4 @@ - -namespace Terminal.Gui.Drivers; +namespace Terminal.Gui.Drivers; /// /// Interface for main loop class that will process the queued input. @@ -12,7 +11,7 @@ public interface IInputProcessor public event EventHandler? AnsiSequenceSwallowed; /// - /// Gets the name of the driver associated with this input processor. + /// Gets the name of the driver associated with this input processor. /// string? DriverName { get; init; } @@ -58,7 +57,8 @@ public interface IInputProcessor /// Called when a key up event has been dequeued. Raises the event. /// /// - /// Drivers that do not support key release events will call this method after processing + /// Drivers that do not support key release events will call this method after + /// processing /// is complete. /// /// The key event data. @@ -89,7 +89,10 @@ public interface IInputProcessor /// /// Adds a mouse input event to the input queue. For unit tests. /// + /// + /// The application instance to use. Used to use Invoke to raise the mouse + /// event in the case where this method is not called on the main thread. + /// /// - void EnqueueMouseEvent (MouseEventArgs mouseEvent); - + void EnqueueMouseEvent (IApplication? app, MouseEventArgs mouseEvent); } diff --git a/Terminal.Gui/Drivers/InputProcessorImpl.cs b/Terminal.Gui/Drivers/InputProcessorImpl.cs index 24c249a84..57b74f1f4 100644 --- a/Terminal.Gui/Drivers/InputProcessorImpl.cs +++ b/Terminal.Gui/Drivers/InputProcessorImpl.cs @@ -122,7 +122,7 @@ public abstract class InputProcessorImpl : IInputProcessor, IDispo public event EventHandler? MouseEvent; /// - public virtual void EnqueueMouseEvent (MouseEventArgs mouseEvent) + public virtual void EnqueueMouseEvent (IApplication? app, MouseEventArgs mouseEvent) { // Base implementation: For drivers where TInputRecord cannot represent mouse events // (e.g., ConsoleKeyInfo), derived classes should override this method. diff --git a/Terminal.Gui/Drivers/OutputBase.cs b/Terminal.Gui/Drivers/OutputBase.cs index 6c10fad0b..ad1f4120e 100644 --- a/Terminal.Gui/Drivers/OutputBase.cs +++ b/Terminal.Gui/Drivers/OutputBase.cs @@ -90,14 +90,16 @@ public abstract class OutputBase } } - foreach (SixelToRender s in Application.Sixel) - { - if (!string.IsNullOrWhiteSpace (s.SixelData)) - { - SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y); - Console.Out.Write (s.SixelData); - } - } + // BUGBUG: The Sixel impl depends on the legacy static Application object + // BUGBUG: Disabled for now + //foreach (SixelToRender s in Application.Sixel) + //{ + // if (!string.IsNullOrWhiteSpace (s.SixelData)) + // { + // SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y); + // Console.Out.Write (s.SixelData); + // } + //} SetCursorVisibility (savedVisibility ?? CursorVisibility.Default); _cachedCursorVisibility = savedVisibility; diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsInputProcessor.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsInputProcessor.cs index 739a393fb..3777a034a 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsInputProcessor.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsInputProcessor.cs @@ -18,7 +18,7 @@ internal class WindowsInputProcessor : InputProcessorImpl } /// - public override void EnqueueMouseEvent (MouseEventArgs mouseEvent) + public override void EnqueueMouseEvent (IApplication? app, MouseEventArgs mouseEvent) { InputQueue.Enqueue (new () { diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs index 5a81ae0ab..b351696a2 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs @@ -149,7 +149,7 @@ internal partial class WindowsOutput : OutputBase, IOutput // Force 16 colors if not in virtual terminal mode. // BUGBUG: This is bad. It does not work if the app was crated without // BUGBUG: Apis. - ApplicationImpl.Instance.Force16Colors = true; + //ApplicationImpl.Instance.Force16Colors = true; } @@ -357,7 +357,8 @@ internal partial class WindowsOutput : OutputBase, IOutput { // BUGBUG: This is bad. It does not work if the app was crated without // BUGBUG: Apis. - bool force16Colors = ApplicationImpl.Instance.Force16Colors; + // bool force16Colors = ApplicationImpl.Instance.Force16Colors; + bool force16Colors = false; if (force16Colors) { diff --git a/Terminal.Gui/FileServices/IFileOperations.cs b/Terminal.Gui/FileServices/IFileOperations.cs index 610d097be..920f3e947 100644 --- a/Terminal.Gui/FileServices/IFileOperations.cs +++ b/Terminal.Gui/FileServices/IFileOperations.cs @@ -9,28 +9,31 @@ namespace Terminal.Gui.FileServices; public interface IFileOperations { /// Specifies how to handle file/directory deletion attempts in . + /// /// /// if operation was completed or if cancelled /// /// Ensure you use a try/catch block with appropriate error handling (e.g. showing a /// - bool Delete (IEnumerable toDelete); + bool Delete (IApplication? app, IEnumerable toDelete); /// Specifies how to handle 'new directory' operation in . + /// /// /// The parent directory in which the new directory should be created /// The newly created directory or null if cancelled. /// /// Ensure you use a try/catch block with appropriate error handling (e.g. showing a /// - IFileSystemInfo New (IFileSystem fileSystem, IDirectoryInfo inDirectory); + IFileSystemInfo New (IApplication? app, IFileSystem fileSystem, IDirectoryInfo inDirectory); /// Specifies how to handle file/directory rename attempts in . + /// /// /// /// The new name for the file or null if cancelled /// /// Ensure you use a try/catch block with appropriate error handling (e.g. showing a /// - IFileSystemInfo Rename (IFileSystem fileSystem, IFileSystemInfo toRename); + IFileSystemInfo Rename (IApplication? app, IFileSystem fileSystem, IFileSystemInfo toRename); } diff --git a/Terminal.Gui/Input/InputBindings.cs b/Terminal.Gui/Input/InputBindings.cs index 8711cf87c..75e2e8aea 100644 --- a/Terminal.Gui/Input/InputBindings.cs +++ b/Terminal.Gui/Input/InputBindings.cs @@ -1,19 +1,15 @@ -namespace Terminal.Gui.Input; +using System.Collections.Concurrent; + +namespace Terminal.Gui.Input; /// /// Abstract class for and . +/// This class is thread-safe for all public operations. /// /// The type of the event (e.g. or ). /// The binding type (e.g. ). -public abstract class InputBindings where TBinding : IInputBinding, new () where TEvent : notnull +public abstract class InputBindings where TBinding : IInputBinding, new() where TEvent : notnull { - /// - /// The bindings. - /// - private readonly Dictionary _bindings; - - private readonly Func _constructBinding; - /// /// Initializes a new instance. /// @@ -26,11 +22,11 @@ public abstract class InputBindings where TBinding : IInputBin } /// - /// Tests whether is valid or not. + /// The bindings. /// - /// - /// - public abstract bool IsValid (TEvent eventArgs); + private readonly ConcurrentDictionary _bindings; + + private readonly Func _constructBinding; /// Adds a bound to to the collection. /// @@ -42,24 +38,21 @@ public abstract class InputBindings where TBinding : IInputBin throw new ArgumentException (@"Invalid newEventArgs", nameof (eventArgs)); } -#pragma warning disable CS8601 // Possible null reference assignment. - if (TryGet (eventArgs, out TBinding _)) + // IMPORTANT: Add a COPY of the eventArgs. This is needed because ConfigurationManager.Apply uses DeepMemberWiseCopy + // IMPORTANT: update the memory referenced by the key, and Dictionary uses caching for performance, and thus + // IMPORTANT: Apply will update the Dictionary with the new eventArgs, but the old eventArgs will still be in the dictionary. + // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details. + if (!_bindings.TryAdd (eventArgs, binding)) { throw new InvalidOperationException (@$"A binding for {eventArgs} exists ({binding})."); } -#pragma warning restore CS8601 // Possible null reference assignment. - - // IMPORTANT: Add a COPY of the eventArgs. This is needed because ConfigurationManager.Apply uses DeepMemberWiseCopy - // IMPORTANT: update the memory referenced by the key, and Dictionary uses caching for performance, and thus - // IMPORTANT: Apply will update the Dictionary with the new eventArgs, but the old eventArgs will still be in the dictionary. - // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details. - _bindings.Add (eventArgs, binding); } /// /// Adds a new that will trigger the commands in . /// - /// If the is already bound to a different set of s it will be rebound + /// If the is already bound to a different set of s it will be + /// rebound /// . /// /// @@ -77,31 +70,32 @@ public abstract class InputBindings where TBinding : IInputBin throw new ArgumentException (@"At least one command must be specified", nameof (commands)); } - if (TryGet (eventArgs, out TBinding? binding)) + if (!IsValid (eventArgs)) + { + throw new ArgumentException (@"Invalid newEventArgs", nameof (eventArgs)); + } + + TBinding binding = _constructBinding (commands, eventArgs); + + if (!_bindings.TryAdd (eventArgs, binding)) { throw new InvalidOperationException (@$"A binding for {eventArgs} exists ({binding})."); } - - Add (eventArgs, _constructBinding (commands, eventArgs)); } - /// - /// Gets the bindings. - /// - /// - public IEnumerable> GetBindings () { return _bindings; } - /// Removes all objects from the collection. public void Clear () { _bindings.Clear (); } /// - /// Removes all bindings that trigger the given command set. Views can have multiple different + /// Removes all bindings that trigger the given command set. Views can have multiple different + /// /// bound to /// the same command sets and this method will clear all of them. /// /// public void Clear (params Command [] command) { + // ToArray() creates a snapshot to avoid modification during enumeration KeyValuePair [] kvps = _bindings .Where (kvp => kvp.Value.Commands.SequenceEqual (command)) .ToArray (); @@ -125,16 +119,29 @@ public abstract class InputBindings where TBinding : IInputBin throw new InvalidOperationException ($"{eventArgs} is not bound."); } - /// Gets the commands bound with the specified . - /// - /// The to check. - /// - /// When this method returns, contains the commands bound with the , if the is - /// not - /// found; otherwise, null. This parameter is passed uninitialized. - /// - /// if the is bound; otherwise . - public bool TryGet (TEvent eventArgs, out TBinding? binding) { return _bindings.TryGetValue (eventArgs, out binding); } + /// Gets all bound to the set of commands specified by . + /// The set of commands to search. + /// + /// The s bound to the set of commands specified by . An empty + /// list if + /// the + /// set of commands was not found. + /// + public IEnumerable GetAllFromCommands (params Command [] commands) + { + // ToList() creates a snapshot to ensure thread-safe enumeration + return _bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key).ToList (); + } + + /// + /// Gets the bindings. + /// + /// + public IEnumerable> GetBindings () + { + // ConcurrentDictionary provides a snapshot enumeration that is safe for concurrent access + return _bindings; + } /// Gets the array of s bound to if it exists. /// The to check. @@ -163,17 +170,16 @@ public abstract class InputBindings where TBinding : IInputBin /// public TEvent? GetFirstFromCommands (params Command [] commands) { return _bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; } - /// Gets all bound to the set of commands specified by . - /// The set of commands to search. - /// - /// The s bound to the set of commands specified by . An empty list if - /// the - /// set of commands was not found. - /// - public IEnumerable GetAllFromCommands (params Command [] commands) - { - return _bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key); - } + /// + /// Tests whether is valid or not. + /// + /// + /// + public abstract bool IsValid (TEvent eventArgs); + + /// Removes a from the collection. + /// + public void Remove (TEvent eventArgs) { _bindings.TryRemove (eventArgs, out _); } /// Replaces a combination already bound to a set of s. /// @@ -188,15 +194,28 @@ public abstract class InputBindings where TBinding : IInputBin throw new ArgumentException (@"Invalid newEventArgs", nameof (newEventArgs)); } - if (TryGet (oldEventArgs, out TBinding? binding)) + // Thread-safe: Handle the case where oldEventArgs == newEventArgs + if (EqualityComparer.Default.Equals (oldEventArgs, newEventArgs)) { - Remove (oldEventArgs); - Add (newEventArgs, binding!); - } - else - { - Add (newEventArgs, binding!); + // Same key - nothing to do, binding stays as-is + return; } + + // Thread-safe: Get the binding from oldEventArgs, or create default if it doesn't exist + // This is atomic - either gets existing or adds new + TBinding binding = _bindings.GetOrAdd (oldEventArgs, _ => new TBinding ()); + + // Thread-safe: Atomically add/update newEventArgs with the binding from oldEventArgs + // The updateValueFactory is only called if the key already exists, ensuring we don't + // accidentally overwrite a binding that was added by another thread + _bindings.AddOrUpdate ( + newEventArgs, + binding, // Add this binding if newEventArgs doesn't exist + (_, _) => binding); + + // Thread-safe: Remove oldEventArgs only after newEventArgs has been set + // This ensures we don't lose the binding if another thread is reading it + _bindings.TryRemove (oldEventArgs, out _); } /// Replaces the commands already bound to a combination of . @@ -209,28 +228,21 @@ public abstract class InputBindings where TBinding : IInputBin /// The set of commands to replace the old ones with. public void ReplaceCommands (TEvent eventArgs, params Command [] newCommands) { -#pragma warning disable CS8601 // Possible null reference assignment. - if (TryGet (eventArgs, out TBinding _)) - { - Remove (eventArgs); - Add (eventArgs, newCommands); - } - else - { - Add (eventArgs, newCommands); - } -#pragma warning restore CS8601 // Possible null reference assignment. + TBinding newBinding = _constructBinding (newCommands, eventArgs); + + // Thread-safe: Add or update atomically + _bindings.AddOrUpdate (eventArgs, newBinding, (_, _) => newBinding); } - /// Removes a from the collection. - /// - public void Remove (TEvent eventArgs) - { - if (!TryGet (eventArgs, out _)) - { - return; - } - - _bindings.Remove (eventArgs); - } + /// Gets the commands bound with the specified . + /// + /// The to check. + /// + /// When this method returns, contains the commands bound with the , if the + /// is + /// not + /// found; otherwise, null. This parameter is passed uninitialized. + /// + /// if the is bound; otherwise . + public bool TryGet (TEvent eventArgs, out TBinding? binding) { return _bindings.TryGetValue (eventArgs, out binding); } } diff --git a/Terminal.Gui/ViewBase/Runnable.cs b/Terminal.Gui/ViewBase/Runnable.cs index eedbc09f1..cb2fef4ca 100644 --- a/Terminal.Gui/ViewBase/Runnable.cs +++ b/Terminal.Gui/ViewBase/Runnable.cs @@ -92,7 +92,7 @@ public class Runnable : View, IRunnable /// // Or check if user wants to save first /// if (HasUnsavedChanges ()) /// { - /// int result = MessageBox.Query ("Save?", "Save changes?", "Yes", "No", "Cancel"); + /// int result = MessageBox.Query (App, "Save?", "Save changes?", "Yes", "No", "Cancel"); /// if (result == 2) return true; // Cancel stopping /// if (result == 0) Save (); /// } diff --git a/Terminal.Gui/ViewBase/RunnableWrapper.cs b/Terminal.Gui/ViewBase/RunnableWrapper.cs index 5f1d46136..6d030c600 100644 --- a/Terminal.Gui/ViewBase/RunnableWrapper.cs +++ b/Terminal.Gui/ViewBase/RunnableWrapper.cs @@ -8,7 +8,8 @@ namespace Terminal.Gui.ViewBase; /// The type of result data returned when the session completes. /// /// -/// This class enables any View to be run as a blocking session with +/// This class enables any View to be run as a blocking session with +/// /// without requiring the View to implement or derive from /// . /// diff --git a/Terminal.Gui/ViewBase/View.Diagnostics.cs b/Terminal.Gui/ViewBase/View.Diagnostics.cs index 19f77eac7..07379c5dc 100644 --- a/Terminal.Gui/ViewBase/View.Diagnostics.cs +++ b/Terminal.Gui/ViewBase/View.Diagnostics.cs @@ -2,6 +2,7 @@ public partial class View { + // TODO: Make this a configuration property /// Gets or sets whether diagnostic information will be drawn. This is a bit-field of .e diagnostics. /// /// diff --git a/Terminal.Gui/ViewBase/View.Drawing.Attribute.cs b/Terminal.Gui/ViewBase/View.Drawing.Attribute.cs index 81167c439..075b461f4 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.Attribute.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.Attribute.cs @@ -104,7 +104,7 @@ public partial class View /// /// Selects the specified Attribute - /// as the Attribute to use for subsequent calls to and . + /// as the Attribute to use for subsequent calls to and . /// /// THe Attribute to set. /// The previously set Attribute. @@ -112,7 +112,7 @@ public partial class View /// /// Selects the Attribute associated with the specified - /// as the Attribute to use for subsequent calls to and . + /// as the Attribute to use for subsequent calls to and . /// /// Calls to get the Attribute associated with the specified role, which will /// raise /. diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index 626af1852..b003d0c75 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -1,4 +1,3 @@ - #nullable disable namespace Terminal.Gui.Views; @@ -24,6 +23,9 @@ namespace Terminal.Gui.Views; /// public class Button : View, IDesignable { + private static ShadowStyle _defaultShadow = ShadowStyle.Opaque; // Resources/config.json overrides + private static MouseState _defaultHighlightStates = MouseState.In | MouseState.Pressed | MouseState.PressedOutside; // Resources/config.json overrides + private readonly Rune _leftBracket; private readonly Rune _leftDefault; private readonly Rune _rightBracket; @@ -34,13 +36,21 @@ public class Button : View, IDesignable /// Gets or sets whether s are shown with a shadow effect by default. /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static ShadowStyle DefaultShadow { get; set; } = ShadowStyle.Opaque; + public static ShadowStyle DefaultShadow + { + get => _defaultShadow; + set => _defaultShadow = value; + } /// /// Gets or sets the default Highlight Style. /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static MouseState DefaultHighlightStates { get; set; } = MouseState.In | MouseState.Pressed | MouseState.PressedOutside; + public static MouseState DefaultHighlightStates + { + get => _defaultHighlightStates; + set => _defaultHighlightStates = value; + } /// Initializes a new instance of . public Button () diff --git a/Terminal.Gui/Views/CharMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs index 370c50c6b..094b7a9ba 100644 --- a/Terminal.Gui/Views/CharMap/CharMap.cs +++ b/Terminal.Gui/Views/CharMap/CharMap.cs @@ -281,8 +281,8 @@ public class CharMap : View, IDesignable } } - private void CopyCodePoint () { Clipboard.Contents = $"U+{SelectedCodePoint:x5}"; } - private void CopyGlyph () { Clipboard.Contents = $"{new Rune (SelectedCodePoint)}"; } + private void CopyCodePoint () { App?.Clipboard?.SetClipboardData($"U+{SelectedCodePoint:x5}"); } + private void CopyGlyph () { App?.Clipboard?.SetClipboardData($"{new Rune (SelectedCodePoint)}"); } private bool? Move (ICommandContext? commandContext, int cpOffset) { @@ -335,7 +335,7 @@ public class CharMap : View, IDesignable [RequiresDynamicCode ("AOT")] private void ShowDetails () { - if (!Application.Initialized) + if (App is not { Initialized: true }) { // Some unit tests invoke Accept without Init return; @@ -380,15 +380,15 @@ public class CharMap : View, IDesignable try { decResponse = await client.GetCodepointDec (SelectedCodePoint).ConfigureAwait (false); - Application.Invoke ((_) => waitIndicator.RequestStop ()); + App?.Invoke ((_) => (s as Dialog)?.RequestStop ()); } catch (HttpRequestException e) { getCodePointError = errorLabel.Text = e.Message; - Application.Invoke ((_) => waitIndicator.RequestStop ()); + App?.Invoke ((_) => (s as Dialog)?.RequestStop ()); } }; - Application.Run (waitIndicator); + App?.Run (waitIndicator); waitIndicator.Dispose (); var name = string.Empty; @@ -521,7 +521,7 @@ public class CharMap : View, IDesignable dlg.Add (json); - Application.Run (dlg); + App?.Run (dlg); dlg.Dispose (); } diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index 2a0535469..0dfb47e51 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -1,5 +1,3 @@ - - namespace Terminal.Gui.Views; /// Shows a checkbox that can be cycled between two or three states. @@ -10,11 +8,17 @@ namespace Terminal.Gui.Views; /// public class CheckBox : View { + private static MouseState _defaultHighlightStates = MouseState.PressedOutside | MouseState.Pressed | MouseState.In; // Resources/config.json overrides + /// /// Gets or sets the default Highlight Style. /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static MouseState DefaultHighlightStates { get; set; } = MouseState.PressedOutside | MouseState.Pressed | MouseState.In; + public static MouseState DefaultHighlightStates + { + get => _defaultHighlightStates; + set => _defaultHighlightStates = value; + } /// /// Initializes a new instance of . diff --git a/Terminal.Gui/Views/CollectionNavigation/CollectionNavigator.cs b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigator.cs index c0eb7a312..ae3626fd1 100644 --- a/Terminal.Gui/Views/CollectionNavigation/CollectionNavigator.cs +++ b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigator.cs @@ -1,5 +1,5 @@ #nullable disable -using System.Collections; +using System.Collections; namespace Terminal.Gui.Views; @@ -7,6 +7,9 @@ namespace Terminal.Gui.Views; /// This implementation is based on a static of objects. internal class CollectionNavigator : CollectionNavigatorBase, IListCollectionNavigator { + private readonly object _collectionLock = new (); + private IList _collection; + /// Constructs a new CollectionNavigator. public CollectionNavigator () { } @@ -15,11 +18,39 @@ internal class CollectionNavigator : CollectionNavigatorBase, IListCollectionNav public CollectionNavigator (IList collection) { Collection = collection; } /// - public IList Collection { get; set; } + public IList Collection + { + get + { + lock (_collectionLock) + { + return _collection; + } + } + set + { + lock (_collectionLock) + { + _collection = value; + } + } + } /// - protected override object ElementAt (int idx) { return Collection [idx]; } + protected override object ElementAt (int idx) + { + lock (_collectionLock) + { + return Collection [idx]; + } + } /// - protected override int GetCollectionLength () { return Collection.Count; } + protected override int GetCollectionLength () + { + lock (_collectionLock) + { + return Collection.Count; + } + } } diff --git a/Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs index 7fd71f491..b7abad344 100644 --- a/Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs +++ b/Terminal.Gui/Views/CollectionNavigation/CollectionNavigatorBase.cs @@ -1,10 +1,9 @@ - - namespace Terminal.Gui.Views; /// internal abstract class CollectionNavigatorBase : ICollectionNavigator { + private readonly object _lock = new (); private DateTime _lastKeystroke = DateTime.Now; private string _searchString = ""; @@ -14,10 +13,20 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator /// public string SearchString { - get => _searchString; + get + { + lock (_lock) + { + return _searchString; + } + } private set { - _searchString = value; + lock (_lock) + { + _searchString = value; + } + OnSearchStringChanged (new (value)); } } @@ -40,15 +49,22 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator // but if we find none then we must fallback on cycling // d instead and discard the candidate state var candidateState = ""; - TimeSpan elapsedTime = DateTime.Now - _lastKeystroke; + TimeSpan elapsedTime; + string currentSearchString; + + lock (_lock) + { + elapsedTime = DateTime.Now - _lastKeystroke; + currentSearchString = _searchString; + } Logging.Debug ($"CollectionNavigator began processing '{keyStruck}', it has been {elapsedTime} since last keystroke"); // is it a second or third (etc) keystroke within a short time - if (SearchString.Length > 0 && elapsedTime < TimeSpan.FromMilliseconds (TypingDelay)) + if (currentSearchString.Length > 0 && elapsedTime < TimeSpan.FromMilliseconds (TypingDelay)) { // "dd" is a candidate - candidateState = SearchString + keyStruck; + candidateState = currentSearchString + keyStruck; Logging.Debug ($"Appending, search is now for '{candidateState}'"); } else @@ -72,7 +88,11 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator if (idxCandidate is { }) { // found "dd" so candidate search string is accepted - _lastKeystroke = DateTime.Now; + lock (_lock) + { + _lastKeystroke = DateTime.Now; + } + SearchString = candidateState; Logging.Debug ($"Found collection item that matched search:{idxCandidate}"); @@ -82,7 +102,11 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator //// nothing matches "dd" so discard it as a candidate //// and just cycle "d" instead - _lastKeystroke = DateTime.Now; + lock (_lock) + { + _lastKeystroke = DateTime.Now; + } + idxCandidate = GetNextMatchingItem (currentIndex, candidateState); Logging.Debug ($"CollectionNavigator searching (any match) matched:{idxCandidate}"); @@ -206,6 +230,10 @@ internal abstract class CollectionNavigatorBase : ICollectionNavigator private void ClearSearchString () { SearchString = ""; - _lastKeystroke = DateTime.Now; + + lock (_lock) + { + _lastKeystroke = DateTime.Now; + } } } diff --git a/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs b/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs index 400040e63..11609be67 100644 --- a/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs +++ b/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs @@ -37,7 +37,7 @@ public partial class ColorPicker { accept = true; e.Handled = true; - Application.RequestStop (); + (s as View)?.App?.RequestStop (); }; var btnCancel = new Button @@ -51,7 +51,7 @@ public partial class ColorPicker btnCancel.Accepting += (s, e) => { e.Handled = true; - Application.RequestStop (); + (s as View)?.App ?.RequestStop (); }; d.Add (btnOk); diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index 1a5b4b362..03797368d 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -1,4 +1,3 @@ - namespace Terminal.Gui.Views; /// @@ -11,10 +10,17 @@ namespace Terminal.Gui.Views; /// . This will execute the dialog until /// it terminates via the (`Esc` by default), /// or when one of the views or buttons added to the dialog calls -/// . +/// . /// public class Dialog : Window { + private static LineStyle _defaultBorderStyle = LineStyle.Heavy; // Resources/config.json overrides + private static Alignment _defaultButtonAlignment = Alignment.End; // Resources/config.json overrides + private static AlignmentModes _defaultButtonAlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems; // Resources/config.json overrides + private static int _defaultMinimumHeight = 80; // Resources/config.json overrides + private static int _defaultMinimumWidth = 80; // Resources/config.json overrides + private static ShadowStyle _defaultShadow = ShadowStyle.Transparent; // Resources/config.json overrides + /// /// Initializes a new instance of the class with no s. /// @@ -107,37 +113,61 @@ public class Dialog : Window /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public new static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Heavy; + public new static LineStyle DefaultBorderStyle + { + get => _defaultBorderStyle; + set => _defaultBorderStyle = value; + } /// The default for . /// This property can be set in a Theme. [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Alignment DefaultButtonAlignment { get; set; } = Alignment.End; + public static Alignment DefaultButtonAlignment + { + get => _defaultButtonAlignment; + set => _defaultButtonAlignment = value; + } /// The default for . /// This property can be set in a Theme. [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static AlignmentModes DefaultButtonAlignmentModes { get; set; } = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems; + public static AlignmentModes DefaultButtonAlignmentModes + { + get => _defaultButtonAlignmentModes; + set => _defaultButtonAlignmentModes = value; + } /// /// Defines the default minimum Dialog height, as a percentage of the container width. Can be configured via /// . /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static int DefaultMinimumHeight { get; set; } = 80; + public static int DefaultMinimumHeight + { + get => _defaultMinimumHeight; + set => _defaultMinimumHeight = value; + } /// /// Defines the default minimum Dialog width, as a percentage of the container width. Can be configured via /// . /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static int DefaultMinimumWidth { get; set; } = 80; + public static int DefaultMinimumWidth + { + get => _defaultMinimumWidth; + set => _defaultMinimumWidth = value; + } /// /// Gets or sets whether all s are shown with a shadow effect by default. /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public new static ShadowStyle DefaultShadow { get; set; } = ShadowStyle.Transparent; + public new static ShadowStyle DefaultShadow + { + get => _defaultShadow; + set => _defaultShadow = value; + } // Dialogs are Modal and Focus is indicated by their Border. The following code ensures the diff --git a/Terminal.Gui/Views/FileDialogs/DefaultFileOperations.cs b/Terminal.Gui/Views/FileDialogs/DefaultFileOperations.cs index 467e8d74c..25214cf17 100644 --- a/Terminal.Gui/Views/FileDialogs/DefaultFileOperations.cs +++ b/Terminal.Gui/Views/FileDialogs/DefaultFileOperations.cs @@ -7,7 +7,7 @@ namespace Terminal.Gui.Views; public class DefaultFileOperations : IFileOperations { /// - public bool Delete (IEnumerable toDelete) + public bool Delete (IApplication app, IEnumerable toDelete) { // Default implementation does not allow deleting multiple files if (toDelete.Count () != 1) @@ -18,7 +18,7 @@ public class DefaultFileOperations : IFileOperations IFileSystemInfo d = toDelete.Single (); string adjective = d.Name; - int result = MessageBox.Query ( + int? result = MessageBox.Query (app, string.Format (Strings.fdDeleteTitle, adjective), string.Format (Strings.fdDeleteBody, adjective), Strings.btnYes, @@ -43,14 +43,14 @@ public class DefaultFileOperations : IFileOperations } catch (Exception ex) { - MessageBox.ErrorQuery (Strings.fdDeleteFailedTitle, ex.Message, Strings.btnOk); + MessageBox.ErrorQuery (app, Strings.fdDeleteFailedTitle, ex.Message, Strings.btnOk); } return false; } /// - public IFileSystemInfo Rename (IFileSystem fileSystem, IFileSystemInfo toRename) + public IFileSystemInfo Rename (IApplication app, IFileSystem fileSystem, IFileSystemInfo toRename) { // Don't allow renaming C: or D: or / (on linux) etc if (toRename is IDirectoryInfo dir && dir.Parent is null) @@ -95,7 +95,7 @@ public class DefaultFileOperations : IFileOperations } catch (Exception ex) { - MessageBox.ErrorQuery (Strings.fdRenameFailedTitle, ex.Message, "Ok"); + MessageBox.ErrorQuery (app, Strings.fdRenameFailedTitle, ex.Message, "Ok"); } } } @@ -104,7 +104,7 @@ public class DefaultFileOperations : IFileOperations } /// - public IFileSystemInfo New (IFileSystem fileSystem, IDirectoryInfo inDirectory) + public IFileSystemInfo New (IApplication app, IFileSystem fileSystem, IDirectoryInfo inDirectory) { if (Prompt (Strings.fdNewTitle, "", out string named)) { @@ -122,7 +122,7 @@ public class DefaultFileOperations : IFileOperations } catch (Exception ex) { - MessageBox.ErrorQuery (Strings.fdNewFailed, ex.Message, "Ok"); + MessageBox.ErrorQuery (app, Strings.fdNewFailed, ex.Message, "Ok"); } } } @@ -138,7 +138,7 @@ public class DefaultFileOperations : IFileOperations btnOk.Accepting += (s, e) => { confirm = true; - Application.RequestStop (); + (s as View)?.App?.RequestStop (); // When Accepting is handled, set e.Handled to true to prevent further processing. e.Handled = true; }; @@ -147,7 +147,7 @@ public class DefaultFileOperations : IFileOperations btnCancel.Accepting += (s, e) => { confirm = false; - Application.RequestStop (); + (s as View)?.App?.RequestStop (); // When Accepting is handled, set e.Handled to true to prevent further processing. e.Handled = true; }; diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.cs b/Terminal.Gui/Views/FileDialogs/FileDialog.cs index a24c82f0a..b0dfeb9d6 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialog.cs @@ -108,7 +108,7 @@ public class FileDialog : Dialog, IDesignable if (Modal) { - Application.RequestStop (); + (s as View)?.App?.RequestStop (); } }; @@ -468,7 +468,6 @@ public class FileDialog : Dialog, IDesignable Style.IconProvider.IsOpenGetter = _treeView.IsExpanded; _treeView.AddObjects (_treeRoots.Keys); -#if MENU_V1 // if filtering on file type is configured then create the ComboBox and establish // initial filtering by extension(s) @@ -479,6 +478,7 @@ public class FileDialog : Dialog, IDesignable // Fiddle factor int width = AllowedTypes.Max (a => a.ToString ()!.Length) + 6; +#if MENU_V1 _allowedTypeMenu = new ( "", _allowedTypeMenuItems = AllowedTypes.Select ( @@ -512,8 +512,8 @@ public class FileDialog : Dialog, IDesignable }; Add (_allowedTypeMenuBar); - } #endif + } // if no path has been provided if (_tbPath.Text.Length <= 0) @@ -849,7 +849,7 @@ public class FileDialog : Dialog, IDesignable { IFileSystemInfo [] toDelete = GetFocusedFiles ()!; - if (FileOperationsHandler.Delete (toDelete)) + if (FileOperationsHandler.Delete (App, toDelete)) { RefreshState (); } @@ -879,7 +879,7 @@ public class FileDialog : Dialog, IDesignable if (Modal) { - Application.RequestStop (); + App?.RequestStop (); } } @@ -1039,7 +1039,7 @@ public class FileDialog : Dialog, IDesignable private void New () { { - IFileSystemInfo created = FileOperationsHandler.New (_fileSystem!, State!.Directory); + IFileSystemInfo created = FileOperationsHandler.New (App, _fileSystem!, State!.Directory); if (created is { }) { @@ -1174,13 +1174,13 @@ public class FileDialog : Dialog, IDesignable PushState (State, false, false, false); } - private void Rename () + private void Rename (IApplication? app) { IFileSystemInfo [] toRename = GetFocusedFiles ()!; if (toRename?.Length == 1) { - IFileSystemInfo newNamed = FileOperationsHandler.Rename (_fileSystem!, toRename.Single ()); + IFileSystemInfo newNamed = FileOperationsHandler.Rename (app, _fileSystem!, toRename.Single ()); if (newNamed is { }) { @@ -1230,7 +1230,7 @@ public class FileDialog : Dialog, IDesignable PopoverMenu? contextMenu = new ( [ new (Strings.fdCtxNew, string.Empty, New), - new (Strings.fdCtxRename, string.Empty, Rename), + new (Strings.fdCtxRename, string.Empty, () => Rename (App)), new (Strings.fdCtxDelete, string.Empty, Delete) ]); @@ -1327,7 +1327,7 @@ public class FileDialog : Dialog, IDesignable if (keyEvent.KeyCode == (KeyCode.CtrlMask | KeyCode.R)) { - Rename (); + Rename (App); return true; } diff --git a/Terminal.Gui/Views/FrameView.cs b/Terminal.Gui/Views/FrameView.cs index cd99731b9..db2c0c2df 100644 --- a/Terminal.Gui/Views/FrameView.cs +++ b/Terminal.Gui/Views/FrameView.cs @@ -1,5 +1,3 @@ - - namespace Terminal.Gui.Views; // TODO: FrameView is mis-named, really. It's far more about it being a TabGroup than a frame. @@ -19,6 +17,8 @@ namespace Terminal.Gui.Views; /// public class FrameView : View { + private static LineStyle _defaultBorderStyle = LineStyle.Rounded; // Resources/config.json overrides + /// /// Initializes a new instance of the class. /// layout. @@ -31,13 +31,17 @@ public class FrameView : View } /// - /// The default for 's border. The default is - /// . + /// Defines the default border styling for . Can be configured via + /// . /// /// /// This property can be set in a Theme to change the default for all /// s. /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Rounded; + public static LineStyle DefaultBorderStyle + { + get => _defaultBorderStyle; + set => _defaultBorderStyle = value; + } } diff --git a/Terminal.Gui/Views/GraphView/GraphView.cs b/Terminal.Gui/Views/GraphView/GraphView.cs index 69f708da8..03cda9fed 100644 --- a/Terminal.Gui/Views/GraphView/GraphView.cs +++ b/Terminal.Gui/Views/GraphView/GraphView.cs @@ -8,7 +8,6 @@ public class GraphView : View, IDesignable /// Creates a new graph with a 1 to 1 graph space with absolute layout. public GraphView () { - App = ApplicationImpl.Instance; CanFocus = true; AxisX = new (); diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index 67312b01e..7e45d2d6f 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -591,7 +591,7 @@ public class MenuBar : Menu, IDesignable { Title = "_File Settings...", HelpText = "More file settings", - Action = () => MessageBox.Query ( + Action = () => MessageBox.Query (App, "File Settings", "This is the File Settings Dialog\n", "_Ok", @@ -665,12 +665,12 @@ public class MenuBar : Menu, IDesignable new MenuItem { Title = "_Online Help...", - Action = () => MessageBox.Query ("Online Help", "https://gui-cs.github.io/Terminal.Gui", "Ok") + Action = () => MessageBox.Query (App, "Online Help", "https://gui-cs.github.io/Terminal.Gui", "Ok") }, new MenuItem { Title = "About...", - Action = () => MessageBox.Query ("About", "Something About Mary.", "Ok") + Action = () => MessageBox.Query (App, "About", "Something About Mary.", "Ok") } ] ) @@ -734,7 +734,7 @@ public class MenuBar : Menu, IDesignable { Title = "_Deeper Detail", Text = "Deeper Detail", - Action = () => { MessageBox.Query ("Deeper Detail", "Lots of details", "_Ok"); } + Action = () => { MessageBox.Query (App, "Deeper Detail", "Lots of details", "_Ok"); } }; var belowLineDetail = new MenuItem diff --git a/Terminal.Gui/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs index bdbf323a0..e9e874285 100644 --- a/Terminal.Gui/Views/MessageBox.cs +++ b/Terminal.Gui/Views/MessageBox.cs @@ -1,112 +1,205 @@ -#nullable disable - namespace Terminal.Gui.Views; /// -/// MessageBox displays a modal message to the user, with a title, a message and a series of options that the user -/// can choose from. +/// Displays a modal message box with a title, message, and buttons. Returns the index of the selected button, +/// or if the user cancels with . /// -/// -/// The difference between the and -/// method is the default set of colors used for the message box. -/// -/// -/// The following example pops up a with the specified title and text, plus two -/// s. The value -1 is returned when the user cancels the by pressing the -/// ESC key. -/// -/// -/// -/// var n = MessageBox.Query ("Quit Demo", "Are you sure you want to quit this demo?", "Yes", "No"); -/// if (n == 0) -/// quit = true; -/// else -/// quit = false; -/// -/// +/// +/// +/// MessageBox provides static methods for displaying modal dialogs with customizable buttons and messages. +/// All methods return where the value is the 0-based index of the button pressed, +/// or if the user pressed (typically Esc). +/// +/// +/// uses the default Dialog color scheme. +/// uses the Error color scheme. +/// +/// +/// Important: All MessageBox methods require an instance to be passed. +/// This enables proper modal dialog management and respects the application's lifecycle. Pass your +/// application instance (from ) or use the legacy +/// if using the static Application pattern. +/// +/// +/// Example using instance-based pattern: +/// +/// IApplication app = Application.Create(); +/// app.Init(); +/// +/// int? result = MessageBox.Query(app, "Quit Demo", "Are you sure you want to quit?", "Yes", "No"); +/// if (result == 0) // User clicked "Yes" +/// app.RequestStop(); +/// else if (result == null) // User pressed Esc +/// // Handle cancellation +/// +/// app.Shutdown(); +/// +/// +/// +/// Example using legacy static pattern: +/// +/// Application.Init(); +/// +/// int? result = MessageBox.Query(ApplicationImpl.Instance, "Quit Demo", "Are you sure?", "Yes", "No"); +/// if (result == 0) // User clicked "Yes" +/// Application.RequestStop(); +/// +/// Application.Shutdown(); +/// +/// +/// +/// The property provides a global variable alternative for web-based consoles +/// without SynchronizationContext. However, using the return value is preferred as it's more thread-safe +/// and follows modern async patterns. +/// +/// public static class MessageBox { + private static LineStyle _defaultBorderStyle = LineStyle.Heavy; // Resources/config.json overrides + private static Alignment _defaultButtonAlignment = Alignment.Center; // Resources/config.json overrides + private static int _defaultMinimumWidth = 0; // Resources/config.json overrides + private static int _defaultMinimumHeight = 0; // Resources/config.json overrides + /// /// Defines the default border styling for . Can be configured via /// . /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Heavy; + public static LineStyle DefaultBorderStyle + { + get => _defaultBorderStyle; + set => _defaultBorderStyle = value; + } /// The default for . /// This property can be set in a Theme. [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Alignment DefaultButtonAlignment { get; set; } = Alignment.Center; + public static Alignment DefaultButtonAlignment + { + get => _defaultButtonAlignment; + set => _defaultButtonAlignment = value; + } /// /// Defines the default minimum MessageBox width, as a percentage of the screen width. Can be configured via /// . /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static int DefaultMinimumWidth { get; set; } = 0; + public static int DefaultMinimumWidth + { + get => _defaultMinimumWidth; + set => _defaultMinimumWidth = value; + } /// /// Defines the default minimum Dialog height, as a percentage of the screen width. Can be configured via /// . /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static int DefaultMinimumHeight { get; set; } = 0; - /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. This is useful for web - /// based console where there is no SynchronizationContext or TaskScheduler. - /// - /// - /// Warning: This is a global variable and should be used with caution. It is not thread safe. - /// - public static int Clicked { get; private set; } = -1; + public static int DefaultMinimumHeight + { + get => _defaultMinimumHeight; + set => _defaultMinimumHeight = value; + } /// - /// Presents an error with the specified title and message and a list of buttons. + /// The index of the selected button, or if the user pressed . /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. + /// + /// This global variable is useful for web-based consoles without a SynchronizationContext or TaskScheduler. + /// Warning: Not thread-safe. + /// + public static int? Clicked { get; private set; } + + /// + /// Displays an error with fixed dimensions. + /// + /// The application instance. If , uses . /// Width for the MessageBox. /// Height for the MessageBox. /// Title for the MessageBox. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Array of buttons to add. + /// Message to display. May contain multiple lines and will be word-wrapped. + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . /// - /// Use instead; it automatically sizes the MessageBox based on - /// the contents. + /// Consider using which automatically sizes the + /// MessageBox. /// - public static int ErrorQuery (int width, int height, string title, string message, params string [] buttons) + public static int? ErrorQuery ( + IApplication? app, + int width, + int height, + string title, + string message, + params string [] buttons + ) { - return QueryFull (true, width, height, title, message, 0, true, buttons); + return QueryFull ( + app, + true, + width, + height, + title, + message, + 0, + true, + buttons); } /// - /// Presents an error with the specified title and message and a list of buttons to show - /// to the user. + /// Displays an auto-sized error . /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. - /// Title for the query. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Array of buttons to add. + /// The application instance. If , uses . + /// Title for the MessageBox. + /// Message to display. May contain multiple lines and will be word-wrapped. + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . /// - /// The message box will be vertically and horizontally centered in the container and the size will be - /// automatically determined from the size of the title, message. and buttons. + /// The MessageBox is centered and auto-sized based on title, message, and buttons. /// - public static int ErrorQuery (string title, string message, params string [] buttons) { return QueryFull (true, 0, 0, title, message, 0, true, buttons); } + public static int? ErrorQuery (IApplication? app, string title, string message, params string [] buttons) + { + return QueryFull ( + app, + true, + 0, + 0, + title, + message, + 0, + true, + buttons); + } /// - /// Presents an error with the specified title and message and a list of buttons. + /// Displays an error with fixed dimensions and a default button. /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. + /// The application instance. If , uses . /// Width for the MessageBox. /// Height for the MessageBox. /// Title for the MessageBox. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Index of the default button. - /// Array of buttons to add. + /// Message to display. May contain multiple lines and will be word-wrapped. + /// Index of the default button (0-based). + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . /// - /// Use instead; it automatically sizes the MessageBox based on - /// the contents. + /// Consider using which automatically sizes the + /// MessageBox. /// - public static int ErrorQuery ( + public static int? ErrorQuery ( + IApplication? app, int width, int height, string title, @@ -115,184 +208,73 @@ public static class MessageBox params string [] buttons ) { - return QueryFull (true, width, height, title, message, defaultButton, true, buttons); + return QueryFull ( + app, + true, + width, + height, + title, + message, + defaultButton, + true, + buttons); } /// - /// Presents an error with the specified title and message and a list of buttons to show - /// to the user. + /// Displays an auto-sized error with a default button. /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. + /// The application instance. If , uses . /// Title for the MessageBox. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Index of the default button. - /// Array of buttons to add. + /// Message to display. May contain multiple lines and will be word-wrapped. + /// Index of the default button (0-based). + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . /// - /// The message box will be vertically and horizontally centered in the container and the size will be - /// automatically determined from the size of the title, message. and buttons. + /// The MessageBox is centered and auto-sized based on title, message, and buttons. /// - public static int ErrorQuery (string title, string message, int defaultButton = 0, params string [] buttons) + public static int? ErrorQuery (IApplication? app, string title, string message, int defaultButton = 0, params string [] buttons) { - return QueryFull (true, 0, 0, title, message, defaultButton, true, buttons); + return QueryFull ( + app, + true, + 0, + 0, + title, + message, + defaultButton, + true, + buttons); } /// - /// Presents an error with the specified title and message and a list of buttons to show - /// to the user. + /// Displays an error with fixed dimensions, a default button, and word-wrap control. /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. - /// Width for the window. - /// Height for the window. - /// Title for the query. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Index of the default button. - /// If wrap the message or not. - /// Array of buttons to add. - /// - /// Use instead; it automatically sizes the MessageBox based on - /// the contents. - /// - public static int ErrorQuery ( - int width, - int height, - string title, - string message, - int defaultButton = 0, - bool wrapMessage = true, - params string [] buttons - ) - { - return QueryFull (true, width, height, title, message, defaultButton, wrapMessage, buttons); - } - - /// - /// Presents an error with the specified title and message and a list of buttons to show - /// to the user. - /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. - /// Title for the query. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Index of the default button. - /// If wrap the message or not. The default is - /// Array of buttons to add. - /// - /// The message box will be vertically and horizontally centered in the container and the size will be - /// automatically determined from the size of the title, message. and buttons. - /// - public static int ErrorQuery ( - string title, - string message, - int defaultButton = 0, - bool wrapMessage = true, - params string [] buttons - ) - { - return QueryFull (true, 0, 0, title, message, defaultButton, wrapMessage, buttons); - } - - /// - /// Presents a with the specified title and message and a list of buttons. - /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. + /// The application instance. If , uses . /// Width for the MessageBox. /// Height for the MessageBox. /// Title for the MessageBox. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Array of buttons to add. + /// Message to display. May contain multiple lines. + /// Index of the default button (0-based). + /// + /// If , word-wraps the message; otherwise displays as-is with multi-line + /// support. + /// + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . /// - /// Use instead; it automatically sizes the MessageBox based on - /// the contents. + /// Consider using which automatically + /// sizes the MessageBox. /// - public static int Query (int width, int height, string title, string message, params string [] buttons) - { - return QueryFull (false, width, height, title, message, 0, true, buttons); - } - - /// - /// Presents a with the specified title and message and a list of buttons. - /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. - /// Title for the MessageBox. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Array of buttons to add. - /// - /// - /// The message box will be vertically and horizontally centered in the container and the size will be - /// automatically determined from the size of the title, message. and buttons. - /// - /// - /// Use instead; it automatically sizes the MessageBox based on - /// the contents. - /// - /// - public static int Query (string title, string message, params string [] buttons) { return QueryFull (false, 0, 0, title, message, 0, true, buttons); } - - /// - /// Presents a with the specified title and message and a list of buttons. - /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. - /// Width for the window. - /// Height for the window. - /// Title for the MessageBox. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Index of the default button. - /// Array of buttons to add. - /// - /// - /// The message box will be vertically and horizontally centered in the container and the size will be - /// automatically determined from the size of the title, message. and buttons. - /// - /// - /// Use instead; it automatically sizes the MessageBox based on - /// the contents. - /// - /// - public static int Query ( - int width, - int height, - string title, - string message, - int defaultButton = 0, - params string [] buttons - ) - { - return QueryFull (false, width, height, title, message, defaultButton, true, buttons); - } - - /// - /// Presents a with the specified title and message and a list of buttons. - /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. - /// Title for the MessageBox. - /// Message to display; might contain multiple lines. The message will be word=wrapped by default. - /// Index of the default button. - /// Array of buttons to add. - /// - /// The message box will be vertically and horizontally centered in the container and the size will be - /// automatically determined from the size of the message and buttons. - /// - public static int Query (string title, string message, int defaultButton = 0, params string [] buttons) - { - return QueryFull (false, 0, 0, title, message, defaultButton, true, buttons); - } - - /// - /// Presents a with the specified title and message and a list of buttons to show - /// to the user. - /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. - /// Width for the window. - /// Height for the window. - /// Title for the query. - /// Message to display, might contain multiple lines. - /// Index of the default button. - /// If wrap the message or not. - /// Array of buttons to add. - /// - /// Use instead; it automatically sizes the MessageBox based on the - /// contents. - /// - public static int Query ( + public static int? ErrorQuery ( + IApplication? app, int width, int height, string title, @@ -302,20 +284,40 @@ public static class MessageBox params string [] buttons ) { - return QueryFull (false, width, height, title, message, defaultButton, wrapMessage, buttons); + return QueryFull ( + app, + true, + width, + height, + title, + message, + defaultButton, + wrapMessage, + buttons); } /// - /// Presents a with the specified title and message and a list of buttons to show - /// to the user. + /// Displays an auto-sized error with a default button and word-wrap control. /// - /// The index of the selected button, or -1 if the user pressed to close the MessageBox. - /// Title for the query. - /// Message to display, might contain multiple lines. - /// Index of the default button. - /// If wrap the message or not. - /// Array of buttons to add. - public static int Query ( + /// The application instance. If , uses . + /// Title for the MessageBox. + /// Message to display. May contain multiple lines. + /// Index of the default button (0-based). + /// + /// If , word-wraps the message; otherwise displays as-is with multi-line + /// support. + /// + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . + /// + /// The MessageBox is centered and auto-sized based on title, message, and buttons. + /// + public static int? ErrorQuery ( + IApplication? app, string title, string message, int defaultButton = 0, @@ -323,10 +325,239 @@ public static class MessageBox params string [] buttons ) { - return QueryFull (false, 0, 0, title, message, defaultButton, wrapMessage, buttons); + return QueryFull ( + app, + true, + 0, + 0, + title, + message, + defaultButton, + wrapMessage, + buttons); } - private static int QueryFull ( + /// + /// Displays a with fixed dimensions. + /// + /// The application instance. If , uses . + /// Width for the MessageBox. + /// Height for the MessageBox. + /// Title for the MessageBox. + /// Message to display. May contain multiple lines and will be word-wrapped. + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . + /// + /// Consider using which automatically sizes the + /// MessageBox. + /// + public static int? Query (IApplication? app, int width, int height, string title, string message, params string [] buttons) + { + return QueryFull ( + app, + false, + width, + height, + title, + message, + 0, + true, + buttons); + } + + /// + /// Displays an auto-sized . + /// + /// The application instance. If , uses . + /// Title for the MessageBox. + /// Message to display. May contain multiple lines and will be word-wrapped. + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . + /// + /// The MessageBox is centered and auto-sized based on title, message, and buttons. + /// + public static int? Query (IApplication? app, string title, string message, params string [] buttons) + { + return QueryFull ( + app, + false, + 0, + 0, + title, + message, + 0, + true, + buttons); + } + + /// + /// Displays a with fixed dimensions and a default button. + /// + /// The application instance. If , uses . + /// Width for the MessageBox. + /// Height for the MessageBox. + /// Title for the MessageBox. + /// Message to display. May contain multiple lines and will be word-wrapped. + /// Index of the default button (0-based). + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . + /// + /// Consider using which automatically sizes the + /// MessageBox. + /// + public static int? Query ( + IApplication? app, + int width, + int height, + string title, + string message, + int defaultButton = 0, + params string [] buttons + ) + { + return QueryFull ( + app, + false, + width, + height, + title, + message, + defaultButton, + true, + buttons); + } + + /// + /// Displays an auto-sized with a default button. + /// + /// The application instance. If , uses . + /// Title for the MessageBox. + /// Message to display. May contain multiple lines and will be word-wrapped. + /// Index of the default button (0-based). + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . + /// + /// The MessageBox is centered and auto-sized based on title, message, and buttons. + /// + public static int? Query (IApplication? app, string title, string message, int defaultButton = 0, params string [] buttons) + { + return QueryFull ( + app, + false, + 0, + 0, + title, + message, + defaultButton, + true, + buttons); + } + + /// + /// Displays a with fixed dimensions, a default button, and word-wrap control. + /// + /// The application instance. If , uses . + /// Width for the MessageBox. + /// Height for the MessageBox. + /// Title for the MessageBox. + /// Message to display. May contain multiple lines. + /// Index of the default button (0-based). + /// + /// If , word-wraps the message; otherwise displays as-is with multi-line + /// support. + /// + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . + /// + /// Consider using which automatically sizes + /// the MessageBox. + /// + public static int? Query ( + IApplication? app, + int width, + int height, + string title, + string message, + int defaultButton = 0, + bool wrapMessage = true, + params string [] buttons + ) + { + return QueryFull ( + app, + false, + width, + height, + title, + message, + defaultButton, + wrapMessage, + buttons); + } + + /// + /// Displays an auto-sized with a default button and word-wrap control. + /// + /// The application instance. If , uses . + /// Title for the MessageBox. + /// Message to display. May contain multiple lines. + /// Index of the default button (0-based). + /// + /// If , word-wraps the message; otherwise displays as-is with multi-line + /// support. + /// + /// Array of button labels. + /// + /// The index of the selected button, or if the user pressed + /// . + /// + /// Thrown if is . + /// + /// The MessageBox is centered and auto-sized based on title, message, and buttons. + /// + public static int? Query ( + IApplication? app, + string title, + string message, + int defaultButton = 0, + bool wrapMessage = true, + params string [] buttons + ) + { + return QueryFull ( + app, + false, + 0, + 0, + title, + message, + defaultButton, + wrapMessage, + buttons); + } + + private static int? QueryFull ( + IApplication? app, bool useErrorColors, int width, int height, @@ -337,10 +568,12 @@ public static class MessageBox params string [] buttons ) { + ArgumentNullException.ThrowIfNull (app); + // Create button array for Dialog var count = 0; List public class StatusBar : Bar, IDesignable { + private static LineStyle _defaultSeparatorLineStyle = LineStyle.Single; // Resources/config.json overrides + /// public StatusBar () : this ([]) { } @@ -55,7 +57,11 @@ public class StatusBar : Bar, IDesignable /// Gets or sets the default Line Style for the separators between the shortcuts of the StatusBar. /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultSeparatorLineStyle { get; set; } = LineStyle.Single; + public static LineStyle DefaultSeparatorLineStyle + { + get => _defaultSeparatorLineStyle; + set => _defaultSeparatorLineStyle = value; + } /// protected override void OnSubViewLayout (LayoutEventArgs args) @@ -160,7 +166,7 @@ public class StatusBar : Bar, IDesignable return true; - void OnButtonClicked (object? sender, EventArgs? e) { MessageBox.Query ("Hi", $"You clicked {sender}"); } + void OnButtonClicked (object? sender, EventArgs? e) { MessageBox.Query (App, "Hi", $"You clicked {sender}"); } } /// diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index 1c5f298e0..5d1e79f7f 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -1534,7 +1534,7 @@ public class TableView : View, IDesignable /// private void ClearLine (int row, int width) { - if (Application.Screen.Height == 0) + if (App?.Screen.Height == 0) { return; } @@ -1810,7 +1810,7 @@ public class TableView : View, IDesignable } } - if (Application.Screen.Height > 0) + if (App?.Screen.Height > 0) { AddRuneAt (c, row, rune); } diff --git a/Terminal.Gui/Views/TextInput/TextField.cs b/Terminal.Gui/Views/TextInput/TextField.cs index c4aa19754..794b09b40 100644 --- a/Terminal.Gui/Views/TextInput/TextField.cs +++ b/Terminal.Gui/Views/TextInput/TextField.cs @@ -617,7 +617,7 @@ public class TextField : View, IDesignable return; } - Clipboard.Contents = SelectedText; + App?.Clipboard?.SetClipboardData (SelectedText); } /// Cut the selected text to the clipboard. @@ -628,7 +628,7 @@ public class TextField : View, IDesignable return; } - Clipboard.Contents = SelectedText; + App?.Clipboard?.SetClipboardData (SelectedText); List newText = DeleteSelectedText (); Text = StringExtensions.ToString (newText); Adjust (); @@ -1079,7 +1079,7 @@ public class TextField : View, IDesignable return; } - string cbTxt = Clipboard.Contents.Split ("\n") [0] ?? ""; + string cbTxt = App?.Clipboard?.GetClipboardData ()?.Split ("\n") [0]; if (string.IsNullOrEmpty (cbTxt)) { @@ -1731,9 +1731,9 @@ public class TextField : View, IDesignable private void SetClipboard (IEnumerable text) { - if (!Secret) + if (!Secret && App?.Clipboard is { }) { - Clipboard.Contents = StringExtensions.ToString (text.ToList ()); + App.Clipboard.SetClipboardData (StringExtensions.ToString (text.ToList ())); } } diff --git a/Terminal.Gui/Views/TextInput/TextView.cs b/Terminal.Gui/Views/TextInput/TextView.cs index 5e2124d38..23fd1e89a 100644 --- a/Terminal.Gui/Views/TextInput/TextView.cs +++ b/Terminal.Gui/Views/TextInput/TextView.cs @@ -1960,7 +1960,7 @@ public class TextView : View, IDesignable } SetWrapModel (); - string? contents = Clipboard.Contents; + string? contents = App?.Clipboard?.GetClipboardData (); if (_copyWithoutSelection && contents!.FirstOrDefault (x => x is '\n' or '\r') == 0) { @@ -2363,7 +2363,7 @@ public class TextView : View, IDesignable OnUnwrappedCursorPosition (); } - private void AppendClipboard (string text) { Clipboard.Contents += text; } + private void AppendClipboard (string text) { App?.Clipboard?.SetClipboardData (App?.Clipboard?.GetClipboardData () + text); } private PopoverMenu CreateContextMenu () { @@ -3842,7 +3842,7 @@ public class TextView : View, IDesignable List currentLine = GetCurrentLine (); - if (currentLine.Count > 0 && currentLine[CurrentColumn - 1].Grapheme == "\t") + if (currentLine.Count > 0 && currentLine [CurrentColumn - 1].Grapheme == "\t") { _historyText.Add (new () { new (currentLine) }, CursorPosition); @@ -4470,7 +4470,7 @@ public class TextView : View, IDesignable { if (text is { }) { - Clipboard.Contents = text; + App?.Clipboard?.SetClipboardData (text); } } diff --git a/Terminal.Gui/Views/Window.cs b/Terminal.Gui/Views/Window.cs index b374459a2..597ba121d 100644 --- a/Terminal.Gui/Views/Window.cs +++ b/Terminal.Gui/Views/Window.cs @@ -1,5 +1,3 @@ - - namespace Terminal.Gui.Views; /// @@ -18,6 +16,9 @@ namespace Terminal.Gui.Views; /// public class Window : Toplevel { + private static ShadowStyle _defaultShadow = ShadowStyle.None; // Resources/config.json overrides + private static LineStyle _defaultBorderStyle = LineStyle.Single; // Resources/config.json overrides + /// /// Initializes a new instance of the class. /// @@ -35,7 +36,11 @@ public class Window : Toplevel /// Gets or sets whether all s are shown with a shadow effect by default. /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static ShadowStyle DefaultShadow { get; set; } = ShadowStyle.None; + public static ShadowStyle DefaultShadow + { + get => _defaultShadow; + set => _defaultShadow = value; + } // TODO: enable this ///// @@ -56,5 +61,9 @@ public class Window : Toplevel /// s. /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single; + public static LineStyle DefaultBorderStyle + { + get => _defaultBorderStyle; + set => _defaultBorderStyle = value; + } } diff --git a/Terminal.Gui/Views/Wizard/Wizard.cs b/Terminal.Gui/Views/Wizard/Wizard.cs index 50b21c7ed..3415c572a 100644 --- a/Terminal.Gui/Views/Wizard/Wizard.cs +++ b/Terminal.Gui/Views/Wizard/Wizard.cs @@ -458,7 +458,7 @@ public class Wizard : Dialog if (IsCurrentTop) { - Application.RequestStop (this); + (sender as View)?.App?.RequestStop (this); e.Handled = true; } diff --git a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs index 6cc94ec45..50aba981c 100644 --- a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs @@ -60,10 +60,10 @@ public class FileDialogFluentTests public void CancelFileDialog_QuitKey_Quits (TestDriver d) { SaveDialog? sd = null; - using var c = With.A (() => NewSaveDialog (out sd), 100, 20, d) - .ScreenShot ("Save dialog", _out) - .EnqueueKeyEvent (Application.QuitKey) - .AssertTrue (sd!.Canceled); + using GuiTestContext c = With.A (() => NewSaveDialog (out sd), 100, 20, d, logWriter: _out) + .ScreenShot ("Save dialog", _out) + .EnqueueKeyEvent (Application.QuitKey) + .AssertTrue (sd!.Canceled); } [Theory] @@ -93,7 +93,7 @@ public class FileDialogFluentTests public void CancelFileDialog_UsingCancelButton_AltC (TestDriver d) { SaveDialog? sd = null; - using var c = With.A (() => NewSaveDialog (out sd), 100, 20, d) + using var c = With.A (() => NewSaveDialog (out sd), 100, 20, d, _out) .ScreenShot ("Save dialog", _out) .EnqueueKeyEvent (Key.C.WithAlt) .AssertTrue (sd!.Canceled); @@ -132,12 +132,13 @@ public class FileDialogFluentTests { SaveDialog? sd = null; MockFileSystem? fs = null; - using var c = With.A (() => NewSaveDialog (out sd, out fs, modal: false), 100, 20, d) - .ScreenShot ("Save dialog", _out) - .Focus