diff --git a/Example/demo.cs b/Example/demo.cs index 53974b3c0..95a5726e6 100644 --- a/Example/demo.cs +++ b/Example/demo.cs @@ -190,7 +190,7 @@ static class Demo { new DateField (3, 22, DateTime.Now), new DateField (23, 22, DateTime.Now, true), progress, - new Label (3, 24, "Press F9 (on Unix, ESC+9 is an alias) to activate the menubar"), + new Label (3, 24, "Press F9 (on Unix, ESC+9 is an alias) or Ctrl+T to activate the menubar"), menuKeysStyle, menuAutoMouseNav @@ -636,10 +636,23 @@ static class Demo { }; #endif + win.KeyPress += Win_KeyPress; + top.Add (win); //top.Add (menu); top.Add (menu, statusBar); Application.Run (); } + + private static void Win_KeyPress (object sender, View.KeyEventEventArgs e) + { + if (e.KeyEvent.Key == Key.ControlT) { + if (menu.IsMenuOpen) + menu.CloseMenu (); + else + menu.OpenMenu (); + e.Handled = true; + } + } } diff --git a/Terminal.Gui/Core.cs b/Terminal.Gui/Core.cs index c6568fb3e..a81647195 100644 --- a/Terminal.Gui/Core.cs +++ b/Terminal.Gui/Core.cs @@ -1087,6 +1087,11 @@ namespace Terminal.Gui { /// The for the event. /// public KeyEvent KeyEvent { get; set; } + /// + /// Indicates if the current Key event has already been processed and the driver should stop notifying any other event subscriber. + /// Its important to set this value to true specially when updating any View's layout from inside the subscriber method. + /// + public bool Handled { get; set; } = false; } /// @@ -1097,7 +1102,11 @@ namespace Terminal.Gui { /// public override bool ProcessKey (KeyEvent keyEvent) { - KeyPress?.Invoke (this, new KeyEventEventArgs(keyEvent)); + + KeyEventEventArgs args = new KeyEventEventArgs (keyEvent); + KeyPress?.Invoke (this, args); + if (args.Handled) + return true; if (Focused?.ProcessKey (keyEvent) == true) return true; @@ -1107,7 +1116,10 @@ namespace Terminal.Gui { /// public override bool ProcessHotKey (KeyEvent keyEvent) { - KeyPress?.Invoke (this, new KeyEventEventArgs (keyEvent)); + KeyEventEventArgs args = new KeyEventEventArgs (keyEvent); + KeyPress?.Invoke (this, args); + if (args.Handled) + return true; if (subviews == null || subviews.Count == 0) return false; foreach (var view in subviews) @@ -1119,7 +1131,10 @@ namespace Terminal.Gui { /// public override bool ProcessColdKey (KeyEvent keyEvent) { - KeyPress?.Invoke (this, new KeyEventEventArgs(keyEvent)); + KeyEventEventArgs args = new KeyEventEventArgs (keyEvent); + KeyPress?.Invoke (this, args); + if (args.Handled) + return true; if (subviews == null || subviews.Count == 0) return false; foreach (var view in subviews) @@ -1136,7 +1151,10 @@ namespace Terminal.Gui { /// Contains the details about the key that produced the event. public override bool OnKeyDown (KeyEvent keyEvent) { - KeyDown?.Invoke (this, new KeyEventEventArgs (keyEvent)); + KeyEventEventArgs args = new KeyEventEventArgs (keyEvent); + KeyDown?.Invoke (this, args); + if (args.Handled) + return true; if (subviews == null || subviews.Count == 0) return false; foreach (var view in subviews) @@ -1154,7 +1172,10 @@ namespace Terminal.Gui { /// Contains the details about the key that produced the event. public override bool OnKeyUp (KeyEvent keyEvent) { - KeyUp?.Invoke (this, new KeyEventEventArgs (keyEvent)); + KeyEventEventArgs args = new KeyEventEventArgs (keyEvent); + KeyUp?.Invoke (this, args); + if (args.Handled) + return true; if (subviews == null || subviews.Count == 0) return false; foreach (var view in subviews) diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs index da16a08ef..5f25cb797 100644 --- a/Terminal.Gui/Views/Menu.cs +++ b/Terminal.Gui/Views/Menu.cs @@ -784,7 +784,7 @@ namespace Terminal.Gui { /// /// Closes the current Menu programatically, if open. /// - public void CloseMenu() + public void CloseMenu () { CloseMenu (false, false); } diff --git a/UICatalog/Program.cs b/UICatalog/Program.cs deleted file mode 100644 index 86ad5405d..000000000 --- a/UICatalog/Program.cs +++ /dev/null @@ -1,280 +0,0 @@ -using NStack; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using Terminal.Gui; - -namespace UICatalog { - /// - /// Main program for the Terminal.gui UI Catalog app. This app provides a chooser that allows - /// for a calalog of UI demos, examples, and tests. - /// - internal class Program { - private static Toplevel _top; - private static MenuBar _menu; - private static int _nameColumnWidth; - private static Window _leftPane; - private static List _categories; - private static ListView _categoryListView; - private static Window _rightPane; - private static List _scenarios; - private static ListView _scenarioListView; - private static StatusBar _statusBar; - - private static Scenario _selectedScenario = null; - - static void Main (string [] args) - { - if (Debugger.IsAttached) - CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); - - _scenarios = Scenario.GetDerivedClassesCollection ().OrderBy (t => Scenario.ScenarioMetadata.GetName (t)).ToList(); - - if (args.Length > 0) { - var item = _scenarios.FindIndex (t => Scenario.ScenarioMetadata.GetName (t).Equals (args [0], StringComparison.OrdinalIgnoreCase)); - _selectedScenario = (Scenario)Activator.CreateInstance (_scenarios [item]); - _selectedScenario.Init (Application.Top); - _selectedScenario.Setup (); - _selectedScenario.Run (); - _selectedScenario = null; - return; - } - - Scenario scenario = GetScenarioToRun (); - while (scenario != null) { - scenario.Init (Application.Top); - scenario.Setup (); - scenario.Run (); - scenario = GetScenarioToRun (); - } - - } - - /// - /// Create all controls. This gets called once and the controls remain with their state between Sceanrio runs. - /// - private static void Setup () - { - _menu = new MenuBar (new MenuBarItem [] { - new MenuBarItem ("_File", new MenuItem [] { - new MenuItem ("_Quit", "", () => Application.RequestStop() ) - }), - new MenuBarItem ("_About...", "About this app", () => MessageBox.Query (0, 10, "About UI Catalog", "UI Catalog is a comprehensive sample library for Terminal.Gui", "Ok")), - }); - - _leftPane = new Window ("Categories") { - X = 0, - Y = 1, // for menu - Width = 25, - Height = Dim.Fill (), - CanFocus = false, - }; - - - _categories = Scenario.GetAllCategories ().OrderBy(c => c).ToList(); - _categoryListView = new ListView (_categories) { - X = 1, - Y = 0, - Width = Dim.Fill (0), - Height = Dim.Fill (2), - AllowsMarking = false, - CanFocus = true, - }; - _categoryListView.OpenSelectedItem += (o, a) => { - _top.SetFocus (_rightPane); - }; - _categoryListView.SelectedChanged += CategoryListView_SelectedChanged; - _leftPane.Add (_categoryListView); - - _rightPane = new Window ("Scenarios") { - X = 25, - Y = 1, // for menu - Width = Dim.Fill (), - Height = Dim.Fill (), - CanFocus = false, - - }; - - _nameColumnWidth = Scenario.ScenarioMetadata.GetName (_scenarios.OrderByDescending (t => Scenario.ScenarioMetadata.GetName (t).Length).FirstOrDefault ()).Length; - - _scenarioListView = new ListView () { - X = 0, - Y = 0, - Width = Dim.Fill (0), - Height = Dim.Fill (0), - AllowsMarking = false, - CanFocus = true, - }; - - //_scenarioListView.OnKeyPress += (KeyEvent ke) => { - // if (_top.MostFocused == _scenarioListView && ke.Key == Key.Enter) { - // _scenarioListView_OpenSelectedItem (null, null); - // } - //}; - - _scenarioListView.OpenSelectedItem += _scenarioListView_OpenSelectedItem; - _rightPane.Add (_scenarioListView); - - _categoryListView.SelectedItem = 0; - _categoryListView.OnSelectedChanged (); - - _statusBar = new StatusBar (new StatusItem [] { - //new StatusItem(Key.F1, "~F1~ Help", () => Help()), - new StatusItem(Key.ControlQ, "~CTRL-Q~ Quit", () => { - if (_selectedScenario is null){ - // This causes GetScenarioToRun to return null - _selectedScenario = null; - Application.RequestStop(); - } else { - _selectedScenario.RequestStop(); - } - }), - }); - } - - /// - /// This shows the selection UI. Each time it is run, it calls Application.Init to reset everything. - /// - /// - private static Scenario GetScenarioToRun () - { - Application.Init (); - - if (_menu == null) { - Setup (); - } - - _top = Application.Top; - - _top.KeyUp += KeyUpHandler; - - _top.Add (_menu); - _top.Add (_leftPane); - _top.Add (_rightPane); - _top.Add (_statusBar); - - // HACK: There is no other way to SetFocus before Application.Run. See Issue #445 -#if false - if (_runningScenario != null) - Application.Iteration += Application_Iteration; -#else - _top.Ready += (o, a) => { - if (_selectedScenario != null) { - _top.SetFocus (_rightPane); - _selectedScenario = null; - } - }; -#endif - - Application.Run (_top); - Application.Shutdown (); - return _selectedScenario; - } - -#if false - private static void Application_Iteration (object sender, EventArgs e) - { - Application.Iteration -= Application_Iteration; - _top.SetFocus (_rightPane); - } -#endif - private static void _scenarioListView_OpenSelectedItem (object sender, EventArgs e) - { - if (_selectedScenario is null) { - var source = _scenarioListView.Source as ScenarioListDataSource; - _selectedScenario = (Scenario)Activator.CreateInstance (source.Scenarios [_scenarioListView.SelectedItem]); - Application.RequestStop (); - } - } - - internal class ScenarioListDataSource : IListDataSource { - public List Scenarios { get; set; } - - public bool IsMarked (int item) => false;// Scenarios [item].IsMarked; - - public int Count => Scenarios.Count; - - public ScenarioListDataSource (List itemList) => Scenarios = itemList; - - public void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width) - { - container.Move (col, line); - // Equivalent to an interpolated string like $"{Scenarios[item].Name, -widtestname}"; if such a thing were possible - var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenario.ScenarioMetadata.GetName (Scenarios [item])); - RenderUstr (driver, $"{s} {Scenario.ScenarioMetadata.GetDescription (Scenarios [item])}", col, line, width); - } - - public void SetMark (int item, bool value) - { - } - - // A slightly adapted method from: https://github.com/migueldeicaza/gui.cs/blob/fc1faba7452ccbdf49028ac49f0c9f0f42bbae91/Terminal.Gui/Views/ListView.cs#L433-L461 - private void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width) - { - int used = 0; - int index = 0; - while (index < ustr.Length) { - (var rune, var size) = Utf8.DecodeRune (ustr, index, index - ustr.Length); - var count = Rune.ColumnWidth (rune); - if (used + count >= width) break; - driver.AddRune (rune); - used += count; - index += size; - } - - while (used < width) { - driver.AddRune (' '); - used++; - } - } - - public IList ToList () - { - return Scenarios; - } - - } - - /// - /// When Scenarios are running we need to override the behavior of the Menu - /// and Statusbar to enable Scenarios that use those (or related key input) - /// to not be impacted. Same as for tabs. - /// - /// - private static void KeyUpHandler (object sender, View.KeyEventEventArgs a) - { - if (_selectedScenario != null) { - //switch (ke.Key) { - //case Key.Esc: - // //_runningScenario.RequestStop (); - // break; - //case Key.Enter: - // break; - //}< - } else if (a.KeyEvent.Key == Key.Tab || a.KeyEvent.Key == Key.BackTab) { - // BUGBUG: Work around Issue #434 by implementing our own TAB navigation - if (_top.MostFocused == _categoryListView) - _top.SetFocus (_rightPane); - else - _top.SetFocus (_leftPane); - } - } - - private static void CategoryListView_SelectedChanged (object sender, ListViewItemEventArgs e) - { - var item = _categories [_categoryListView.SelectedItem]; - List newlist; - if (item.Equals ("All")) { - newlist = _scenarios; - - } else { - newlist = _scenarios.Where (t => Scenario.ScenarioCategory.GetCategories (t).Contains (item)).ToList (); - } - _scenarioListView.Source = new ScenarioListDataSource (newlist); - _scenarioListView.SelectedItem = 0; - } - } -} diff --git a/UICatalog/Scenarios/Threading.cs b/UICatalog/Scenarios/Threading.cs new file mode 100644 index 000000000..28b2a72d7 --- /dev/null +++ b/UICatalog/Scenarios/Threading.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Terminal.Gui; + +namespace UICatalog { + [ScenarioMetadata (Name: "Threading", Description: "Demonstration of how to use threading in different ways")] + [ScenarioCategory ("Threading")] + class Threading : Scenario { + private Action _action; + private Action _lambda; + private EventHandler _handler; + private Action _sync; + + private ListView _itemsList; + private Button _btnActionCancel; + List log = new List (); + private ListView _logJob; + + public override void Setup () + { + _action = LoadData; + _lambda = async () => { + _itemsList.Source = null; + LogJob ("Loading task lambda"); + var items = await LoadDataAsync (); + LogJob ("Returning from task lambda"); + _itemsList.SetSource (items); + }; + _handler = async (s, e) => { + _itemsList.Source = null; + LogJob ("Loading task handler"); + var items = await LoadDataAsync (); + LogJob ("Returning from task handler"); + _itemsList.SetSource (items); + + }; + _sync = () => { + _itemsList.Source = null; + LogJob ("Loading task synchronous"); + List items = new List () { "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten" }; + LogJob ("Returning from task synchronous"); + _itemsList.SetSource (items); + }; + + Application.Init (); + + _btnActionCancel = new Button (1, 1, "Cancelable Load Items"); + _btnActionCancel.Clicked += () => Application.MainLoop.Invoke (CallLoadItemsAsync); + + _itemsList = new ListView { + X = Pos.X (_btnActionCancel), + Y = Pos.Y (_btnActionCancel) + 4, + Width = 10, + Height = 10 + }; + + _logJob = new ListView (log) { + X = Pos.Right (_itemsList) + 10, + Y = Pos.Y (_itemsList), + Width = 50, + Height = Dim.Fill () + }; + + var text = new TextField (1, 3, 100, "Type anything after press the button"); + + var _btnAction = new Button (80, 10, "Load Data Action"); + _btnAction.Clicked += () => _action.Invoke (); + var _btnLambda = new Button (80, 12, "Load Data Lambda"); + _btnLambda.Clicked += () => _lambda.Invoke (); + var _btnHandler = new Button (80, 14, "Load Data Handler"); + _btnHandler.Clicked += () => _handler.Invoke (null, new EventArgs ()); + var _btnSync = new Button (80, 16, "Load Data Synchronous"); + _btnSync.Clicked += () => _sync.Invoke (); + var _btnMethod = new Button (80, 18, "Load Data Method"); + _btnMethod.Clicked += async () => await MethodAsync (); + var _btnClearData = new Button (80, 20, "Clear Data"); + _btnClearData.Clicked += () => { _itemsList.Source = null; LogJob ("Cleaning Data"); }; + var _btnQuit = new Button (80, 22, "Quit"); + _btnQuit.Clicked += Application.RequestStop; + + Win.Add (_itemsList, _btnActionCancel, _logJob, text, _btnAction, _btnLambda, _btnHandler, _btnSync, _btnMethod, _btnClearData, _btnQuit); + Application.Top.Add (Win); + Application.Run (); + + } + + private async void LoadData () + { + _itemsList.Source = null; + LogJob ("Loading task"); + var items = await LoadDataAsync (); + LogJob ("Returning from task"); + _itemsList.SetSource (items); + } + + private void LogJob (string job) + { + log.Add (job); + _logJob.MoveDown (); + } + + private async Task> LoadDataAsync () + { + _itemsList.Source = null; + LogJob ("Starting delay"); + await Task.Delay (3000); + LogJob ("Finished delay"); + return new List () { "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten" }; + } + + private async Task MethodAsync () + { + _itemsList.Source = null; + LogJob ("Loading task method"); + List items = new List () { "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten" }; + await Task.Delay (3000); + LogJob ("Returning from task method"); + await _itemsList.SetSourceAsync (items); + } + + private CancellationTokenSource cancellationTokenSource; + + private async void CallLoadItemsAsync () + { + cancellationTokenSource = new CancellationTokenSource (); + _itemsList.Source = null; + LogJob ($"Clicked the button"); + if (_btnActionCancel.Text == "Cancel") { + _btnActionCancel.Text = "Cancelable Load Items"; + cancellationTokenSource.Cancel (); + } else + _btnActionCancel.Text = "Cancel"; + try { + if (cancellationTokenSource.Token.IsCancellationRequested) + cancellationTokenSource.Token.ThrowIfCancellationRequested (); + LogJob ($"Calling task Thread:{Thread.CurrentThread.ManagedThreadId} {DateTime.Now}"); + var items = await Task.Run (LoadItemsAsync, cancellationTokenSource.Token); + if (!cancellationTokenSource.IsCancellationRequested) { + LogJob ($"Returned from task Thread:{Thread.CurrentThread.ManagedThreadId} {DateTime.Now}"); + _itemsList.SetSource (items); + LogJob ($"Finished populate list view Thread:{Thread.CurrentThread.ManagedThreadId} {DateTime.Now}"); + _btnActionCancel.Text = "Load Items"; + } else { + LogJob ("Task was canceled!"); + } + } catch (OperationCanceledException ex) { + LogJob (ex.Message); + } + } + + private async Task> LoadItemsAsync () + { + // Do something that takes lot of times. + LogJob ($"Starting delay Thread:{Thread.CurrentThread.ManagedThreadId} {DateTime.Now}"); + await Task.Delay (5000); + LogJob ($"Finished delay Thread:{Thread.CurrentThread.ManagedThreadId} {DateTime.Now}"); + return new List () { "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten" }; + } + } +}