diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..d27ace4f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +root = true + + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf298386..feb12c7b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,19 +4,22 @@ on: push jobs: package: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Build run: cd src && dotnet build + - name: Test run: | cd tests && dotnet test --configuration Debug --verbosity normal --collect:'XPlat Code Coverage' --settings coverlet.runsettings + - name: Move test results to accessible directory run: | - mkdir TestResults - mv -v tests/TestResults/*/*.* TestResults/ - # Note: this step is currently not writing to the gist for some reason + mkdir -p TestResults + find tests/TestResults -type f -name "*.xml" -exec mv -v --backup=numbered {} TestResults/ \; + - name: Create Test Coverage Badge uses: simon-k/dotnet-code-coverage-badge@v1.0.0 id: create_coverage_badge @@ -33,8 +36,10 @@ jobs: run: | echo "Code coverage percentage: ${{steps.create_coverage_badge.outputs.percentage}}%" echo "Badge data: ${{steps.create_coverage_badge.outputs.badge}}" + - name: Pack - run: cd src && dotnet pack - - name: Publish to Nuget + run: cd src && dotnet pack --configuration Debug --output ./nupkg + + - name: Publish to NuGet if: contains(github.ref, 'refs/tags/v') - run: dotnet nuget push ./src/nupkg/TerminalGuiDesigner.$(fgrep \ ./src/TerminalGuiDesigner.csproj | grep -oEi '[0-9.]+').nupkg --api-key ${{ secrets.NUGET_KEY }} --source https://api.nuget.org/v3/index.json + run: dotnet nuget push ./src/nupkg/*.nupkg --api-key ${{ secrets.NUGET_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..ba257406 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,67 @@ +name: "CodeQL" + +on: + push: + branches: [ "convert-tests-to-constraint-model","v2","v2_latest_changes_pre_220" ] + + +jobs: + analyze: + name: Analyze + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners + # Consider using larger runners for possible analysis time improvements. + runs-on: ${{ 'ubuntu-latest' }} + timeout-minutes: ${{ 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'csharp' ] + + steps: + - name: Set up .net 8 SDK + uses: actions/setup-dotnet@v3.2.0 + with: + dotnet-version: 8.0 + + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..e69de29b diff --git a/README.md b/README.md index 7c5047ec..3fddd479 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ Cross platform designer for [Terminal.Gui](https://github.com/migueldeicaza/gui. Built with CodeDom and Roslyn, TerminalGuiDesigner lets you create complicated Views with drag and drop just like the WinForms designer you know and love (or hate). + +## V1 + Install the tool from NuGet or follow the [Hello World Tutorial](./README.md#usage): ``` dotnet tool install --global TerminalGuiDesigner @@ -16,10 +19,19 @@ Update to the latest version using ``` dotnet tool update --global TerminalGuiDesigner ``` - This project is in alpha. See the [feature list](./README.md#features) for progress. -![newdemo](https://user-images.githubusercontent.com/31306100/175789072-27fcacf0-e9f0-4543-818d-50058bb46a2f.gif) +## V2 + +If you are targetting Terminal.Gui version 2 (currently pre-alpha) then you will want to install version 2 of the designer +``` +dotnet tool install --global TerminalGuiDesigner --prerelease +``` +Ensure that you match the Terminal.Gui library version you reference to the designer version. + +## Demo + +![long-demo](https://github.com/gui-cs/TerminalGuiDesigner/assets/31306100/5df9f545-8c61-4655-bc0c-1e75d1c149d9) ### Building ---------------- @@ -62,7 +74,7 @@ You can add new code to `MyDialog.cs` but avoid making any changes to `MyDialog. For example in `MyDialog.cs` after `InitializeComponent()` add the following: ```csharp -button1.Clicked += ()=>MessageBox.Query("Hello","Hello World","Ok"); +button1.Accept += ()=>MessageBox.Query("Hello","Hello World","Ok"); ``` Now when run clicking the button will trigger a message box. @@ -207,6 +219,7 @@ italics are experimental and require passing the `-e` flag when starting applica - [x] TimeField - [x] TreeView - [x] View + - [x] Slider ### Class Diagram ------------------------------- diff --git a/src/.gitattributes b/src/.gitattributes new file mode 100644 index 00000000..47590036 --- /dev/null +++ b/src/.gitattributes @@ -0,0 +1 @@ +Keys.yaml eol=lf \ No newline at end of file diff --git a/src/ApplicationExtensions.cs b/src/ApplicationExtensions.cs deleted file mode 100644 index 24a79fc9..00000000 --- a/src/ApplicationExtensions.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Reflection; -using Terminal.Gui; - -namespace TerminalGuiDesigner; - -/// -/// Extension methods to access private/internal functions of . -/// -public static class ApplicationExtensions -{ - /// - /// Finds the deepest at screen coordinates x,y. - /// This is a private static method in the main Terminal.Gui library - /// invoked via reflection. - /// - /// The top level to start looking down from. - /// Screen X coordinate. - /// Screen Y coordinate. - /// The that renders into the screen space (hit by the click). - /// Thrown if Terminal.Gui private API changes. - public static View? FindDeepestView(View start, int x, int y) - { - var method = typeof(Application).GetMethod( - nameof(FindDeepestView), - BindingFlags.Static | BindingFlags.NonPublic, - new[] { typeof(View), typeof(int), typeof(int), typeof(int).MakeByRefType(), typeof(int).MakeByRefType() }); - - if (method == null) - { - throw new MissingMethodException("Static method FindDeepestView not found on Application class"); - } - - int resx = 0; - int resy = 0; - - return (View?)method.Invoke(null, new object[] { start, x, y, resx, resy }); - } -} diff --git a/src/ArrayExtensions.cs b/src/ArrayExtensions.cs index b6bfa9e9..9b8cea42 100644 --- a/src/ArrayExtensions.cs +++ b/src/ArrayExtensions.cs @@ -1,3 +1,7 @@ +using System.Collections; +using System.Collections.ObjectModel; +using Terminal.Gui; + namespace TerminalGuiDesigner; /// @@ -26,4 +30,23 @@ public static class ArrayExtensions return toReturn; } + + public static IListDataSource ToListDataSource(this IEnumerable enumerable) + { + // Get the type of the elements + var elementType = enumerable.GetType().GetElementType() ?? enumerable.GetType().GetGenericArguments().FirstOrDefault(); + if (elementType == null) + { + throw new Exception("Unable to get element type for collection"); + } + + // Convert the enumerable to an ObservableCollection + var observableCollectionType = typeof(ObservableCollection<>).MakeGenericType(elementType); + var list = Activator.CreateInstance(observableCollectionType, enumerable); + + // Create an instance of ListWrapper + var listWrapperType = typeof(ListWrapper<>).MakeGenericType(elementType); + return (IListDataSource)Activator.CreateInstance(listWrapperType, list); + } + } diff --git a/src/AttributeExtensions.cs b/src/AttributeExtensions.cs index 9c46ff05..9600e349 100644 --- a/src/AttributeExtensions.cs +++ b/src/AttributeExtensions.cs @@ -12,6 +12,6 @@ public static class AttributeExtensions /// Code construct . public static string ToCode(this Terminal.Gui.Attribute a) { - return $"Terminal.Gui.Attribute.Make(Color.{a.Foreground},Color.{a.Background})"; + return $"new Terminal.Gui.Attribute(Terminal.Gui.Color.{a.Foreground},Terminal.Gui.Color.{a.Background})"; } } diff --git a/src/ColorSchemeExtensions.cs b/src/ColorSchemeExtensions.cs deleted file mode 100644 index b0b36e44..00000000 --- a/src/ColorSchemeExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Terminal.Gui; - -namespace TerminalGuiDesigner; - -/// -/// Extension methods for the class. -/// -public static class ColorSchemeExtensions -{ - /// - /// Compares two by value (not reference). - /// - /// First to compare. - /// To compare with. - /// if both have the same for all fields. - public static bool AreEqual(this ColorScheme a, ColorScheme b) - { - return - a.Normal.Value == b.Normal.Value && - a.HotNormal.Value == b.HotNormal.Value && - a.Focus.Value == b.Focus.Value && - a.HotFocus.Value == b.HotFocus.Value && - a.Disabled.Value == b.Disabled.Value; - } -} diff --git a/src/ColorSchemeManager.cs b/src/ColorSchemeManager.cs index fd449c21..0e6e950d 100644 --- a/src/ColorSchemeManager.cs +++ b/src/ColorSchemeManager.cs @@ -61,7 +61,7 @@ public void Remove(NamedColorScheme toDelete) /// schemes. /// /// View to find color schemes in, must be the root design (i.e. ). - /// Thrown if passed a non root . + /// Thrown if passed a non-root . public void FindDeclaredColorSchemes(Design viewBeingEdited) { if (!viewBeingEdited.IsRoot) @@ -94,7 +94,7 @@ public void FindDeclaredColorSchemes(Design viewBeingEdited) /// The name of the scheme or null if it is not known. public string? GetNameForColorScheme(ColorScheme s) { - var match = this.colorSchemes.Where(kvp => s.AreEqual(kvp.Scheme)).ToArray(); + var match = this.colorSchemes.Where(kvp => s.Equals(kvp.Scheme)).ToArray(); if (match.Length > 0) { @@ -114,16 +114,16 @@ public void FindDeclaredColorSchemes(Design viewBeingEdited) /// Will become . /// The new color values to use. /// The topmost the user is editing (see ). - public void AddOrUpdateScheme(string name, ColorScheme scheme, Design rootDesign) + /// A reference to the that was added or updated. + public ColorScheme AddOrUpdateScheme(string name, ColorScheme scheme, Design rootDesign) { - var oldScheme = this.colorSchemes.FirstOrDefault(c => c.Name.Equals(name)); - // if we don't currently know about this scheme - if (oldScheme == null) + if (this.colorSchemes.FirstOrDefault(c => c.Name.Equals(name)) is not { } oldScheme) { // simply record that we now know about it and exit - this.colorSchemes.Add(new NamedColorScheme(name, scheme)); - return; + NamedColorScheme newColorScheme = new (name, scheme); + this.colorSchemes.Add(newColorScheme); + return newColorScheme.Scheme; } // we know about this color already and people may be using it! @@ -138,6 +138,7 @@ public void AddOrUpdateScheme(string name, ColorScheme scheme, Design rootDesign } oldScheme.Scheme = scheme; + return scheme; } /// diff --git a/src/DefaultColorSchemes.cs b/src/DefaultColorSchemes.cs index 0bdde179..2d1565c2 100644 --- a/src/DefaultColorSchemes.cs +++ b/src/DefaultColorSchemes.cs @@ -16,43 +16,49 @@ public class DefaultColorSchemes /// public DefaultColorSchemes() { - this.RedOnBlack = new NamedColorScheme("redOnBlack"); - this.RedOnBlack.Scheme.Normal = new Terminal.Gui.Attribute(Terminal.Gui.Color.Red, Terminal.Gui.Color.Black); - this.RedOnBlack.Scheme.HotNormal = new Terminal.Gui.Attribute(Terminal.Gui.Color.BrightRed, Terminal.Gui.Color.Black); - this.RedOnBlack.Scheme.Focus = new Terminal.Gui.Attribute(Terminal.Gui.Color.Red, Terminal.Gui.Color.Brown); - this.RedOnBlack.Scheme.HotFocus = new Terminal.Gui.Attribute(Terminal.Gui.Color.BrightRed, Terminal.Gui.Color.Brown); - this.RedOnBlack.Scheme.Disabled = new Terminal.Gui.Attribute(Terminal.Gui.Color.Gray, Terminal.Gui.Color.Black); + this.RedOnBlack = new NamedColorScheme("redOnBlack", + new ColorScheme( + normal: new Terminal.Gui.Attribute(Terminal.Gui.Color.Red, Terminal.Gui.Color.Black), + hotNormal: new Terminal.Gui.Attribute(Terminal.Gui.Color.BrightRed, Terminal.Gui.Color.Black), + focus: new Terminal.Gui.Attribute(Terminal.Gui.Color.Red, Terminal.Gui.Color.Yellow), + hotFocus: new Terminal.Gui.Attribute(Terminal.Gui.Color.BrightRed, Terminal.Gui.Color.Yellow), + disabled: new Terminal.Gui.Attribute(Terminal.Gui.Color.Gray, Terminal.Gui.Color.Black) + )); - this.GreenOnBlack = new NamedColorScheme("greenOnBlack"); - this.GreenOnBlack.Scheme.Normal = new Terminal.Gui.Attribute(Terminal.Gui.Color.Green, Terminal.Gui.Color.Black); - this.GreenOnBlack.Scheme.HotNormal = new Terminal.Gui.Attribute(Terminal.Gui.Color.BrightGreen, Terminal.Gui.Color.Black); - this.GreenOnBlack.Scheme.Focus = new Terminal.Gui.Attribute(Terminal.Gui.Color.Green, Terminal.Gui.Color.Magenta); - this.GreenOnBlack.Scheme.HotFocus = new Terminal.Gui.Attribute(Terminal.Gui.Color.BrightGreen, Terminal.Gui.Color.Magenta); - this.GreenOnBlack.Scheme.Disabled = new Terminal.Gui.Attribute(Terminal.Gui.Color.Gray, Terminal.Gui.Color.Black); + this.GreenOnBlack = new NamedColorScheme("greenOnBlack", + new ColorScheme( + normal : new Terminal.Gui.Attribute(Terminal.Gui.Color.Green, Terminal.Gui.Color.Black), + hotNormal: new Terminal.Gui.Attribute(Terminal.Gui.Color.BrightGreen, Terminal.Gui.Color.Black), + focus: new Terminal.Gui.Attribute(Terminal.Gui.Color.Green, Terminal.Gui.Color.Magenta), + hotFocus: new Terminal.Gui.Attribute(Terminal.Gui.Color.BrightGreen, Terminal.Gui.Color.Magenta), + disabled : new Terminal.Gui.Attribute(Terminal.Gui.Color.Gray, Terminal.Gui.Color.Black))); - this.BlueOnBlack = new NamedColorScheme("blueOnBlack"); - this.BlueOnBlack.Scheme.Normal = new Terminal.Gui.Attribute(Terminal.Gui.Color.BrightBlue, Terminal.Gui.Color.Black); - this.BlueOnBlack.Scheme.HotNormal = new Terminal.Gui.Attribute(Terminal.Gui.Color.Cyan, Terminal.Gui.Color.Black); - this.BlueOnBlack.Scheme.Focus = new Terminal.Gui.Attribute(Terminal.Gui.Color.BrightBlue, Terminal.Gui.Color.BrightYellow); - this.BlueOnBlack.Scheme.HotFocus = new Terminal.Gui.Attribute(Terminal.Gui.Color.Cyan, Terminal.Gui.Color.BrightYellow); - this.BlueOnBlack.Scheme.Disabled = new Terminal.Gui.Attribute(Terminal.Gui.Color.Gray, Terminal.Gui.Color.Black); + this.BlueOnBlack = new NamedColorScheme("blueOnBlack", + new ColorScheme( + normal : new Terminal.Gui.Attribute(Terminal.Gui.Color.BrightBlue, Terminal.Gui.Color.Black), + hotNormal: new Terminal.Gui.Attribute(Terminal.Gui.Color.Cyan, Terminal.Gui.Color.Black), + focus: new Terminal.Gui.Attribute(Terminal.Gui.Color.BrightBlue, Terminal.Gui.Color.BrightYellow), + hotFocus: new Terminal.Gui.Attribute(Terminal.Gui.Color.Cyan, Terminal.Gui.Color.BrightYellow), + disabled: new Terminal.Gui.Attribute(Terminal.Gui.Color.Gray, Terminal.Gui.Color.Black))); - this.GrayOnBlack = new NamedColorScheme("greyOnBlack"); - this.GrayOnBlack.Scheme.Normal = new Terminal.Gui.Attribute(Terminal.Gui.Color.DarkGray, Terminal.Gui.Color.Black); - this.GrayOnBlack.Scheme.HotNormal = new Terminal.Gui.Attribute(Terminal.Gui.Color.DarkGray, Terminal.Gui.Color.Black); - this.GrayOnBlack.Scheme.Focus = new Terminal.Gui.Attribute(Terminal.Gui.Color.Black, Terminal.Gui.Color.DarkGray); - this.GrayOnBlack.Scheme.HotFocus = new Terminal.Gui.Attribute(Terminal.Gui.Color.Black, Terminal.Gui.Color.DarkGray); - this.GrayOnBlack.Scheme.Disabled = new Terminal.Gui.Attribute(Terminal.Gui.Color.DarkGray, Terminal.Gui.Color.Black); + this.GrayOnBlack = new NamedColorScheme("greyOnBlack", + new ColorScheme( + normal : new Terminal.Gui.Attribute(Terminal.Gui.Color.DarkGray, Terminal.Gui.Color.Black), + hotNormal: new Terminal.Gui.Attribute(Terminal.Gui.Color.DarkGray, Terminal.Gui.Color.Black), + focus: new Terminal.Gui.Attribute(Terminal.Gui.Color.Black, Terminal.Gui.Color.DarkGray), + hotFocus: new Terminal.Gui.Attribute(Terminal.Gui.Color.Black, Terminal.Gui.Color.DarkGray), + disabled: new Terminal.Gui.Attribute(Terminal.Gui.Color.DarkGray, Terminal.Gui.Color.Black))); - this.TerminalGuiDefault = new NamedColorScheme("tgDefault"); - this.TerminalGuiDefault.Scheme.Normal = new Terminal.Gui.Attribute(Color.White, Color.Blue); - this.TerminalGuiDefault.Scheme.HotNormal = new Terminal.Gui.Attribute(Color.BrightCyan, Color.Blue); - this.TerminalGuiDefault.Scheme.Focus = new Terminal.Gui.Attribute(Color.Black, Color.Gray); - this.TerminalGuiDefault.Scheme.HotFocus = new Terminal.Gui.Attribute(Color.BrightBlue, Color.Gray); + this.TerminalGuiDefault = new NamedColorScheme("tgDefault", + new ColorScheme( + normal : new Terminal.Gui.Attribute(Color.White, Color.Blue), + hotNormal : new Terminal.Gui.Attribute(Color.BrightCyan, Color.Blue), + focus : new Terminal.Gui.Attribute(Color.Black, Color.Gray), + hotFocus : new Terminal.Gui.Attribute(Color.BrightBlue, Color.Gray), // HACK : Keeping this foreground as Brown because otherwise designer will think this is legit // the real default and assume user has not chosen it. See: https://github.com/gui-cs/TerminalGuiDesigner/issues/133 - this.TerminalGuiDefault.Scheme.Disabled = new Terminal.Gui.Attribute(Color.Brown, Color.Blue); + disabled: new Terminal.Gui.Attribute(Color.Yellow, Color.Blue))); } /// diff --git a/src/Design.cs b/src/Design.cs index fb9e63fa..12e1ddae 100644 --- a/src/Design.cs +++ b/src/Design.cs @@ -1,8 +1,9 @@ using System.Data; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Xml.Linq; using NLog; using Terminal.Gui; -using Terminal.Gui.Graphs; -using Terminal.Gui.Trees; using TerminalGuiDesigner.Operations; using TerminalGuiDesigner.Operations.MenuOperations; using TerminalGuiDesigner.Operations.StatusBarOperations; @@ -31,13 +32,12 @@ public class Design /// View Types for which does not make sense as a user /// configurable field (e.g. there is a Title field instead). /// - private HashSet excludeTextPropertyFor = new HashSet + private readonly HashSet excludeTextPropertyFor = new() { typeof(FrameView), typeof(TabView), typeof(Window), typeof(Toplevel), - typeof(PanelView), typeof(View), typeof(GraphView), typeof(HexView), @@ -49,7 +49,6 @@ public class Design typeof(TableView), typeof(TabView), typeof(TreeView), - typeof(TreeView<>), typeof(Dialog), }; @@ -132,6 +131,22 @@ public Design(SourceCodeFile sourceCode, string fieldName, View view) return this.GetDesignableProperties().SingleOrDefault(p => p.PropertyInfo.Name.Equals(propertyName)); } + /// + /// Returns the named if designing is supported for it + /// on the Type. + /// + /// Name of the designable you want to find. + /// The retrieved property, if true, or null, if false. + /// + /// if the exists and is designable or if not found or not + /// supported. + /// + public bool TryGetDesignableProperty(string propertyName, [NotNullWhen(true)] out Property? property) + { + property = this.GetDesignableProperties( ).SingleOrDefault( p => p.PropertyInfo.Name.Equals( propertyName ) ); + return property is not null; + } + /// /// Walk the hierarchy and create wrappers /// for any found that were created by user (ignoring those that @@ -152,30 +167,24 @@ public void CreateSubControlDesigns() /// A new wrapper wrapping . public Design CreateSubControlDesign(string name, View subView) { - // HACK: if you don't pull the label out first it complains that you cant set Focusable to true - // on the Label because its super is not focusable :( - var super = subView.SuperView; - if (super != null) - { - super.Remove(subView); - } - // all views can be focused so that they can be edited // or deleted etc subView.CanFocus = true; - if (subView is TableView tv && tv.Table != null && tv.Table.Rows.Count == 0) + if (subView is TableView tv && tv.Table != null && tv.GetDataTable().Rows.Count == 0) { + var dt = tv.GetDataTable(); + // add example rows so that it is easier to design the view for (int i = 0; i < 100; i++) { - var row = tv.Table.NewRow(); - for (int c = 0; c < tv.Table.Columns.Count; c++) + var row = dt.NewRow(); + for (int c = 0; c < dt.Columns.Count; c++) { row[c] = DBNull.Value; } - tv.Table.Rows.Add(row); + dt.Rows.Add(row); } } @@ -205,14 +214,14 @@ public Design CreateSubControlDesign(string name, View subView) { // prevent control from responding to events txt.MouseClick += this.SuppressNativeClickEvents; - txt.KeyDown += (s) => s.Handled = true; + txt.KeyDown += this.SuppressNativeKeyboardEvents; } if (subView is TextField tf) { // prevent control from responding to events tf.MouseClick += this.SuppressNativeClickEvents; - tf.KeyDown += (s) => s.Handled = true; + tf.KeyDown += SuppressNativeKeyboardEvents; } if (subView is TreeView tree) @@ -235,12 +244,7 @@ public Design CreateSubControlDesign(string name, View subView) tree.AddObject(new TreeNode($"Example Leaf {l}")); } } - - if (super != null) - { - super.Add(subView); - } - + var d = new Design(this.SourceCode, name, subView); return d; } @@ -293,7 +297,7 @@ public bool UsesColorScheme(ColorScheme scheme) { // we use this scheme if it is a known scheme return this.HasKnownColorScheme() && - (this.View.ColorScheme.AreEqual(scheme) || (this.State.OriginalScheme?.AreEqual(scheme) ?? false)); + (this.View.ColorScheme.Equals(scheme) || (this.State.OriginalScheme?.Equals(scheme) ?? false)); } /// @@ -329,23 +333,26 @@ public IEnumerable GetExtraOperations(Point pos) // Extra TableView operations if (this.View is TableView tv) { + var dt = tv.GetDataTable(); + // if user right clicks a cell then provide options relating to the clicked column DataColumn? col = null; if (!pos.IsEmpty) { // See which column the right click lands. - var cell = tv.ScreenToCell(pos.X, pos.Y, out col); + var cell = tv.ScreenToCell(pos.X, pos.Y, out var colIdx); + - if (cell != null && col == null) + if (cell != null && colIdx == null) { - col = tv.Table.Columns[cell.Value.X]; + col = dt.Columns[cell.Value.X]; } } // if no column was right clicked then provide commands for the selected column if (col == null && tv.SelectedColumn >= 0) { - col = tv.Table.Columns[tv.SelectedColumn]; + col = dt.Columns[tv.SelectedColumn]; } yield return new AddColumnOperation(this, null); @@ -376,47 +383,54 @@ public IEnumerable GetExtraOperations(Point pos) yield return new DeleteViewOperation(this); - if (this.View is TabView tabView) + switch ( this.View ) { - yield return new AddTabOperation(this, null); - - if (tabView.SelectedTab != null) + case TabView tabView: { - yield return new RemoveTabOperation(this, tabView.SelectedTab); - yield return new RenameTabOperation(this, tabView.SelectedTab, null); - yield return new MoveTabOperation(this, tabView.SelectedTab, -1); - yield return new MoveTabOperation(this, tabView.SelectedTab, 1); + yield return new AddTabOperation(this, null); + + if (tabView.SelectedTab != null) + { + yield return new RemoveTabOperation(this, tabView.SelectedTab); + yield return new RenameTabOperation(this, tabView.SelectedTab, null); + yield return new MoveTabOperation(this, tabView.SelectedTab, -1); + yield return new MoveTabOperation(this, tabView.SelectedTab, 1); + } + + break; } - } + case MenuBar mb: + { + yield return new AddMenuOperation(this, null); - if (this.View is MenuBar mb) - { - yield return new AddMenuOperation(this, null); + var menu = pos.IsEmpty ? mb.GetSelectedMenuItem() : mb.ScreenToMenuBarItem(pos.X); - var menu = pos.IsEmpty ? mb.GetSelectedMenuItem() : mb.ScreenToMenuBarItem(pos.X); + if (menu != null) + { + yield return new RemoveMenuOperation(this, menu); + yield return new RenameMenuOperation(this, menu, null); + yield return new MoveMenuOperation(this, menu, -1); + yield return new MoveMenuOperation(this, menu, 1); + } - if (menu != null) - { - yield return new RemoveMenuOperation(this, menu); - yield return new RenameMenuOperation(this, menu, null); - yield return new MoveMenuOperation(this, menu, -1); - yield return new MoveMenuOperation(this, menu, 1); + break; } - } + case StatusBar sb: + { + yield return new AddStatusItemOperation(this, null); - if (this.View is StatusBar sb) - { - yield return new AddStatusItemOperation(this, null); + var item = sb.ScreenToMenuBarItem(pos.X); - var item = sb.ScreenToMenuBarItem(pos.X); + if (item != null) + { + yield return new RemoveStatusItemOperation(this, item); + yield return new RenameStatusItemOperation(this, item, null); + yield return new SetShortcutOperation(this, item, null); + yield return new MoveStatusItemOperation(this, item, -1); + yield return new MoveStatusItemOperation(this, item, 1); + } - if (item != null) - { - yield return new RemoveStatusItemOperation(this, item); - yield return new RenameStatusItemOperation(this, item, null); - yield return new SetShortcutOperation(this, item, null); - yield return new MoveStatusItemOperation(this, item, -1); - yield return new MoveStatusItemOperation(this, item, 1); + break; } } } @@ -596,18 +610,34 @@ private void CreateSubControlDesigns(View view) } } - private void SuppressNativeClickEvents(View.MouseEventArgs obj) + private void SuppressNativeClickEvents(object? sender, MouseEventEventArgs obj) { // Suppress everything except single click (selection) obj.Handled = obj.MouseEvent.Flags != MouseFlags.Button1Clicked; } + private void SuppressNativeKeyboardEvents(object? sender, Key e) + { + if (sender == null) + { + return; + } + + if (e == Key.Tab || e == Key.Tab.WithShift || e == Key.Esc || e == Application.QuitKey) + { + e.Handled = false; + return; + } + + e.Handled = true; + } + private void RegisterCheckboxDesignTimeChanges(CheckBox cb) { // prevent space toggling the checkbox // (gives better typing experience e.g. "my lovely checkbox") - cb.ClearKeybinding(Key.Space); - cb.MouseClick += (e) => + cb.KeyBindings.Remove(Key.Space); + cb.MouseClick += (s, e) => { if (e.MouseEvent.Flags.HasFlag(MouseFlags.Button1Clicked)) { @@ -619,12 +649,19 @@ private void RegisterCheckboxDesignTimeChanges(CheckBox cb) private IEnumerable LoadDesignableProperties() { + var viewType = this.View.GetType(); + var isGenericType = viewType.IsGenericType; + yield return this.CreateProperty(nameof(this.View.Width)); yield return this.CreateProperty(nameof(this.View.Height)); yield return this.CreateProperty(nameof(this.View.X)); yield return this.CreateProperty(nameof(this.View.Y)); + yield return this.CreateSuppressedProperty(nameof(this.View.Visible), true); + + yield return this.CreateSuppressedProperty(nameof(this.View.Arrangement), ViewArrangement.Fixed); + yield return new ColorSchemeProperty(this); // its important that this comes before Text because @@ -639,14 +676,33 @@ private IEnumerable LoadDesignableProperties() yield return this.CreateProperty(nameof(TextField.Secret)); } - if (this.View is ScrollView) + if (isGenericType && viewType.GetGenericTypeDefinition() == typeof(Slider<>)) { - yield return this.CreateProperty(nameof(ScrollView.ContentSize)); + yield return this.CreateProperty(nameof(Slider.Options)); + yield return this.CreateProperty(nameof(Slider.Orientation)); + yield return this.CreateProperty(nameof(Slider.RangeAllowSingle)); + yield return this.CreateProperty(nameof(Slider.AllowEmpty)); + yield return this.CreateProperty(nameof(Slider.MinimumInnerSpacing)); + yield return this.CreateProperty(nameof(Slider.LegendsOrientation)); + yield return this.CreateProperty(nameof(Slider.ShowLegends)); + yield return this.CreateProperty(nameof(Slider.ShowEndSpacing)); + yield return this.CreateProperty(nameof(Slider.Type)); + } + + if (this.View is SpinnerView) + { + yield return this.CreateProperty(nameof(SpinnerView.AutoSpin)); + + yield return new InstanceOfProperty( + this, + viewType.GetProperty(nameof(SpinnerView.Style)) ?? throw new Exception($"Could not find expected Property SpinnerView.Style on View of Type '{this.View.GetType()}'")); } if (this.View is TextView) { - yield return this.CreateProperty(nameof(TextView.AllowsTab)); + // Do not allow tab at design time so that we don't get stuck in the View (adding more tabs each time!) + // But let user edit if they want + yield return this.CreateSuppressedProperty(nameof(TextView.AllowsTab), false); yield return this.CreateProperty(nameof(TextView.AllowsReturn)); yield return this.CreateProperty(nameof(TextView.WordWrap)); } @@ -668,15 +724,18 @@ private IEnumerable LoadDesignableProperties() yield return new Property(this, this.View.GetActualTextProperty()); } - // Border properties - Most views dont have a border so Border is - if (this.View.Border != null) - { - yield return this.CreateSubProperty(nameof(Border.BorderStyle), nameof(this.View.Border), this.View.Border); - yield return this.CreateSubProperty(nameof(Border.BorderBrush), nameof(this.View.Border), this.View.Border); - yield return this.CreateSubProperty(nameof(Border.Effect3D), nameof(this.View.Border), this.View.Border); - yield return this.CreateSubProperty(nameof(Border.Effect3DBrush), nameof(this.View.Border), this.View.Border); - yield return this.CreateSubProperty(nameof(Border.DrawMarginFrame), nameof(this.View.Border), this.View.Border); - } + /* + TODO: Borders are changed a lot in v2 + // Border properties - Most views dont have a border so Border is + if (this.View.Border != null) + { + yield return this.CreateSubProperty(nameof(Border.BorderStyle), nameof(this.View.Border), this.View.Border); + yield return this.CreateSubProperty(nameof(Border.BorderBrush), nameof(this.View.Border), this.View.Border); + yield return this.CreateSubProperty(nameof(Border.Effect3D), nameof(this.View.Border), this.View.Border); + yield return this.CreateSubProperty(nameof(Border.Effect3DBrush), nameof(this.View.Border), this.View.Border); + yield return this.CreateSubProperty(nameof(Border.DrawMarginFrame), nameof(this.View.Border), this.View.Border); + } + */ yield return this.CreateProperty(nameof(this.View.TextAlignment)); @@ -702,7 +761,7 @@ private IEnumerable LoadDesignableProperties() if (this.View is CheckBox) { - yield return this.CreateProperty(nameof(CheckBox.Checked)); + yield return this.CreateProperty(nameof(CheckBox.CheckedState)); } if (this.View is ListView lv) @@ -742,7 +801,7 @@ private IEnumerable LoadDesignableProperties() yield return this.CreateProperty(nameof(FrameView.Title)); } - if (this.View is TreeView tree) + if (this.View is ITreeView tree) { yield return this.CreateSubProperty(nameof(TreeStyle.CollapseableSymbol), nameof(TreeView.Style), tree.Style); yield return this.CreateSubProperty(nameof(TreeStyle.ColorExpandSymbol), nameof(TreeView.Style), tree.Style); @@ -751,6 +810,13 @@ private IEnumerable LoadDesignableProperties() yield return this.CreateSubProperty(nameof(TreeStyle.LeaveLastRow), nameof(TreeView.Style), tree.Style); yield return this.CreateSubProperty(nameof(TreeStyle.ShowBranchLines), nameof(TreeView.Style), tree.Style); } + + if (isGenericType && viewType.GetGenericTypeDefinition() == typeof(TreeView<>)) + { + var prop = this.CreateTreeObjectsProperty(viewType); + if(((ITreeObjectsProperty)prop).IsSupported()) + yield return prop; + } if (this.View is TableView tv) { @@ -777,10 +843,26 @@ private IEnumerable LoadDesignableProperties() if (this.View is RadioGroup) { yield return this.CreateProperty(nameof(RadioGroup.RadioLabels)); - yield return this.CreateProperty(nameof(RadioGroup.DisplayMode)); } } + private Property CreateTreeObjectsProperty(Type viewType) + { + if(viewType.GetGenericTypeDefinition() != typeof(TreeView<>)) + { + throw new ArgumentException("Method should only be called for TreeView"); + } + + var tType = viewType.GetGenericArguments()[0]; + var propertyType = typeof(TreeObjectsProperty<>).MakeGenericType(tType); + + var instance = + Activator.CreateInstance(propertyType, new object?[] { this }) + ?? throw new Exception($"Failed to construct {propertyType}"); + + return (Property)instance; + } + private bool ShowTextProperty() { // never show Text for root because it's almost certainly a container @@ -790,6 +872,12 @@ private bool ShowTextProperty() return false; } + // Do not let Text be set on Slider or Slider<> implementations as weird stuff happens + if(this.View.GetType().Name.StartsWith("Slider") || View is RadioGroup) + { + return false; + } + return !this.excludeTextPropertyFor.Contains(this.View.GetType()); } @@ -809,6 +897,14 @@ private Property CreateProperty(string name) this.View.GetType().GetProperty(name) ?? throw new Exception($"Could not find expected Property '{name}' on View of Type '{this.View.GetType()}'")); } + private Property CreateSuppressedProperty(string name, object? designTimeValue) + { + return new SuppressedProperty( + this, + this.View.GetType().GetProperty(name) ?? throw new Exception($"Could not find expected Property '{name}' on View of Type '{this.View.GetType()}'"), + designTimeValue); + } + private bool DependsOnUs(Design other, Design[] everyone) { // obviously we cannot depend on ourselves diff --git a/src/DesignState.cs b/src/DesignState.cs index a0c83bcd..22c2c411 100644 --- a/src/DesignState.cs +++ b/src/DesignState.cs @@ -1,4 +1,5 @@ -using Terminal.Gui; +using System.Text; +using Terminal.Gui; using TerminalGuiDesigner.UI; namespace TerminalGuiDesigner; @@ -21,7 +22,7 @@ public DesignState(Design design) { this.Design = design; this.OriginalScheme = this.Design.View.GetExplicitColorScheme(); - this.Design.View.DrawContent += this.DrawContent; + this.Design.View.DrawContentComplete += this.DrawContentComplete; this.Design.View.Enter += this.Enter; } @@ -38,7 +39,7 @@ public DesignState(Design design) /// public Design Design { get; } - private void Enter(View.FocusEventArgs obj) + private void Enter(object? sender, FocusEventArgs obj) { // when tabbing or clicking into this View when nothing complicated is going on (e.g. Ctrl+Click multi select) if (SelectionManager.Instance.Selected.Count <= 1) @@ -48,15 +49,15 @@ private void Enter(View.FocusEventArgs obj) } } - private void DrawContent(Rect r) + private void DrawContentComplete(object? sender, DrawEventArgs r) { if (this.Design.View.IsBorderlessContainerView() && Editor.ShowBorders) { - this.DrawBorderlessViewFrame(r); + this.DrawBorderlessViewFrame(r.NewViewport); } } - private void DrawBorderlessViewFrame(Rect r) + private void DrawBorderlessViewFrame(Rectangle r) { bool isSelected = SelectionManager.Instance.Selected.Contains(this.Design); @@ -75,7 +76,7 @@ private void DrawBorderlessViewFrame(Rect r) if (y == 0 || y == r.Height - 1 || x == 0 || x == r.Width - 1) { var rune = (y == r.Height - 1 && x == r.Width - 1 && isSelected) ? '╬' : '.'; - v.AddRune(x, y, rune); + v.AddRune(x, y, new Rune(rune)); } } } diff --git a/src/DimExtensions.cs b/src/DimExtensions.cs index 798d8133..107b8907 100644 --- a/src/DimExtensions.cs +++ b/src/DimExtensions.cs @@ -17,7 +17,7 @@ public static class DimExtensions private const bool TreatNullDimAs0 = true; /// - /// Returns true if the is a DimFactor (i.e. created by ). + /// Returns true if the is a DimFactor (i.e. created by ). /// /// Dimension to determine Type. /// true if is DimFactor. @@ -28,21 +28,20 @@ public static bool IsPercent(this Dim d) return false; } - return d.GetType().Name == "DimFactor"; + return d is DimPercent; } /// /// The to determine whether it represents a percent. /// The 'percentage' value of . This is the value that would/could be - /// passed to to produce the or 0 if is + /// passed to to produce the or 0 if is /// not DimFactor. - public static bool IsPercent(this Dim d, out float percent) + public static bool IsPercent(this Dim d, out int percent) { if (d != null && d.IsPercent()) { - var nField = d.GetType().GetField("factor", BindingFlags.NonPublic | BindingFlags.Instance) - ?? throw new Exception("Expected private field 'factor' of DimPercent was missing"); - percent = ((float?)nField.GetValue(d) ?? throw new Exception("Expected private field 'factor' to be a float")) * 100f; + var dp = (DimPercent)d; + percent = dp.Percentage; return true; } @@ -62,7 +61,7 @@ public static bool IsFill(this Dim d) return false; } - return d.GetType().Name == "DimFill"; + return d is DimFill; } /// @@ -72,9 +71,8 @@ public static bool IsFill(this Dim d, out int margin) { if (d != null && d.IsFill()) { - var nField = d.GetType().GetField("margin", BindingFlags.NonPublic | BindingFlags.Instance) - ?? throw new Exception("Expected private field 'margin' of DimFill was missing"); - margin = (int?)nField.GetValue(d) ?? throw new Exception("Expected private field 'margin' of DimFill had unexpected Type"); + var df = (DimFill)d; + margin = df.Margin; return true; } @@ -94,9 +92,35 @@ public static bool IsAbsolute(this Dim d) return TreatNullDimAs0; } - return d.GetType().Name == "DimAbsolute"; + return d is DimAbsolute; } + + /// + /// True if is a width/height. + /// + /// The to determine whether it is auto. + /// if is a auto sizing. + public static bool IsAuto(this Dim d, out DimAutoStyle das, out Dim? min, out Dim? max) + { + if(d is DimAuto da) + { + das = da.Style; + min = da.MinimumContentDim; + max = da.MaximumContentDim; + + return true; + } + + das = default(DimAutoStyle); + min = null; + max = null; + return false; + } + + + + /// /// The to determine whether it is absolute. /// The value of the fixed number absolute value or 0. @@ -110,10 +134,8 @@ public static bool IsAbsolute(this Dim d, out int n) return TreatNullDimAs0; } - var nField = d.GetType().GetField("n", BindingFlags.NonPublic | BindingFlags.Instance) - ?? throw new Exception("Expected private field was missing from DimAbsolute"); - n = (int?)nField.GetValue(d) - ?? throw new Exception("Expected private field 'n' to be in int for DimAbsolute"); + var da = (DimAbsolute)d; + n = da.Size; return true; } @@ -133,8 +155,7 @@ public static bool IsCombine(this Dim d) { return false; } - - return d.GetType().Name == "DimCombine"; + return d is DimCombine; } /// @@ -151,14 +172,11 @@ public static bool IsCombine(this Dim d, out Dim left, out Dim right, out bool a { if (d.IsCombine()) { - var fLeft = d.GetType().GetField("left", BindingFlags.NonPublic | BindingFlags.Instance) ?? throw new Exception("Expected private field was missing from Dim.Combine"); - left = fLeft.GetValue(d) as Dim ?? throw new Exception("Expected private field in DimCombine to be of Type Dim"); - - var fRight = d.GetType().GetField("right", BindingFlags.NonPublic | BindingFlags.Instance) ?? throw new Exception("Expected private field was missing from Dim.Combine"); - right = fRight.GetValue(d) as Dim ?? throw new Exception("Expected private field in DimCombine to be of Type Dim"); + var dc = (DimCombine)d; - var fAdd = d.GetType().GetField("add", BindingFlags.NonPublic | BindingFlags.Instance) ?? throw new Exception("Expected private field was missing from Dim.Combine"); - add = fAdd.GetValue(d) as bool? ?? throw new Exception("Expected private field in DimCombine to be of Type bool"); + left = dc.Left; + right = dc.Right; + add = dc.Add == AddOrSubtract.Add; return true; } @@ -175,13 +193,13 @@ public static bool IsCombine(this Dim d, out Dim left, out Dim right, out bool a /// /// The to determine type of. /// The determined type. - /// The numerical element of the type e.g. for + /// The numerical element of the type e.g. for /// the is the percentage but for the /// is the margin. /// The numerical offset if any, for example -5 in the following: /// Dim.Fill(1)-5 /// True if it was possible to determine . - public static bool GetDimType(this Dim d, out DimType type, out float value, out int offset) + public static bool GetDimType(this Dim d, out DimType type, out int value, out int offset) { if (d.IsAbsolute(out var n)) { @@ -207,6 +225,15 @@ public static bool GetDimType(this Dim d, out DimType type, out float value, out return true; } + // TODO: probably need to care about maxes and mins at some point + if (d.IsAuto(out _, out _, out _)) + { + type = DimType.Auto; + value = 0; + offset = 0; + return true; + } + if (d.IsCombine(out var left, out var right, out var add)) { // we only deal in combines if the right is an absolute @@ -237,8 +264,7 @@ public static bool GetDimType(this Dim d, out DimType type, out float value, out { if (!d.GetDimType(out var type, out var val, out var offset)) { - // could not determine the type - return null; + throw new Exception("Could not determine code for Dim type:" + d.GetType()); } switch (type) @@ -261,16 +287,20 @@ public static bool GetDimType(this Dim d, out DimType type, out float value, out case DimType.Percent: if (offset > 0) { - return $"Dim.Percent({val:G5}f) + {offset}"; + return $"Dim.Percent({val:G5}) + {offset}"; } if (offset < 0) { - return $"Dim.Percent({val:G5}f) - {Math.Abs(offset)}"; + return $"Dim.Percent({val:G5}) - {Math.Abs(offset)}"; } - return $"Dim.Percent({val:G5}f)"; + return $"Dim.Percent({val:G5})"; + + case DimType.Auto: + // TODO: one day support the min/max/style + return "Dim.Auto()"; default: throw new ArgumentOutOfRangeException(nameof(type)); } } diff --git a/src/DimType.cs b/src/DimType.cs index cc2b33f8..2e2670a3 100644 --- a/src/DimType.cs +++ b/src/DimType.cs @@ -13,7 +13,7 @@ public enum DimType Absolute, /// - /// Percent of the remaining width/height e.g. . + /// Percent of the remaining width/height e.g. . /// Percent, @@ -21,4 +21,9 @@ public enum DimType /// Filling the remaining space with a margin e.g. . /// Fill, + + /// + /// Automatically size based on Text property e.g. + /// + Auto } \ No newline at end of file diff --git a/src/EnumerableExtensions.cs b/src/EnumerableExtensions.cs new file mode 100644 index 00000000..7337d2fc --- /dev/null +++ b/src/EnumerableExtensions.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TerminalGuiDesigner +{ + internal static class EnumerableExtensions + { + public static ObservableCollection ToListObs(this IEnumerable e) + { + return new ObservableCollection(e); + } + } +} diff --git a/src/FromCode/CodeToView.cs b/src/FromCode/CodeToView.cs index 27172ac7..6358ee5a 100644 --- a/src/FromCode/CodeToView.cs +++ b/src/FromCode/CodeToView.cs @@ -1,14 +1,18 @@ -using System.ComponentModel; +using System.Collections; +using System.Collections.ObjectModel; +using System.ComponentModel; using System.Reflection; +using System.Text.RegularExpressions; using Basic.Reference.Assemblies; +using JetBrains.Annotations; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Emit; using NLog; -using NStack; using Terminal.Gui; using TerminalGuiDesigner.ToCode; +using static Terminal.Gui.SpinnerStyle; namespace TerminalGuiDesigner.FromCode; @@ -18,7 +22,7 @@ namespace TerminalGuiDesigner.FromCode; /// /// /// Compiling requires having the correct assembly references for dependencies. This is handled -/// by . Most references come from +/// by . Most references come from /// but also . /// public class CodeToView @@ -37,7 +41,10 @@ public CodeToView(SourceCodeFile sourceFile) var syntaxTree = CSharpSyntaxTree.ParseText(File.ReadAllText(sourceFile.CsFile.FullName)); var root = syntaxTree.GetRoot(); - var namespaces = root.DescendantNodes().OfType().ToArray(); + var namespaces = root.DescendantNodes() + .OfType() + .ToArray(); + if (namespaces.Length != 1) { @@ -147,18 +154,30 @@ public Assembly CompileAssembly() var dd = typeof(Enumerable).GetTypeInfo().Assembly.Location; var coreDir = Directory.GetParent(dd) ?? throw new Exception($"Could not find parent directory of dotnet sdk. Sdk directory was {dd}"); - var references = new List(ReferenceAssemblies.Net60); - - references.Add(MetadataReference.CreateFromFile(typeof(View).Assembly.Location)); - references.Add(MetadataReference.CreateFromFile(typeof(ustring).Assembly.Location)); - references.Add(MetadataReference.CreateFromFile(typeof(System.Data.DataTable).Assembly.Location)); - references.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); - references.Add(MetadataReference.CreateFromFile(typeof(MarshalByValueComponent).Assembly.Location)); - references.Add(MetadataReference.CreateFromFile(coreDir.FullName + Path.DirectorySeparatorChar + "mscorlib.dll")); - references.Add(MetadataReference.CreateFromFile(coreDir.FullName + Path.DirectorySeparatorChar + "System.Runtime.dll")); - - var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary); - + var references = new List() + { + MetadataReference.CreateFromFile(typeof(View).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.IO.FileSystemInfo).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location), + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(MarshalByValueComponent).Assembly.Location), + MetadataReference.CreateFromFile(typeof(ObservableCollection).Assembly.Location), + + // New assemblies required by Terminal.Gui version 2 + MetadataReference.CreateFromFile(typeof(Size).Assembly.Location), + MetadataReference.CreateFromFile(typeof(CanBeNullAttribute).Assembly.Location), + + MetadataReference.CreateFromFile(coreDir.FullName + Path.DirectorySeparatorChar + "mscorlib.dll"), + MetadataReference.CreateFromFile(coreDir.FullName + Path.DirectorySeparatorChar + "System.Runtime.dll"), + MetadataReference.CreateFromFile(coreDir.FullName + Path.DirectorySeparatorChar + "System.Collections.dll"), + MetadataReference.CreateFromFile(coreDir.FullName + Path.DirectorySeparatorChar + "System.Data.Common.dll") + , + }; + + var options = new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary + ); + var compilation = CSharpCompilation.Create( Guid.NewGuid().ToString() + ".dll", new CSharpSyntaxTree[] { csTree, designerTree }, diff --git a/src/Keys.yaml b/src/Keys.yaml index 899802a2..b77495c7 100644 --- a/src/Keys.yaml +++ b/src/Keys.yaml @@ -1,28 +1,28 @@ EditProperties: F4 ShowContextMenu: Enter -ViewSpecificOperations: F4, ShiftMask +ViewSpecificOperations: Shift+F4 EditRootProperties: F5 ShowHelp: F1 -New: N, CtrlMask -Open: O, CtrlMask -Save: S, CtrlMask -Redo: Y, CtrlMask -Undo: Z, CtrlMask -Delete: DeleteChar +New: Ctrl+N +Open: Ctrl+O +Save: Ctrl+S +Redo: Ctrl+Y +Undo: Ctrl+Z +Delete: Delete ToggleDragging: F3 AddView: F2 -ToggleShowFocused: L, CtrlMask -ToggleShowBorders: B, CtrlMask +ToggleShowFocused: Ctrl+L +ToggleShowBorders: Ctrl+B RightClick: Button3Clicked -Copy: C, CtrlMask -Paste: V, CtrlMask -Rename: R, CtrlMask -SetShortcut: T, CtrlMask -SelectAll: A, CtrlMask -MoveRight: CursorRight, ShiftMask -MoveLeft: CursorLeft, ShiftMask -MoveUp: CursorUp, ShiftMask -MoveDown: CursorDown, ShiftMask +Copy: Ctrl+C +Paste: Ctrl+V +Rename: Ctrl+R +SetShortcut: Ctrl+T +SelectAll: Ctrl+A +MoveRight: Shift+CursorRight +MoveLeft: Shift+CursorLeft +MoveUp: Shift+CursorUp +MoveDown: Shift+CursorDown ShowColorSchemes: F6 SelectionColor: NormalForeground: BrightGreen diff --git a/src/MenuBarExtensions.cs b/src/MenuBarExtensions.cs index 4d16ca13..9690e73f 100644 --- a/src/MenuBarExtensions.cs +++ b/src/MenuBarExtensions.cs @@ -1,4 +1,3 @@ -using System.Reflection; using Terminal.Gui; namespace TerminalGuiDesigner; @@ -17,7 +16,7 @@ public static class MenuBarExtensions /// Selected or null if none. public static MenuBarItem? GetSelectedMenuItem(this MenuBar menuBar) { - var selected = (int)GetNonNullPrivateFieldValue("selected", menuBar, typeof(MenuBar)); + int selected = menuBar.GetNonNullNonPublicFieldValue( "selected" ); if (selected < 0 || selected >= menuBar.Menus.Length) { @@ -36,6 +35,7 @@ public static class MenuBarExtensions public static MenuBarItem? ScreenToMenuBarItem(this MenuBar menuBar, int screenX) { // These might be changed in Terminal.Gui library + // TODO: Maybe load these from a config file, so we aren't at TG's mercy const int initialWhitespace = 1; const int afterEachItemWhitespace = 2; @@ -44,7 +44,7 @@ public static class MenuBarExtensions return null; } - var clientPoint = menuBar.ScreenToView(screenX, 0); + var clientPoint = menuBar.ScreenToContent(new Point(screenX, 0)); // if click is not in our client area if (clientPoint.X < initialWhitespace) @@ -59,7 +59,7 @@ public static class MenuBarExtensions foreach (var mb in menuBar.Menus) { menuXLocations.Add(distance, mb); - distance += mb.Title.ConsoleWidth + afterEachItemWhitespace; + distance += mb.Title.GetColumns() + afterEachItemWhitespace; } // anything after this is not a click on a menu @@ -75,29 +75,4 @@ public static class MenuBarExtensions // Return the last menu item that begins rendering before this X point return menuXLocations.Last(m => m.Key <= clientPoint.X).Value; } - - /// - /// Changes the even though it has no setter in Terminal.Gui. - /// - /// to change on. - /// The new value for . - public static void SetShortcut(this StatusItem item, Key newShortcut) - { - // See: https://stackoverflow.com/a/40917899/4824531 - const string backingFieldName = "k__BackingField"; - - var field = - typeof(StatusItem).GetField(backingFieldName, BindingFlags.Instance | BindingFlags.NonPublic) - ?? throw new Exception($"Could not find auto backing field '{backingFieldName}'"); - - field.SetValue(item, newShortcut); - } - - private static object GetNonNullPrivateFieldValue(string fieldName, object item, Type type) - { - var selectedField = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance) - ?? throw new Exception($"Expected private field {fieldName} was not present on {type.Name}"); - return selectedField.GetValue(item) - ?? throw new Exception($"Private field {fieldName} was unexpectedly null on {type.Name}"); - } } diff --git a/src/MenuTracker.cs b/src/MenuTracker.cs index f43e4cb6..06399592 100644 --- a/src/MenuTracker.cs +++ b/src/MenuTracker.cs @@ -1,4 +1,6 @@ -using Terminal.Gui; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using Terminal.Gui; namespace TerminalGuiDesigner; @@ -8,7 +10,7 @@ namespace TerminalGuiDesigner; /// public class MenuTracker { - private HashSet bars = new(); + private readonly ConcurrentBag bars = new( ); private MenuTracker() { @@ -45,6 +47,22 @@ public void Register(MenuBar mb) this.bars.Add(mb); } + /// + /// Unregisters listeners for . + /// + /// to stop tracking. + public void UnregisterMenuBar( MenuBar? mb ) + { + if ( !bars.TryTake( out mb ) ) + { + return; + } + + mb.MenuAllClosed -= MenuAllClosed; + mb.MenuOpened -= MenuOpened; + mb.MenuClosing -= MenuClosing; + } + /// /// /// Searches child items of all MenuBars tracked by this class @@ -58,9 +76,10 @@ public void Register(MenuBar mb) /// The item whose parent you want to find. /// The that owns or. /// null if not found or parent not registered (see ). - /// The immediate parent of . May be a top level menu (e.g. File, View) - /// or a sub-menu parent (e.g. View=>Windows). - public MenuBarItem? GetParent(MenuItem item, out MenuBar? hostBar) + /// The immediate parent of . + /// Result may be a top level menu (e.g. File, View) + /// or a sub-menu parent (e.g. View=>Windows). + private MenuBarItem? GetParent( MenuItem item, out MenuBar? hostBar ) { foreach (var bar in this.bars) { @@ -80,6 +99,36 @@ public void Register(MenuBar mb) return null; } + /// + /// Searches child items of all MenuBars tracked by this class to try and find the parent of the item passed. + /// + /// The item whose parent you want to find. + /// + /// When this method returns true, the that owns .
Otherwise, if + /// not found or parent not registered (see ). + /// + /// + /// When this method returns , the immediate parent of .
Otherwise, + /// + /// + /// + /// Search is recursive and dips into sub-menus.
For sub-menus it is the immediate parent that is returned. + ///
+ /// A indicating if the search was successful or not. + public bool TryGetParent( MenuItem item, [NotNullWhen( true )] out MenuBar? hostBar, [NotNullWhen( true )] out MenuBarItem? parentItem ) + { + var parentCandidate = GetParent( item, out hostBar ); + if ( parentCandidate is null ) + { + hostBar = null; + parentItem = null; + return false; + } + + parentItem = parentCandidate; + return true; + } + /// /// Iterates all menus (e.g. 'File F9', 'View' etc) of a MenuBar and /// identifies any entries that have empty sub-menus (MenuBarItem). @@ -89,22 +138,21 @@ public void Register(MenuBar mb) /// the substitution object (). See /// /// for more information. - public Dictionary ConvertEmptyMenus() + public Dictionary ConvertEmptyMenus( ) { - var toReturn = new Dictionary(); - + Dictionary dictionary = []; foreach (var b in this.bars) { foreach (var bi in b.Menus) { - foreach (var converted in this.ConvertEmptyMenus(b, bi)) + foreach ( ( MenuBarItem? convertedMenuBarItem, MenuItem? convertedMenuItem ) in this.ConvertEmptyMenus( dictionary, b, bi ) ) { - toReturn.Add(converted.Key, converted.Value); + dictionary.TryAdd( convertedMenuBarItem, convertedMenuItem ); } } } - return toReturn; + return dictionary; } /// @@ -120,25 +168,22 @@ public Dictionary ConvertEmptyMenus() /// The result of the conversion (same text, same index etc but /// instead of ). /// if conversion was possible (menu was empty and belonged to tracked menu). - public bool ConvertMenuBarItemToRegularItemIfEmpty(MenuBarItem bar, out MenuItem? added) + internal static bool ConvertMenuBarItemToRegularItemIfEmpty( MenuBarItem bar, [NotNullWhen( true )] out MenuItem? added ) { added = null; // bar still has more children so don't convert - if (bar.Children.Any()) + if ( bar.Children.Length != 0 ) { return false; } - var parent = MenuTracker.Instance.GetParent(bar, out _); - - if (parent == null) + if ( !Instance.TryGetParent( bar, out _, out MenuBarItem? parent ) ) { return false; } - var children = parent.Children.ToList(); - var idx = children.IndexOf(bar); + int idx = Array.IndexOf( parent.Children, bar ); if (idx < 0) { @@ -146,53 +191,46 @@ public bool ConvertMenuBarItemToRegularItemIfEmpty(MenuBarItem bar, out MenuItem } // bar has no children so convert to MenuItem - added = new MenuItem { Title = bar.Title }; - added.Data = bar.Data; - added.Shortcut = bar.Shortcut; - - children.RemoveAt(idx); - children.Insert(idx, added); - - parent.Children = children.ToArray(); + parent.Children[ idx ] = added = new( ) + { + Title = bar.Title, + Data = bar.Data, + ShortcutKey = bar.ShortcutKey + }; return true; } /// - private Dictionary ConvertEmptyMenus(MenuBar bar, MenuBarItem mbi) + private Dictionary ConvertEmptyMenus(Dictionary dictionary, MenuBar bar, MenuBarItem mbi) { - var toReturn = new Dictionary(); - foreach (var c in mbi.Children.OfType()) { - this.ConvertEmptyMenus(bar, c); - if (this.ConvertMenuBarItemToRegularItemIfEmpty(c, out var added)) + this.ConvertEmptyMenus(dictionary,bar, c); + if ( ConvertMenuBarItemToRegularItemIfEmpty( c, out MenuItem? added)) { - if (added != null) - { - toReturn.Add(c, added); - } + dictionary.TryAdd( c, added ); - bar.CloseMenu(); + bar.CloseMenu(false); bar.OpenMenu(); } } - return toReturn; + return dictionary; } - private void MenuClosing(MenuClosingEventArgs obj) + private void MenuClosing(object? sender, MenuClosingEventArgs obj) { this.CurrentlyOpenMenuItem = null; } - private void MenuOpened(MenuItem obj) + private void MenuOpened(object? sender, MenuOpenedEventArgs obj) { - this.CurrentlyOpenMenuItem = obj; - this.ConvertEmptyMenus(); + this.CurrentlyOpenMenuItem = obj.MenuItem; + this.ConvertEmptyMenus( ); } - private void MenuAllClosed() + private void MenuAllClosed(object? sender, EventArgs e) { this.CurrentlyOpenMenuItem = null; } diff --git a/src/ObjectExtensions.cs b/src/ObjectExtensions.cs index 13f64068..0955d5e5 100644 --- a/src/ObjectExtensions.cs +++ b/src/ObjectExtensions.cs @@ -1,6 +1,6 @@ using System.CodeDom; using System.Reflection; -using NStack; +using System.Text; using Terminal.Gui; namespace TerminalGuiDesigner; @@ -18,11 +18,6 @@ public static class ObjectExtensions /// Hard typed dynamic of . public static dynamic CastToReflected(this object o, Type type) { - if (o is string s && type == typeof(ustring)) - { - return ustring.Make(s); - } - var methodInfo = typeof(ObjectExtensions).GetMethod(nameof(CastTo), BindingFlags.Static | BindingFlags.NonPublic); var genericArguments = new[] { type }; var genericMethodInfo = methodInfo?.MakeGenericMethod(genericArguments); @@ -47,68 +42,34 @@ public static CodePrimitiveExpression ToCodePrimitiveExpression(this object? val /// value if it is not possible to convert. Supports: /// /// - /// to /// to /// Absolute to int /// Absolute to int /// /// /// Value type to convert. - /// A simpler value type for e.g. convert + /// A simpler value type for e.g. convert PosAbsolute to int /// to . If no conversion exists then is returned /// unchanged. /// Thrown if Type is or /// but not absolute (i.e. cannot be converted to a value type - int). public static object? ToPrimitive(this object? value) { - if (value == null) - { - return null; - } - - if (value is ustring u) - { - return u.ToString(); - } - - if (value is Rune r) - { - return (char)r; - } - - if (value is Pos p) + return value switch { + null => null, // Value is a position e.g. X=2 // Pos can be many different subclasses all of which are public // lets deal with only PosAbsolute for now - if (p.IsAbsolute(out int n)) - { - return n; - } - else - { - throw new ArgumentException("Only absolute positions are supported at the moment"); - } - } - else if (value is Dim d) - { + Pos p when p.IsAbsolute( out int n ) => n, + Pos => throw new ArgumentException( "Only absolute positions are supported at the moment" ), // Value is a position e.g. X=2 // Dim can be many different subclasses all of which are public // lets deal with only DimAbsolute for now - if (d.IsAbsolute(out int n)) - { - return n; - } - else - { - throw new ArgumentException("Only absolute dimensions are convertible to primitives"); - } - } - else - { - // assume it is already a primitive - return value; - } + Dim d when d.IsAbsolute( out int n ) => n, + Dim => throw new ArgumentException( "Only absolute dimensions are convertible to primitives" ), + _ => value + }; } private static T CastTo(this object o) => (T)o; diff --git a/src/Operations/AddViewOperation.cs b/src/Operations/AddViewOperation.cs index 01e636b5..ed890487 100644 --- a/src/Operations/AddViewOperation.cs +++ b/src/Operations/AddViewOperation.cs @@ -76,12 +76,25 @@ protected override bool DoImpl() { if (this.add == null) { - var factory = new ViewFactory(); - var selectable = ViewFactory.GetSupportedViews().ToArray(); + var selectable = ViewFactory.SupportedViewTypes.ToArray(); - if (Modals.Get("Type of Control", "Add", true, selectable, t => t?.Name ?? "Null", false, null, out var selected) && selected != null) + if (Modals.Get("Type of Control", "Add", true, selectable, this.TypeNameDelegate, false, null, out var selected) && selected != null) { - this.add = factory.Create(selected); + if (selected.IsGenericType) + { + var allowedTTypes = TTypes.GetSupportedTTypesForGenericViewOfType(selected).ToArray(); + + if(Modals.Get("Enter a Type for ", "Choose", true, allowedTTypes, this.TypeNameDelegate, false, null, out var selectedTType) && selectedTType != null) + { + selected = selected.MakeGenericType(new[] { selectedTType }); + } + else + { + return false; + } + } + + this.add = ViewFactory.Create(selected); this.fieldName = this.to.GetUniqueFieldName(selected); } } @@ -109,6 +122,16 @@ protected override bool DoImpl() return true; } + private string TypeNameDelegate(Type? t) + { + if (t == null) + { + return "Null"; + } + + return t.Name.Replace("`1", ""); + } + private View GetViewToAddTo() { if (this.to.View is TabView tabView) diff --git a/src/Operations/DeleteColorSchemeOperation.cs b/src/Operations/DeleteColorSchemeOperation.cs index 3bb0926a..15a82dcb 100644 --- a/src/Operations/DeleteColorSchemeOperation.cs +++ b/src/Operations/DeleteColorSchemeOperation.cs @@ -76,15 +76,16 @@ protected override bool DoImpl() { switch (d.View) { - case Dialog: return Colors.Dialog; - case Window: return Colors.Base; + case Dialog: return Colors.ColorSchemes["Dialog"]; + case Window: return Colors.ColorSchemes["Base"]; default: return null; } } if (d.View is MenuBar) { - return Colors.Menu; + + return Colors.ColorSchemes["Menu"]; } return null; diff --git a/src/Operations/DragOperation.cs b/src/Operations/DragOperation.cs index 22e9d0e7..b4eb800e 100644 --- a/src/Operations/DragOperation.cs +++ b/src/Operations/DragOperation.cs @@ -202,13 +202,13 @@ private bool Do(DragMemento mem) if (mem.Design.View.X.IsAbsolute() && mem.OriginalX.IsAbsolute(out var originX)) { - mem.Design.GetDesignableProperty("X")?.SetValue(Pos.At(originX + (this.DestinationX - dx))); + mem.Design.GetDesignableProperty("X")?.SetValue(Pos.Absolute(originX + (this.DestinationX - dx))); } if (mem.Design.View.Y.IsAbsolute() && mem.OriginalY.IsAbsolute(out var originY)) { - mem.Design.GetDesignableProperty("Y")?.SetValue(Pos.At(originY + (this.DestinationY - dy))); + mem.Design.GetDesignableProperty("Y")?.SetValue(Pos.Absolute(originY + (this.DestinationY - dy))); } return true; @@ -229,11 +229,11 @@ private Point OffsetByDropInto(DragMemento mem, Point p) } // Calculate screen coordinates of 0,0 in each of the views (from and to) - mem.OriginalSuperView.ViewToScreenActual(0, 0, out var originalSuperX, out var originalSuperY, false); - this.DropInto.ViewToScreenActual(0, 0, out var newSuperX, out var newSuperY, false); + var originalSuper = mem.OriginalSuperView.ContentToScreen(new Point(0, 0)); + var newSuper = this.DropInto.ContentToScreen(new Point(0, 0)); // Offset the point by the difference in screen space between 0,0 on each view - p.Offset(newSuperX - originalSuperX, newSuperY - originalSuperY); + p.Offset(newSuper.X - originalSuper.X, newSuper.Y - originalSuper.Y); return p; } diff --git a/src/Operations/Generics/AddOperation.cs b/src/Operations/Generics/AddOperation.cs index 08e9cb6d..e49edbf2 100644 --- a/src/Operations/Generics/AddOperation.cs +++ b/src/Operations/Generics/AddOperation.cs @@ -5,7 +5,7 @@ namespace TerminalGuiDesigner.Operations.Generics; /// /// Generic abstract class for an which adds a new element -/// to a collection hosted by a e.g. a new +/// to a collection hosted by a e.g. a new /// to a . /// /// The that hosts the collection you want to modify. diff --git a/src/Operations/Generics/RenameOperation.cs b/src/Operations/Generics/RenameOperation.cs index 8c497d8f..b2e086c0 100644 --- a/src/Operations/Generics/RenameOperation.cs +++ b/src/Operations/Generics/RenameOperation.cs @@ -9,7 +9,7 @@ namespace TerminalGuiDesigner.Operations.Generics; /// of Type . /// /// Type of that hosts the collection. -/// Type of array element that is to be renamed (e.g. ). +/// Type of array element that is to be renamed (e.g. ). public abstract class RenameOperation : GenericArrayElementOperation where T1 : View { diff --git a/src/Operations/IOperation.cs b/src/Operations/IOperation.cs index 91d81a0c..1f9822da 100644 --- a/src/Operations/IOperation.cs +++ b/src/Operations/IOperation.cs @@ -30,6 +30,11 @@ public interface IOperation ///
bool SupportsUndo { get; } + /// + /// Gets the number of times this has been executed successfully + /// + ref int TimesDone { get; } + /// /// Performs the operation. /// diff --git a/src/Operations/MenuOperations/AddMenuOperation.cs b/src/Operations/MenuOperations/AddMenuOperation.cs index 2ddd8bbb..3c22918c 100644 --- a/src/Operations/MenuOperations/AddMenuOperation.cs +++ b/src/Operations/MenuOperations/AddMenuOperation.cs @@ -8,20 +8,6 @@ namespace TerminalGuiDesigner.Operations.MenuOperations; /// public class AddMenuOperation : AddOperation { - /// - /// - /// adds a new top level menu (e.g. File, Edit etc). In the designer - /// all menus must have at least 1 under them so it will be - /// created with a single in it already. That item will - /// bear this text. - /// - /// - /// This string should be used by any other areas of code that want to create new under - /// a top/sub menu (e.g. ). - /// - /// - public const string DefaultMenuItemText = "Edit Me"; - /// /// Initializes a new instance of the class. /// Calling will add a new top level menu to the @@ -35,7 +21,7 @@ public AddMenuOperation(Design design, string? name) (v) => v.Menus, (v, a) => v.Menus = a, (s) => s.Title.ToString() ?? "blank menu", - (v, n) => { return new MenuBarItem(n, new MenuItem[] { new MenuItem { Title = DefaultMenuItemText } }); }, + (v, n) => new(n, new MenuItem[] { new() { Title = ViewFactory.DefaultMenuItemText } }), design, name) { diff --git a/src/Operations/MenuOperations/MenuItemOperation.cs b/src/Operations/MenuOperations/MenuItemOperation.cs index c52e260c..5353fbfb 100644 --- a/src/Operations/MenuOperations/MenuItemOperation.cs +++ b/src/Operations/MenuOperations/MenuItemOperation.cs @@ -16,11 +16,9 @@ protected MenuItemOperation(MenuItem operateOn) { // if taking a new line add an extra menu item // menuItem.Parent doesn't work for root menu items - var parent = MenuTracker.Instance.GetParent(operateOn, out var bar); - - if (parent == null || bar == null) + if ( !MenuTracker.Instance.TryGetParent( operateOn, out MenuBar? bar, out MenuBarItem? parent ) ) { - this.IsImpossible = true; + IsImpossible = true; return; } diff --git a/src/Operations/MenuOperations/MoveMenuItemLeftOperation.cs b/src/Operations/MenuOperations/MoveMenuItemLeftOperation.cs index ee61c9ed..3bcfe173 100644 --- a/src/Operations/MenuOperations/MoveMenuItemLeftOperation.cs +++ b/src/Operations/MenuOperations/MoveMenuItemLeftOperation.cs @@ -66,9 +66,7 @@ protected override bool DoImpl() return false; } - var parentsParent = MenuTracker.Instance.GetParent(this.Parent, out var bar); - - if (parentsParent == null) + if ( !MenuTracker.Instance.TryGetParent( Parent, out _, out MenuBarItem? parentsParent ) ) { return false; } @@ -83,7 +81,7 @@ protected override bool DoImpl() if (new RemoveMenuItemOperation(this.OperateOn).Do()) { // We are the parent but parents children don't contain - // us. Thats bad. TODO: log this + // us. That's bad. TODO: log this if (parentsIdx == -1) { return false; diff --git a/src/Operations/MenuOperations/MoveMenuItemRightOperation.cs b/src/Operations/MenuOperations/MoveMenuItemRightOperation.cs index 48029725..39800333 100644 --- a/src/Operations/MenuOperations/MoveMenuItemRightOperation.cs +++ b/src/Operations/MenuOperations/MoveMenuItemRightOperation.cs @@ -110,7 +110,7 @@ private MenuBarItem ConvertToMenuBarItem(List children, int idx) var added = new MenuBarItem(children[idx].Title, new MenuItem[0], null); added.Data = children[idx].Data; - added.Shortcut = children[idx].Shortcut; + added.ShortcutKey = children[idx].ShortcutKey; children.RemoveAt(idx); children.Insert(idx, added); diff --git a/src/Operations/MenuOperations/RemoveMenuItemOperation.cs b/src/Operations/MenuOperations/RemoveMenuItemOperation.cs index d0007a89..c6cc9d60 100644 --- a/src/Operations/MenuOperations/RemoveMenuItemOperation.cs +++ b/src/Operations/MenuOperations/RemoveMenuItemOperation.cs @@ -64,10 +64,12 @@ public override void Undo() return; } - var children = this.Parent.Children.ToList(); - - children.Insert(this.removedAtIdx, this.OperateOn); - this.Parent.Children = children.ToArray(); + this.Parent.Children = + [ + .. Parent.Children[ .. removedAtIdx ], + this.OperateOn, + .. Parent.Children[ removedAtIdx .. ] + ]; this.Bar?.SetNeedsDisplay(); // if any MenuBarItem were converted to vanilla MenuItem @@ -77,15 +79,13 @@ public override void Undo() { foreach (var converted in this.convertedMenuBars) { - var grandparent = MenuTracker.Instance.GetParent(converted.Value, out _); - if (grandparent != null) + if(MenuTracker.Instance.TryGetParent(converted.Value,out _, out MenuBarItem? grandparent)) { - var popIdx = Array.IndexOf(grandparent.Children, converted.Value); - var newParents = grandparent.Children.ToList(); - newParents.RemoveAt(popIdx); - newParents.Insert(popIdx, converted.Key); - - grandparent.Children = newParents.ToArray(); + int replacementIndex = Array.IndexOf(grandparent.Children, converted.Value); + if(replacementIndex >=0 && replacementIndex < grandparent.Children.Length) + { + grandparent.Children[ replacementIndex ] = converted.Key; + } } } } @@ -125,12 +125,12 @@ protected override bool DoImpl() return false; } - var children = this.Parent.Children.ToList(); - - this.removedAtIdx = Math.Max(0, children.IndexOf(this.OperateOn)); - - children.Remove(this.OperateOn); - this.Parent.Children = children.ToArray(); + this.removedAtIdx = Math.Max( 0, Array.IndexOf( Parent.Children, OperateOn ) ); + this.Parent.Children = + [ + .. Parent.Children[ ..removedAtIdx ], + .. Parent.Children[ ( removedAtIdx + 1 ).. ] + ]; this.Bar?.SetNeedsDisplay(); if (this.Bar != null) @@ -139,27 +139,24 @@ protected override bool DoImpl() } // if a top level menu now has no children - if (this.Bar != null) + var empty = this.Bar?.Menus.Where(bi => bi.Children.Length == 0).ToArray(); + if (empty?.Any() == true) { - var empty = this.Bar.Menus.Where(bi => bi.Children.Length == 0).ToArray(); - if (empty.Any()) - { - // remember where they were - this.prunedEmptyTopLevelMenus = empty.ToDictionary(e => Array.IndexOf(this.Bar.Menus, e), v => v); + // remember where they were + this.prunedEmptyTopLevelMenus = empty.ToDictionary(e => Array.IndexOf(this.Bar.Menus, e), v => v); - // and remove them - this.Bar.Menus = this.Bar.Menus.Except(this.prunedEmptyTopLevelMenus.Values).ToArray(); - } + // and remove them + this.Bar.Menus = this.Bar.Menus.Except(this.prunedEmptyTopLevelMenus.Values).ToArray(); + } - // if we just removed the last menu header - // leaving a completely blank menu bar - if (this.Bar.Menus.Length == 0 && this.Bar.SuperView != null) - { - // remove the bar completely - this.Bar.CloseMenu(); - this.barRemovedFrom = this.Bar.SuperView; - this.barRemovedFrom.Remove(this.Bar); - } + // if we just removed the last menu header + // leaving a completely blank menu bar + if (this.Bar?.Menus.Length == 0 && this.Bar.SuperView != null) + { + // remove the bar completely + this.Bar.CloseMenu(false); + this.barRemovedFrom = this.Bar.SuperView; + this.barRemovedFrom.Remove(this.Bar); } return true; diff --git a/src/Operations/MoveViewOperation.cs b/src/Operations/MoveViewOperation.cs index 24617f5b..ebdf1aae 100644 --- a/src/Operations/MoveViewOperation.cs +++ b/src/Operations/MoveViewOperation.cs @@ -28,8 +28,9 @@ public MoveViewOperation(Design toMove, int deltaX, int deltaY) // start out assuming X and Y are PosRelative so cannot be moved this.IsImpossible = true; var super = this.BeingMoved.View.SuperView; - int maxWidth = (super?.Bounds.Width ?? int.MaxValue) - 1; - int maxHeight = (super?.Bounds.Height ?? int.MaxValue) - 1; + + int maxWidth = (super?.GetContentSize().Width ?? int.MaxValue) - 1; + int maxHeight = (super?.GetContentSize().Height ?? int.MaxValue) - 1; if (this.BeingMoved.View.X.IsAbsolute(out var x)) { diff --git a/src/Operations/Operation.cs b/src/Operations/Operation.cs index f7b925e8..be32983c 100644 --- a/src/Operations/Operation.cs +++ b/src/Operations/Operation.cs @@ -5,6 +5,8 @@ /// public abstract class Operation : IOperation { + protected int _timesDone; + /// /// The name to give to all objects which do not have a title/text etc. /// @@ -17,6 +19,9 @@ public abstract class Operation : IOperation /// Defaults to true. public bool SupportsUndo { get; protected set; } = true; + /// + public ref int TimesDone => ref _timesDone; + /// /// Defaults to . public Guid UniqueIdentifier { get; } = Guid.NewGuid(); @@ -32,7 +37,7 @@ public override string ToString() } /// - /// Returns false if . + /// Returns false if or if failed. public bool Do() { if (this.IsImpossible) @@ -40,7 +45,13 @@ public bool Do() return false; } - return this.DoImpl(); + if ( this.DoImpl( ) ) + { + Interlocked.Increment( ref _timesDone ); + return true; + } + + return false; } /// diff --git a/src/Operations/OperationFactory.cs b/src/Operations/OperationFactory.cs index f477fa27..84fcf032 100644 --- a/src/Operations/OperationFactory.cs +++ b/src/Operations/OperationFactory.cs @@ -103,7 +103,7 @@ private IEnumerable CreateOperations(MouseEvent? m, Design d) { var ops = m == null ? d.GetExtraOperations() : - d.GetExtraOperations(d.View.ScreenToView(m.X, m.Y)); + d.GetExtraOperations(d.View.ScreenToContent(m.Position)); foreach (var extra in ops.Where(c => !c.IsImpossible)) { diff --git a/src/Operations/PasteOperation.cs b/src/Operations/PasteOperation.cs index 835b2e8f..606006c4 100644 --- a/src/Operations/PasteOperation.cs +++ b/src/Operations/PasteOperation.cs @@ -141,8 +141,7 @@ private void Paste(View copy, Design into) private bool Paste(Design copy, Design into) { - var v = new ViewFactory(); - var clone = v.Create(copy.View.GetType()); + var clone = ViewFactory.Create(copy.View.GetType()); var addOperation = new AddViewOperation(clone, into, null); // couldn't add for some reason @@ -210,13 +209,15 @@ private void CopyProperties(Design from, Design toClone) private void CloneTableView(TableView copy, TableView pasted) { - pasted.Table = copy.Table.Clone(); + var copyDt = copy.GetDataTable(); + var pastedDt = copyDt.Clone(); - foreach (DataRow row in copy.Table.Rows) + foreach (DataRow row in copyDt.Rows) { - pasted.Table.Rows.Add(row.ItemArray); + pastedDt.Rows.Add(row.ItemArray); } + pasted.Table = new DataTableSource(pastedDt); pasted.Update(); } @@ -231,7 +232,7 @@ private void CloneTabView(TabView copy, TabView pasted) // add a new Tab for each one in the source foreach (var copyTab in copy.Tabs) { - var tab = pasted.AddEmptyTab(copyTab.Text?.ToString() ?? Operation.Unnamed); + var tab = pasted.AddEmptyTab(copyTab.DisplayText?.ToString() ?? Operation.Unnamed); // copy the tab contents copy.SelectedTab = copyTab; diff --git a/src/Operations/ResizeOperation.cs b/src/Operations/ResizeOperation.cs index 5e2b5f55..4f6019e1 100644 --- a/src/Operations/ResizeOperation.cs +++ b/src/Operations/ResizeOperation.cs @@ -96,7 +96,7 @@ private void SetHeight() { // update width, the +1 comes because we want to include the cursor location in the Width. // e.g. resize bounds 0,0 to 1,1 means we want a width/height of 2 - if (this.BeingResized.View.Y.IsAbsolute(out var y)) + if (this.BeingResized.View.Y.IsAbsolute(out var y) && this.BeingResized.View.Height.IsAbsolute()) { this.BeingResized.GetDesignableProperty("Height")?.SetValue(Math.Max(1, this.DestinationY + 1 - y)); } @@ -104,7 +104,7 @@ private void SetHeight() private void SetWidth() { - if (this.BeingResized.View.X.IsAbsolute(out var x)) + if (this.BeingResized.View.X.IsAbsolute(out var x) && this.BeingResized.View.Width.IsAbsolute()) { this.BeingResized.GetDesignableProperty("Width")?.SetValue(Math.Max(1, this.DestinationX + 1 - x)); } diff --git a/src/Operations/StatusBarOperations/AddStatusItemOperation.cs b/src/Operations/StatusBarOperations/AddStatusItemOperation.cs index aa4704b9..1894de5c 100644 --- a/src/Operations/StatusBarOperations/AddStatusItemOperation.cs +++ b/src/Operations/StatusBarOperations/AddStatusItemOperation.cs @@ -4,9 +4,9 @@ namespace TerminalGuiDesigner.Operations.StatusBarOperations { /// - /// Operation for adding a new to a . + /// Operation for adding a new to a . /// - public class AddStatusItemOperation : AddOperation + public class AddStatusItemOperation : AddOperation { /// /// Initializes a new instance of the class. @@ -15,10 +15,10 @@ public class AddStatusItemOperation : AddOperation /// Name for the new item created or null to prompt user. public AddStatusItemOperation(Design design, string? name) : base( - (d) => d.Items, - (d, v) => d.Items = v, + (d) => d.GetShortcuts(), + (d, v) => d.SetShortcuts(v), (v) => v.Title.ToString() ?? Operation.Unnamed, - (d, name) => { return new StatusItem(Key.Null, name, null); }, + (d, name) => { return new Shortcut(KeyCode.Null, name, null); }, design, name) { diff --git a/src/Operations/StatusBarOperations/MoveStatusItemOperation.cs b/src/Operations/StatusBarOperations/MoveStatusItemOperation.cs index 7a552494..f8c4797f 100644 --- a/src/Operations/StatusBarOperations/MoveStatusItemOperation.cs +++ b/src/Operations/StatusBarOperations/MoveStatusItemOperation.cs @@ -6,18 +6,18 @@ namespace TerminalGuiDesigner.Operations.StatusBarOperations /// /// Moves a on a left or right. /// - public class MoveStatusItemOperation : MoveOperation + public class MoveStatusItemOperation : MoveOperation { /// /// Initializes a new instance of the class. /// /// Wrapper for a . - /// The to move. + /// The to move. /// Negative for left, positive for right. - public MoveStatusItemOperation(Design design, StatusItem toMove, int adjustment) + public MoveStatusItemOperation(Design design, Shortcut toMove, int adjustment) : base( - (v) => v.Items, - (v, a) => v.Items = a, + (v) => v.GetShortcuts(), + (v, a) => v.SetShortcuts(a), (e) => e.Title?.ToString() ?? Operation.Unnamed, design, toMove, diff --git a/src/Operations/StatusBarOperations/RemoveStatusItemOperation.cs b/src/Operations/StatusBarOperations/RemoveStatusItemOperation.cs index df82133b..5f0cc8ed 100644 --- a/src/Operations/StatusBarOperations/RemoveStatusItemOperation.cs +++ b/src/Operations/StatusBarOperations/RemoveStatusItemOperation.cs @@ -7,17 +7,17 @@ namespace TerminalGuiDesigner.Operations.StatusBarOperations /// /// Removes a from a . /// - public class RemoveStatusItemOperation : RemoveOperation + public class RemoveStatusItemOperation : RemoveOperation { /// /// Initializes a new instance of the class. /// /// Wrapper for a . /// A to remove from bar. - public RemoveStatusItemOperation(Design design, StatusItem toRemove) + public RemoveStatusItemOperation(Design design, Shortcut toRemove) : base( - (v) => v.Items, - (v, a) => v.Items = a, + (v) => v.GetShortcuts(), + (v, a) => v.SetShortcuts(a), (e) => e.Title?.ToString() ?? Operation.Unnamed, design, toRemove) diff --git a/src/Operations/StatusBarOperations/RenameStatusItemOperation.cs b/src/Operations/StatusBarOperations/RenameStatusItemOperation.cs index bedc9d9c..f32eee92 100644 --- a/src/Operations/StatusBarOperations/RenameStatusItemOperation.cs +++ b/src/Operations/StatusBarOperations/RenameStatusItemOperation.cs @@ -7,7 +7,7 @@ namespace TerminalGuiDesigner.Operations.StatusBarOperations /// /// Renames a on a . /// - public class RenameStatusItemOperation : RenameOperation + public class RenameStatusItemOperation : RenameOperation { /// /// Initializes a new instance of the class. @@ -15,10 +15,10 @@ public class RenameStatusItemOperation : RenameOperation /// Design wrapper for a . /// The to rename. /// The new name to use or null to prompt user. - public RenameStatusItemOperation(Design design, StatusItem toRename, string? newName) + public RenameStatusItemOperation(Design design, Shortcut toRename, string? newName) : base( - (d) => d.Items, - (d, v) => d.Items = v, + (d) => d.GetShortcuts(), + (d, v) => d.SetShortcuts(v), (v) => v.Title.ToString() ?? Operation.Unnamed, (v, n) => v.Title = n, design, diff --git a/src/Operations/StatusBarOperations/SetShortcutOperation.cs b/src/Operations/StatusBarOperations/SetShortcutOperation.cs index 4c3abb92..f0c8195d 100644 --- a/src/Operations/StatusBarOperations/SetShortcutOperation.cs +++ b/src/Operations/StatusBarOperations/SetShortcutOperation.cs @@ -1,4 +1,4 @@ -using Terminal.Gui; +using Terminal.Gui; using TerminalGuiDesigner.Operations.Generics; using TerminalGuiDesigner.UI.Windows; @@ -8,7 +8,7 @@ namespace TerminalGuiDesigner.Operations.StatusBarOperations /// Changes the of a on /// a . /// - public class SetShortcutOperation : GenericArrayElementOperation + public class SetShortcutOperation : GenericArrayElementOperation { private Key originalShortcut; private Key? shortcut; @@ -19,16 +19,16 @@ public class SetShortcutOperation : GenericArrayElementOperationWrapper for a . /// The whose shortcut you want to change. /// The new shortcut or null to prompt user at runtime. - public SetShortcutOperation(Design design, StatusItem statusItem, Key? shortcut) + public SetShortcutOperation(Design design, Shortcut statusItem, Key? shortcut) : base( - (v) => v.Items, - (v, a) => v.Items = a, + (v) => v.GetShortcuts(), + (v, a) => v.SetShortcuts(a), (e) => e.Title?.ToString() ?? Operation.Unnamed, design, statusItem) { this.shortcut = shortcut; - this.originalShortcut = statusItem.Shortcut; + this.originalShortcut = statusItem.Key; } /// @@ -39,13 +39,13 @@ public override void Redo() return; } - this.OperateOn.SetShortcut(this.shortcut.Value); + this.OperateOn.Key = this.shortcut; } /// public override void Undo() { - this.OperateOn.SetShortcut(this.originalShortcut); + this.OperateOn.Key = this.originalShortcut; } /// @@ -56,7 +56,7 @@ protected override bool DoImpl() this.shortcut = Modals.GetShortcut(); } - this.OperateOn.SetShortcut(this.shortcut.Value); + this.OperateOn.Key = this.shortcut; return true; } } diff --git a/src/Operations/TabOperations/AddTabOperation.cs b/src/Operations/TabOperations/AddTabOperation.cs index 7a19a412..606aa99e 100644 --- a/src/Operations/TabOperations/AddTabOperation.cs +++ b/src/Operations/TabOperations/AddTabOperation.cs @@ -18,7 +18,7 @@ public AddTabOperation(Design design, string? name) : base( (t) => t.Tabs.ToArray(), (v, a) => v.ReOrderTabs(a), - tab => tab.Text.ToString() ?? "unnamed tab", + tab => tab.DisplayText.ToString() ?? "unnamed tab", AddTab, design, name) @@ -27,7 +27,11 @@ public AddTabOperation(Design design, string? name) private static Tab AddTab(TabView view, string name) { - var tab = new Tab(name, new View { Width = Dim.Fill(), Height = Dim.Fill() }); + var tab = new Tab() + { + DisplayText = name, + View = new View { Width = Dim.Fill(), Height = Dim.Fill() } + }; view.AddTab(tab, true); return tab; } diff --git a/src/Operations/TabOperations/MoveTabOperation.cs b/src/Operations/TabOperations/MoveTabOperation.cs index caf2130d..6d7b2c9a 100644 --- a/src/Operations/TabOperations/MoveTabOperation.cs +++ b/src/Operations/TabOperations/MoveTabOperation.cs @@ -4,10 +4,10 @@ namespace TerminalGuiDesigner.Operations.TabOperations; /// -/// Moves a left or right within the ordering +/// Moves a left or right within the ordering /// of tabs in a . /// -public class MoveTabOperation : MoveOperation +public class MoveTabOperation : MoveOperation { /// /// Initializes a new instance of the class. @@ -17,7 +17,7 @@ public class MoveTabOperation : MoveOperation /// Wrapper for a . /// The Tab to move. /// Negative to move tab left, positive to move tab right. - public MoveTabOperation(Design design, TabView.Tab toMove, int adjustment) + public MoveTabOperation(Design design, Tab toMove, int adjustment) : base( (t) => t.Tabs.ToArray(), (v, a) => v.ReOrderTabs(a), diff --git a/src/Operations/TabOperations/RemoveTabOperation.cs b/src/Operations/TabOperations/RemoveTabOperation.cs index ad701ff3..4a447bc1 100644 --- a/src/Operations/TabOperations/RemoveTabOperation.cs +++ b/src/Operations/TabOperations/RemoveTabOperation.cs @@ -4,9 +4,9 @@ namespace TerminalGuiDesigner.Operations.TabOperations; /// -/// Removes (deletes) a from a . +/// Removes (deletes) a from a . /// -public class RemoveTabOperation : RemoveOperation +public class RemoveTabOperation : RemoveOperation { /// /// Initializes a new instance of the class. @@ -15,7 +15,7 @@ public class RemoveTabOperation : RemoveOperation /// Wrapper for a from which you want to remove the tab. /// The tab to remove. /// Thrown if does not wrap a . - public RemoveTabOperation(Design design, TabView.Tab toRemove) + public RemoveTabOperation(Design design, Tab toRemove) : base( (t) => t.Tabs.ToArray(), (v, a) => v.ReOrderTabs(a), diff --git a/src/Operations/TabOperations/RenameTabOperation.cs b/src/Operations/TabOperations/RenameTabOperation.cs index e06f1864..461604d8 100644 --- a/src/Operations/TabOperations/RenameTabOperation.cs +++ b/src/Operations/TabOperations/RenameTabOperation.cs @@ -4,24 +4,24 @@ namespace TerminalGuiDesigner.Operations.TabOperations; /// -/// Renames the of the currently selected -/// of a . +/// Renames the of the currently selected +/// of a . /// -public class RenameTabOperation : RenameOperation +public class RenameTabOperation : RenameOperation { /// /// Initializes a new instance of the class. - /// This command changes the on a . + /// This command changes the on a . /// /// Wrapper for a . /// Tab to rename. /// New name to use or null to prompt. - public RenameTabOperation(Design design, TabView.Tab toRename, string? newName) + public RenameTabOperation(Design design, Tab toRename, string? newName) : base( (t) => t.Tabs.ToArray(), (v, a) => v.ReOrderTabs(a), - tab => tab.Text.ToString() ?? "unnamed tab", - (tab, n) => tab.Text = n, + tab => tab.DisplayText.ToString() ?? "unnamed tab", + (tab, n) => tab.DisplayText = n, design, toRename, newName) diff --git a/src/Operations/TableViewOperations/AddColumnOperation.cs b/src/Operations/TableViewOperations/AddColumnOperation.cs index 31ed0e55..d524ff19 100644 --- a/src/Operations/TableViewOperations/AddColumnOperation.cs +++ b/src/Operations/TableViewOperations/AddColumnOperation.cs @@ -18,10 +18,10 @@ public class AddColumnOperation : AddOperation /// Thrown if is not wrapping a . public AddColumnOperation(Design design, string? newColumnName) : base( - (v) => v.Table.Columns.Cast().ToArray(), + (v) => v.GetDataTable().Columns.Cast().ToArray(), (v, a) => v.ReOrderColumns(a), (c) => c.ColumnName, - (v, n) => { return v.Table?.Columns.Add(n) ?? throw new Exception("TableView Table had not been initialized at the time we were asked to construct a new DataColumn for it."); }, + (v, n) => { return v.GetDataTable()?.Columns.Add(n) ?? throw new Exception("TableView Table had not been initialized at the time we were asked to construct a new DataColumn for it."); }, design, newColumnName) { diff --git a/src/Operations/TableViewOperations/MoveColumnOperation.cs b/src/Operations/TableViewOperations/MoveColumnOperation.cs index bcaf5568..2100f829 100644 --- a/src/Operations/TableViewOperations/MoveColumnOperation.cs +++ b/src/Operations/TableViewOperations/MoveColumnOperation.cs @@ -20,7 +20,7 @@ public class MoveColumnOperation : MoveOperation /// Negative to move left, positive to move right. public MoveColumnOperation(Design design, DataColumn column, int adjustment) : base( - (v) => v.Table.Columns.Cast().ToArray(), + (v) => v.GetDataTable().Columns.Cast().ToArray(), (v, a) => v.ReOrderColumns(a), (c) => c.ColumnName, design, diff --git a/src/Operations/TableViewOperations/RemoveColumnOperation.cs b/src/Operations/TableViewOperations/RemoveColumnOperation.cs index 10f323f2..94e3632c 100644 --- a/src/Operations/TableViewOperations/RemoveColumnOperation.cs +++ b/src/Operations/TableViewOperations/RemoveColumnOperation.cs @@ -17,7 +17,7 @@ public class RemoveColumnOperation : RemoveOperation /// Column to remove. public RemoveColumnOperation(Design design, DataColumn column) : base( - (v) => v.Table.Columns.Cast().ToArray(), + (v) => v.GetDataTable().Columns.Cast().ToArray(), (v, a) => v.ReOrderColumns(a), (c) => c.ColumnName, design, @@ -25,7 +25,7 @@ public RemoveColumnOperation(Design design, DataColumn column) { // TODO: currently this crashes TableView in its ReDraw (calculate view port) method // don't let them remove the last column - if (this.View.Table.Columns.Count == 1) + if (this.View.GetDataTable().Columns.Count == 1) { this.IsImpossible = true; } diff --git a/src/Operations/TableViewOperations/RenameColumnOperation.cs b/src/Operations/TableViewOperations/RenameColumnOperation.cs index 0d2c3892..855904fa 100644 --- a/src/Operations/TableViewOperations/RenameColumnOperation.cs +++ b/src/Operations/TableViewOperations/RenameColumnOperation.cs @@ -18,7 +18,7 @@ public class RenameColumnOperation : RenameOperation /// Thrown if does not wrap a . public RenameColumnOperation(Design design, DataColumn column, string? newName) : base( - (v) => v.Table.Columns.Cast().ToArray(), + (v) => v.GetDataTable().Columns.Cast().ToArray(), (v, a) => v.ReOrderColumns(a), (c) => c.ColumnName, (c, name) => c.ColumnName = name, diff --git a/src/Options.cs b/src/Options.cs index a87829a8..bf8657e5 100644 --- a/src/Options.cs +++ b/src/Options.cs @@ -49,11 +49,6 @@ public static IEnumerable Examples [Option('n', HelpText = "The C# namespace to be used for the View code generated")] public string Namespace { get; set; } - /// - /// Gets or sets a value indicating whether should be enabled. - /// - [Option(HelpText = "Enables UseSystemConsole, an alternative console display driver")] - public bool Usc { get; set; } /// /// Gets or sets a value indicating whether experimental new features should be accessible. diff --git a/src/PosExtensions.cs b/src/PosExtensions.cs index ea116991..853d2835 100644 --- a/src/PosExtensions.cs +++ b/src/PosExtensions.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; using Terminal.Gui; namespace TerminalGuiDesigner; @@ -18,21 +19,16 @@ public static class PosExtensions /// /// to classify. /// True if is absolute. - public static bool IsAbsolute(this Pos p) + public static bool IsAbsolute(this Pos? p) { - if (p == null) - { - return TreatNullPosAs0; - } - - return p.GetType().Name == "PosAbsolute"; + return p is PosAbsolute; } /// /// to classify. /// The absolute size or 0. /// True if is absolute. - public static bool IsAbsolute(this Pos p, out int n) + public static bool IsAbsolute(this Pos? p, out int n) { if (p.IsAbsolute()) { @@ -42,10 +38,9 @@ public static bool IsAbsolute(this Pos p, out int n) return TreatNullPosAs0; } - var nField = p.GetType().GetField("n", BindingFlags.NonPublic | BindingFlags.Instance) - ?? throw new Exception("Expected private field 'n' of PosAbsolute was missing"); - n = (int?)nField.GetValue(p) - ?? throw new Exception("Expected private field 'n' of PosAbsolute to be int"); + var pa = (PosAbsolute)p; + + n = pa.Position; return true; } @@ -54,35 +49,35 @@ public static bool IsAbsolute(this Pos p, out int n) } /// - /// Returns true if is a percentage position (See ). + /// Returns true if is a percentage position (See ). /// /// to classify. - /// True if is . - public static bool IsPercent(this Pos p) + /// True if is . + public static bool IsPercent(this Pos? p) { if (p == null) { return false; } - return p.GetType().Name == "PosFactor"; + return p is PosPercent; } /// /// to classify. /// The percentage number (typically out of 100) that could be passed - /// to to produce or 0 if + /// to to produce or 0 if /// is not a percent . - /// True if is . - public static bool IsPercent(this Pos p, out float percent) + /// True if is . + public static bool IsPercent(this Pos? p, out int percent) { if (p != null && p.IsPercent()) { - var nField = p.GetType().GetField("factor", BindingFlags.NonPublic | BindingFlags.Instance) - ?? throw new Exception("Expected private field 'factor' was missing from PosFactor"); - percent = ((float?)nField.GetValue(p) - ?? throw new Exception("Expected private field 'factor' of PosFactor to be float")) - * 100f; + var pp = (PosPercent)p; + + // TODO: presumably no longer needs *100? + percent = pp.Percent; + return true; } @@ -95,37 +90,37 @@ public static bool IsPercent(this Pos p, out float percent) /// /// to classify. /// True if . - public static bool IsCenter(this Pos p) + public static bool IsCenter(this Pos? p) { if (p == null) { return false; } - return p.GetType().Name == "PosCenter"; + return p is PosCenter; } /// - /// Returns true if is the result of an . + /// Returns true if is the result of a . /// /// to classify. /// True if is . - public static bool IsAnchorEnd(this Pos p) + // BUG: This should not return true on null, because 0 is an absolute Pos + public static bool IsAnchorEnd(this Pos? p) { if (p == null) { return TreatNullPosAs0; } - - return p.GetType().Name == "PosAnchorEnd"; + return p is PosAnchorEnd; } /// /// to classify. - /// The margin passed to . Typically should - /// be 1 or more otherwise things tend to drift off screen. + /// The margin passed to . Should typically + /// be 1 or more otherwise things tend to drift off-screen. /// - public static bool IsAnchorEnd(this Pos p, out int margin) + public static bool IsAnchorEnd(this Pos? p, out int margin) { if (p.IsAnchorEnd()) { @@ -135,10 +130,9 @@ public static bool IsAnchorEnd(this Pos p, out int margin) return TreatNullPosAs0; } - var nField = p.GetType().GetField("n", BindingFlags.NonPublic | BindingFlags.Instance) - ?? throw new Exception("Expected private field 'n' of PosAbsolute was missing"); - margin = (int?)nField.GetValue(p) - ?? throw new Exception("Expected private field 'n' of PosAbsolute to be int"); + var pae = (PosAnchorEnd)p; + + margin = pae.Offset; return true; } @@ -153,7 +147,7 @@ public static bool IsAnchorEnd(this Pos p, out int margin) /// to classify. /// True if is the result of a call to one of the relative methods /// (e.g. ). - public static bool IsRelative(this Pos p) + public static bool IsRelative(this Pos? p) { return p.IsRelative(out _); } @@ -165,7 +159,8 @@ public static bool IsRelative(this Pos p) /// or null if is not . /// representing the method that was used e.g. , etc. /// - public static bool IsRelative(this Pos p, IList knownDesigns, out Design? relativeTo, out Side side) + // BUG: Side should be nullable, because it gets an explicit value in all cases but isn't meaningful if return value was false + public static bool IsRelative(this Pos? p, IList knownDesigns, [NotNullWhen(true)]out Design? relativeTo, out Side side) { relativeTo = null; side = default; @@ -181,8 +176,7 @@ public static bool IsRelative(this Pos p, IList knownDesigns, out Design if (p.IsRelative(out var posView)) { - var fTarget = posView.GetType().GetField("Target") ?? throw new Exception("PosView was missing expected field 'Target'"); - View view = (View?)fTarget.GetValue(posView) ?? throw new Exception("PosView had a null 'Target' view"); + View view = posView.Target; relativeTo = knownDesigns.FirstOrDefault(d => d.View == view); @@ -192,33 +186,15 @@ public static bool IsRelative(this Pos p, IList knownDesigns, out Design { return false; } - - var fSide = posView.GetType().GetField("side", BindingFlags.NonPublic | BindingFlags.Instance) ?? throw new Exception("PosView was missing expected field 'side'"); - var iSide = (int?)fSide.GetValue(posView) - ?? throw new Exception("Expected PosView property 'side' to be of Type int"); - - side = (Side)iSide; + + + side = posView.Side; return true; } return false; } - /// - /// Returns true if is the summation or subtraction of two - /// other . - /// - /// to classify. - /// True if is a PosCombine. - public static bool IsCombine(this Pos p) - { - if (p == null) - { - return false; - } - - return p.GetType().Name == "PosCombine"; - } /// /// to classify. @@ -226,25 +202,20 @@ public static bool IsCombine(this Pos p) /// The right hand operand of the summation/subtraction. /// if addition or if subtraction. /// True if is PosCombine. - public static bool IsCombine(this Pos p, out Pos left, out Pos right, out bool add) + public static bool IsCombine(this Pos? p, out Pos left, out Pos right, out AddOrSubtract add) { - if (p.IsCombine()) + if (p is PosCombine combine) { - var fLeft = p.GetType().GetField("left", BindingFlags.NonPublic | BindingFlags.Instance) ?? throw new Exception("Expected private field missing from PosCombine"); - left = fLeft.GetValue(p) as Pos ?? throw new Exception("Expected field 'left' of PosCombine to be a Pos"); - - var fRight = p.GetType().GetField("right", BindingFlags.NonPublic | BindingFlags.Instance) ?? throw new Exception("Expected private field missing from PosCombine"); - right = fRight.GetValue(p) as Pos ?? throw new Exception("Expected field 'right' of PosCombine to be a Pos"); - - var fAdd = p.GetType().GetField("add", BindingFlags.NonPublic | BindingFlags.Instance) ?? throw new Exception("Expected private field missing from PosCombine"); - add = fAdd.GetValue(p) as bool? ?? throw new Exception("Expected field 'add' of PosCombine to be a bool"); + left = combine.Left; + right = combine.Right; + add = combine.Add; return true; } left = 0; right = 0; - add = false; + add = AddOrSubtract.Add; return false; } @@ -259,7 +230,7 @@ public static bool IsCombine(this Pos p, out Pos left, out Pos right, out bool a /// Only populated for , this is the direction of offset from . /// The offset from the listed position. Is provided if the input has addition/subtraction e.g.Pos.Center() + 2 /// True if it was possible to determine what is. - public static bool GetPosType(this Pos p, IList knownDesigns, out PosType type, out float value, out Design? relativeTo, out Side side, out int offset) + public static bool GetPosType(this Pos? p, IList knownDesigns, out PosType type, out int value, out Design? relativeTo, out Side side, out int offset) { type = default; relativeTo = null; @@ -309,7 +280,7 @@ public static bool GetPosType(this Pos p, IList knownDesigns, out PosTyp // e.g. Pos.Percent(25) + 5 is supported but Pos.Percent(5) + Pos.Percent(10) is not if (right.IsAbsolute(out int rhsVal)) { - offset = add ? rhsVal : -rhsVal; + offset = add == AddOrSubtract.Add ? rhsVal : -rhsVal; GetPosType(left, knownDesigns, out type, out value, out relativeTo, out side, out _); return true; } @@ -325,7 +296,7 @@ public static bool GetPosType(this Pos p, IList knownDesigns, out PosTyp /// to classify. /// All that we might be expressed as relative to (e.g. see ). /// Code to generate . - public static string? ToCode(this Pos p, List knownDesigns) + public static string? ToCode(this Pos? p, List knownDesigns) { if (!p.GetPosType(knownDesigns, out var type, out var val, out var relativeTo, out var side, out var offset)) { @@ -333,70 +304,24 @@ public static bool GetPosType(this Pos p, IList knownDesigns, out PosTyp return null; } - switch (type) + return type switch { - case PosType.Absolute: - return val.ToString(); - case PosType.Relative: - - if (relativeTo == null) - { - throw new Exception("Pos was Relative but 'relativeTo' was null. What is the Pos relative to?!"); - } - - if (offset > 0) - { - return $"Pos.{GetMethodNameFor(side)}({relativeTo.FieldName}) + {offset}"; - } - - if (offset < 0) - { - return $"Pos.{GetMethodNameFor(side)}({relativeTo.FieldName}) - {Math.Abs(offset)}"; - } - - return $"Pos.{GetMethodNameFor(side)}({relativeTo.FieldName})"; - - case PosType.Percent: - if (offset > 0) - { - return $"Pos.Percent({val:G5}f) + {offset}"; - } - - if (offset < 0) - { - return $"Pos.Percent({val:G5}f) - {Math.Abs(offset)}"; - } - - return $"Pos.Percent({val:G5}f)"; - - case PosType.Center: - if (offset > 0) - { - return $"Pos.Center() + {offset}"; - } - - if (offset < 0) - { - return $"Pos.Center() - {Math.Abs(offset)}"; - } - - return $"Pos.Center()"; - - case PosType.AnchorEnd: - if (offset > 0) - { - return $"Pos.AnchorEnd({(int)val}) + {offset}"; - } - - if (offset < 0) - { - return $"Pos.AnchorEnd({(int)val}) - {Math.Abs(offset)}"; - } - - return $"Pos.AnchorEnd({(int)val})"; - - default: throw new ArgumentOutOfRangeException(nameof(type)); - } + PosType.Absolute => val.ToString( "N0" ), + PosType.Relative when relativeTo is null => throw new InvalidOperationException( "Pos was Relative but 'relativeTo' was null. What is the Pos relative to?!" ), + PosType.Relative when offset > 0 => $"Pos.{GetMethodNameFor( side )}({relativeTo.FieldName}) + {offset}", + PosType.Relative when offset < 0 => $"Pos.{GetMethodNameFor( side )}({relativeTo.FieldName}) - {Math.Abs( offset )}", + PosType.Relative => $"Pos.{GetMethodNameFor( side )}({relativeTo.FieldName})", + PosType.Percent when offset > 0 => $"Pos.Percent({val:G5}) + {offset}", + PosType.Percent when offset < 0 => $"Pos.Percent({val:G5}) - {Math.Abs( offset )}", + PosType.Percent => $"Pos.Percent({val:G5})", + PosType.Center when offset > 0 => $"Pos.Center() + {offset}", + PosType.Center when offset < 0 => $"Pos.Center() - {Math.Abs( offset )}", + PosType.Center => $"Pos.Center()", + PosType.AnchorEnd when offset > 0 => $"Pos.AnchorEnd({(int)val}) + {offset}", + PosType.AnchorEnd when offset < 0 => $"Pos.AnchorEnd({(int)val}) - {Math.Abs( offset )}", + PosType.AnchorEnd => $"Pos.AnchorEnd({(int)val})", + _ => throw new ArgumentOutOfRangeException( nameof( type ) ) + }; } /// @@ -409,35 +334,21 @@ public static bool GetPosType(this Pos p, IList knownDesigns, out PosTyp /// The offset if any to use e.g. if you want: /// Pos.Left(myView) + 5 /// The resulting of the invoked method (e.g. . - public static Pos CreatePosRelative(Design relativeTo, Side side, int offset) + // BUG: This returns absolute positions when offsets are non-zero + // It's a Terminal.Gui issue, but we can probably work around it. + public static Pos CreatePosRelative(this Design relativeTo, Side side, int offset = 0) { - Pos pos; - switch (side) + return side switch { - case Side.Top: - pos = Pos.Top(relativeTo.View); - break; - case Side.Bottom: - pos = Pos.Bottom(relativeTo.View); - break; - case Side.Left: - pos = Pos.Left(relativeTo.View); - break; - case Side.Right: - pos = Pos.Right(relativeTo.View); - break; - default: throw new ArgumentOutOfRangeException(nameof(side)); - } - - if (offset != 0) - { - return pos + offset; - } - - return pos; + Side.Top => Pos.Top( relativeTo.View ) + offset, + Side.Bottom => Pos.Bottom( relativeTo.View ) + offset, + Side.Left => Pos.Left( relativeTo.View ) + offset, + Side.Right => Pos.Right( relativeTo.View ) + offset, + _ => throw new ArgumentOutOfRangeException( nameof( side ) ) + }; } - private static bool IsRelative(this Pos p, out Pos posView) + private static bool IsRelative(this Pos? p, [NotNullWhen(true)]out PosView? posView) { // Terminal.Gui will often use Pos.Combine with RHS of 0 instead of just PosView alone if (p != null && p.IsCombine(out var left, out var right, out _)) @@ -448,13 +359,13 @@ private static bool IsRelative(this Pos p, out Pos posView) } } - if (p != null && p.GetType().Name == "PosView") + if (p is PosView pv) { - posView = p; + posView = pv; return true; } - posView = 0; + posView = null; return false; } diff --git a/src/PosType.cs b/src/PosType.cs index e45b2722..bbaa23c5 100644 --- a/src/PosType.cs +++ b/src/PosType.cs @@ -10,7 +10,7 @@ public enum PosType { /// /// An absolute fixed value e.g. 5. - /// May be the result of call + /// May be the result of call /// or an implicit cast of int value e.g. /// myView.X = 5; /// @@ -22,7 +22,7 @@ public enum PosType AnchorEnd, /// - /// Indicates use of . + /// Indicates use of . /// Percent, diff --git a/src/Program.cs b/src/Program.cs index a3e1dfb4..62bf379f 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -18,7 +18,6 @@ public static void Main(string[] args) Parser.Default.ParseArguments(args) .WithParsed(o => { - Application.UseSystemConsole = o.Usc; Editor.Experimental = o.Experimental; Application.Init(); diff --git a/src/RectExtensions.cs b/src/RectExtensions.cs index 18c222bf..1d935179 100644 --- a/src/RectExtensions.cs +++ b/src/RectExtensions.cs @@ -3,26 +3,26 @@ namespace TerminalGuiDesigner.UI; /// -/// Extension methods for the . +/// Extension methods for the . /// public static class RectExtensions { /// - /// Returns a between the two points. Points argument + /// Returns a between the two points. Points argument /// order does not matter (i.e. p2 can be above/below and/or /// left/right of p1). Returns null if either point is null. /// - /// One corner of the you want to create. - /// Opposite corner to of the + /// One corner of the you want to create. + /// Opposite corner to of the /// you want to create. - internal static Rect? FromBetweenPoints(Point? p1, Point? p2) + internal static Rectangle? FromBetweenPoints(Point? p1, Point? p2) { if (p1 == null || p2 == null) { return null; } - return Rect.FromLTRB( + return Rectangle.FromLTRB( Math.Min(p1.Value.X, p2.Value.X), Math.Min(p1.Value.Y, p2.Value.Y), Math.Max(p1.Value.X, p2.Value.X), diff --git a/src/ReflectionHelpers.cs b/src/ReflectionHelpers.cs new file mode 100644 index 00000000..a7e954bd --- /dev/null +++ b/src/ReflectionHelpers.cs @@ -0,0 +1,72 @@ +using System.Reflection; +using Terminal.Gui; + +namespace TerminalGuiDesigner; + +/// Helper methods to simplify reflection operations. +public static class ReflectionHelpers +{ + /// + /// Gets a non-public field value from a non-null inheriting from as a + /// . + /// + /// The type of the field. Must pass a constraint. + /// + /// The type of the to get the field on. + /// + /// The to reflect on. + /// The name of the field to get via reflection. + /// + /// A non-null from the reflected private field of . + /// + /// + /// If the requested does not exist on type . + /// + /// + /// If the requested on type is not of type . + /// + /// + /// If the value of the requested on was null. + /// + internal static TOut GetNonNullNonPublicFieldValue( this TIn? item, string fieldName ) + where TIn : View + where TOut : notnull + { + ArgumentNullException.ThrowIfNull( item, nameof( item ) ); + ArgumentException.ThrowIfNullOrEmpty( fieldName, nameof( fieldName ) ); + + FieldInfo selectedField = typeof( TIn ).GetField( fieldName, BindingFlags.NonPublic | BindingFlags.Instance ) + ?? throw new MissingFieldException( $"Expected non-public instance field {fieldName} was not present on {typeof( TIn ).Name}" ); + + if ( selectedField.FieldType != typeof( TOut ) ) + { + throw new FieldAccessException( $"Field {fieldName} on {typeof( TIn ).Name} is not of expected type {typeof( TOut ).Name}" ); + } + + return (TOut)( selectedField.GetValue( item ) + ?? throw new InvalidOperationException( $"Non-public instance field {fieldName} was unexpectedly null on {typeof( TIn ).Name}" ) ); + } + + /// + /// Creates an instance of type , using its reflected parameterless constructor. + /// + /// The type of to create. Must be a descendant of and must have a valid parameterless constructor. + /// A new instance of a . + /// If the requested type is not a valid as determined by where targetType is . + /// If the attempt to call the reflected constructor returns . + public static View GetDefaultViewInstance( Type t ) + { + if ( !t.IsAssignableTo( typeof(View) ) ) + { + throw new ArgumentOutOfRangeException( nameof( t ), $"{t.Name} must be assignable to the View type" ); + } + + var instance = Activator.CreateInstance( t ) as View ?? throw new InvalidOperationException( $"CreateInstance returned null for Type '{t.Name}'" ); + instance.SetActualText( "Heya" ); + + instance.Width = Math.Max( instance.GetContentSize().Width, 4 ); + instance.Height = Math.Max( instance.GetContentSize().Height, 1 ); + + return instance; + } +} diff --git a/src/SelectionManager.cs b/src/SelectionManager.cs index 0413f7dd..7ed93304 100644 --- a/src/SelectionManager.cs +++ b/src/SelectionManager.cs @@ -10,7 +10,7 @@ namespace TerminalGuiDesigner; /// public class SelectionManager { - private List selection = new(); + private readonly List selection = new(); private ColorScheme? selectedScheme; private SelectionManager() @@ -31,6 +31,8 @@ private SelectionManager() /// Gets or Sets a value indicating whether to prevent changes to the current /// collection (e.g. if running a modal dialog / context menu). /// + // BUG: Thread-safety + // This is not a valid synchronization method and is prone to a bunch of different issues at runtime. public bool LockSelection { get; set; } /// @@ -142,7 +144,8 @@ private void SetSelection(bool respectLock, Design[] designs) this.Clear(respectLock); // create a new selection based on these - this.selection = new List(designs.Distinct().Where(d => !d.IsRoot)); + this.selection.Clear(); + this.selection.AddRange(designs.Distinct().Where(d => d is { IsRoot: false })); foreach (var d in this.selection) { diff --git a/src/Side.cs b/src/Side.cs deleted file mode 100644 index a27e7854..00000000 --- a/src/Side.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Terminal.Gui; - -namespace TerminalGuiDesigner; - -/// -/// Describes the method used to create a . -/// These map to , etc. -/// -/// -/// These enum values match private field 'side' in PosView see: -/// -/// https://github.com/gui-cs/Terminal.Gui/blob/develop/Terminal.Gui/Core/PosDim.cs -/// -/// -public enum Side -{ - /// - /// Describes . - /// - Left = 0, - - /// - /// Describes . - /// - Top = 1, - - /// - /// Describes . - /// - Right = 2, - - /// - /// Describes . - /// - Bottom = 3, -} \ No newline at end of file diff --git a/src/SizeExtensions.cs b/src/SizeExtensions.cs index 12b0a6aa..820a068d 100644 --- a/src/SizeExtensions.cs +++ b/src/SizeExtensions.cs @@ -1,4 +1,4 @@ -using Terminal.Gui; +using System.Drawing; namespace TerminalGuiDesigner; diff --git a/src/SourceCodeFile.cs b/src/SourceCodeFile.cs index a8797329..ea1d301a 100644 --- a/src/SourceCodeFile.cs +++ b/src/SourceCodeFile.cs @@ -57,6 +57,11 @@ public SourceCodeFile(string path) /// public FileInfo DesignerFile { get; } + /// + /// Returns the class name for the file e.g. for /SomeDir/MyClass.cs it will return MyClass + /// + public string ClassName => Path.GetFileNameWithoutExtension(CsFile.Name); + /// /// Returns the .Designer.cs file for the given class file. /// Returns a reference even if that file does not exist. diff --git a/src/StatusBarExtensions.cs b/src/StatusBarExtensions.cs index 89f4bbc0..3a34d90f 100644 --- a/src/StatusBarExtensions.cs +++ b/src/StatusBarExtensions.cs @@ -14,18 +14,19 @@ public static class StatusBarExtensions /// you want to find the clicked (top level menu) for. /// Screen coordinate of the click in X. /// The under the mouse at this position or null (only considers X). - public static StatusItem? ScreenToMenuBarItem(this StatusBar statusBar, int screenX) + public static Shortcut? ScreenToMenuBarItem(this StatusBar statusBar, int screenX) { // These might be changed in Terminal.Gui library const int initialWhitespace = 1; const int afterEachItemWhitespace = 3; /* currently a space then a '|' then another space*/ - if (statusBar.Items.Length == 0) + + if (statusBar.CountShortcuts() == 0) { return null; } - var clientPoint = statusBar.ScreenToView(screenX, 0); + var clientPoint = statusBar.ScreenToContent(new Point(screenX, 0)); // if click is not in our client area if (clientPoint.X < initialWhitespace) @@ -35,12 +36,12 @@ public static class StatusBarExtensions // Calculate the x display positions of each menu int distance = initialWhitespace; - Dictionary xLocations = new(); + Dictionary xLocations = new(); - foreach (var si in statusBar.Items) + foreach (var si in statusBar.Subviews.OfType()) { xLocations.Add(distance, si); - distance += si.Title.ConsoleWidth + afterEachItemWhitespace; + distance += si.Title.GetColumns() + afterEachItemWhitespace; } // anything after this is not a click on a menu @@ -56,4 +57,32 @@ public static class StatusBarExtensions // Return the last menu item that begins rendering before this X point return xLocations.Last(m => m.Key <= clientPoint.X).Value; } + + /// + /// Return a count of the number of in the . + /// + /// + /// + public static int CountShortcuts(this StatusBar bar) + { + return bar.Subviews.OfType().Count(); + } + + public static Shortcut[] GetShortcuts(this StatusBar bar) + { + return bar.Subviews.OfType().ToArray(); + } + + public static void SetShortcuts(this StatusBar bar, Shortcut[] shortcuts) + { + foreach(var old in bar.GetShortcuts()) + { + bar.Remove(old); + } + + foreach (var shortcut in shortcuts) + { + bar.Add(shortcut); + } + } } \ No newline at end of file diff --git a/src/StringExtensions.cs b/src/StringExtensions.cs index 123d20b0..d6c8477b 100644 --- a/src/StringExtensions.cs +++ b/src/StringExtensions.cs @@ -1,5 +1,6 @@ using System.CodeDom; using System.Text.RegularExpressions; +using Terminal.Gui; namespace TerminalGuiDesigner; diff --git a/src/TTypes.cs b/src/TTypes.cs new file mode 100644 index 00000000..54891b8f --- /dev/null +++ b/src/TTypes.cs @@ -0,0 +1,57 @@ +using System.CodeDom; +using Terminal.Gui; +using TerminalGuiDesigner.ToCode; + +namespace TerminalGuiDesigner +{ + /// + /// Provides knowledge about how to handle different T types for generic + /// views e.g. , + /// + public static class TTypes + { + /// + /// Returns or + /// or similar that represents . + /// + /// + /// + /// + /// + public static CodeExpression ToCode(CodeDomArgs args, Design design, object? value) + { + if(value == null || value is string || value.GetType().IsValueType) + { + return value.ToCodePrimitiveExpression(); + } + + if(value is FileSystemInfo fsi) + { + return new CodeObjectCreateExpression(value.GetType(), fsi.ToString().ToCodePrimitiveExpression()); + } + + throw new NotSupportedException("Value Type ToCode not known" + value.GetType()); + } + + /// + /// Returns all Types which can be used with generic view of the given . + /// + /// A generic view type e.g. (Slider<>) + /// + public static IEnumerable GetSupportedTTypesForGenericViewOfType(Type viewType) + { + if (viewType == typeof(Slider<>)) + { + return new[] { typeof(int), typeof(string), typeof(int), typeof(double), typeof(bool) }; + } + + if (viewType == typeof(TreeView<>)) + { + return new[] { typeof(object), typeof(FileSystemInfo) }; + } + + throw new NotSupportedException($"Generic View {viewType} is not yet supported"); + } + + } +} diff --git a/src/TabViewExtensions.cs b/src/TabViewExtensions.cs index 6d12d3c4..385ea14c 100644 --- a/src/TabViewExtensions.cs +++ b/src/TabViewExtensions.cs @@ -49,7 +49,7 @@ public static void InsertTab(this TabView tv, int atIndex, Tab tab) /// /// The view whose tabs should be reordered. /// The new order to enforce. - public static void ReOrderTabs(this TabView tabView, TabView.Tab[] newOrder) + public static void ReOrderTabs(this TabView tabView, Tab[] newOrder) { var selectedBefore = tabView.SelectedTab; @@ -67,7 +67,7 @@ public static void ReOrderTabs(this TabView tabView, TabView.Tab[] newOrder) } /// - /// Creates a new with a that fills + /// Creates a new with a that fills /// all available space. Tab will have the name . /// /// to add the new tab to. @@ -75,7 +75,11 @@ public static void ReOrderTabs(this TabView tabView, TabView.Tab[] newOrder) /// The tab added. public static Tab AddEmptyTab(this TabView tabView, string named) { - var tab = new TabView.Tab(named, new View { Width = Dim.Fill(), Height = Dim.Fill() }); + var tab = new Tab() + { + DisplayText = named, + View = new View { Width = Dim.Fill(), Height = Dim.Fill() } + }; tabView.AddTab(tab, false); return tab; } diff --git a/src/TableViewExtensions.cs b/src/TableViewExtensions.cs index f31b4eba..ed46a8a6 100644 --- a/src/TableViewExtensions.cs +++ b/src/TableViewExtensions.cs @@ -8,6 +8,17 @@ namespace TerminalGuiDesigner; /// public static class TableViewExtensions { + /// + /// Returns the as a + /// by hard casting. This will fail if source is e.g. . + /// + /// TableView to get the underlying table from. + /// Underlying data table wrapped by . + public static DataTable GetDataTable(this TableView tv) + { + return ((DataTableSource)tv.Table).DataTable; + } + /// /// Reorders all columns to match the element order /// they appear in. @@ -16,7 +27,7 @@ public static class TableViewExtensions /// The new order for all columns in . public static void ReOrderColumns(this TableView tv, DataColumn[] newOrder) { - var dt = tv.Table; + var dt = tv.GetDataTable(); foreach (DataColumn toRemove in dt.Columns.Cast().Except(newOrder).ToArray()) { diff --git a/src/TerminalGuiDesigner.cd b/src/TerminalGuiDesigner.cd index a897ad01..cfc14891 100644 --- a/src/TerminalGuiDesigner.cd +++ b/src/TerminalGuiDesigner.cd @@ -24,7 +24,7 @@ - AhABQBACACKBADAHCAAAAEIQAECJhAAgARAABAAAgAg= + AxABQBACAiKBADAnCAAAAEIQAECJhAAgARAABAAAgAg= Design.cs @@ -39,7 +39,7 @@ - AAAAAAAAAAAABAAAAAAAAIhBABAAAAAAAAAAAAACAAA= + AAAAAAAAAAAABAAAAAAAAIhBABABAAAAAAAAAAACAAA= Operations\AddViewOperation.cs @@ -84,14 +84,14 @@ - IQRiB4gQACICNCQgCABAAAIBigMAQAgBEAFAIQAACAI= + IQRiB4gQACICJCQgKABAAAIBigMAQAgBAAEAIQAACAo= UI\Editor.cs - AAAAEAAAKAABChAEAAAAAAAAAABCAEABAAAAAAAAAAA= + AAAAEAAAKAABChAFAAAAAAEAAABCAEABAAAAAAAAAAA= ToCode\Property.cs @@ -112,7 +112,7 @@ - AAAAAAAAAgAAABAAAAIAAAAAAAAAAAQAEAACAAAAAAA= + AAAAAAAAAgAAABAAAAIEAAAAAAAAAAQAEAACAAAAAAA= ToCode\CodeDomArgs.cs @@ -192,10 +192,62 @@ Operations\OperationFactory.cs + + + + AAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAA= + TTypes.cs + + + + + + + + + + + + + + + + + + + BgBAEAAAIAAAAgIEAAAAAAAAAABAAGAAAAAAAAAAAAA= + ToCode\TreeObjectsProperty.cs + + + + + + + + + + AAAAgAAAACQgAAEEgAggAAQDACBQBABAAIBAEAAAgAQ= + UI\Windows\ArrayEditor.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAABAAAQAAAAAAAAAAAAAA= + TypeExtensions.cs + + + + + + AAAACAEAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAI= + UI\ValueFactory.cs + + - AAAAAAAAAAAABAAAAAAgABABCAAAAAAAACAAAAAACAA= + AAAAAAAAAQAABAAAAAAgABABCAAAAAAAACAAAAAACAA= Operations\IOperation.cs diff --git a/src/TerminalGuiDesigner.csproj b/src/TerminalGuiDesigner.csproj index 4b38cb80..75db0b25 100644 --- a/src/TerminalGuiDesigner.csproj +++ b/src/TerminalGuiDesigner.csproj @@ -1,130 +1,135 @@ - + - + + True + \ + + + True + \ + - - - True - \ - - - True - \ - - - - Exe - true - true - TerminalGuiDesigner - ./nupkg - net6.0 - enable - TerminalGuiDesigner - 1.0.24 - Thomas Nind - enable - MIT - Command line visual designer tool for creating Terminal.Gui C# applications. Create and edit Termianl.Gui views as easily as you could with the Windows Forms Designer. All generated code is contained in a seperate file (e.g. MyControl.Designer.cs). Run the tool by calling directly from the command line 'TerminalGuiDesigner' - Thomas Nind - TerminalGuiDesigner does for Terminal.Gui what the Windows Forms Designer does for WinForms - https://github.com/tznind/TerminalGuiDesigner/ - true - csharp, terminal, c#, gui, toolkit, designer, console - logo.png - README.md - - v1.0.24 - * Update Terminal.Gui dependency to 1.10.1 - v1.0.23 - * Fix bug changing list items - v1.0.22 - * Allow adding `Window` as a content view (handy when designing a Toplevel) - v1.0.21 - * Add Default (Blue/Yellow) ColorScheme - * Take `Toplevel` and `View` root Types out of Experimental mode - * Update Terminal.Gui dependency to 1.9.0 - v1.0.20 - * Ability to copy and paste containers - * StatusBar support (add, remove, rename etc) - * Fix right click on TableView headers - * Improve context menu layout for menus, status bars etc - * Improved private field naming conventions - * RadioGroup can now be set to horizontal - v1.0.19 - * Added `-e` experimental mode flag that lets you create new Toplevel and View classes - * Added dotted border around Views that do not have any visible boundary (e.g. `View`, `TabView` with ShowBorder off) - * Visual improvements in dialog boxes - * Fixed several issues around dragging and dropping (especially into containers) - * Fixed issue with color scheme designer not showing color swatches - * Fixed issue with sub-containers inside TabView not recursively saving all contents - v1.0.18 - * Fixed Checkbox ticking/unticking itself when typing 'space' (e.g. editing its name) - * Fixed issues where pasted/new items sometimes did not receive focus - * Fixed select all not working - v1.0.17 - * Added prompt if there are unsaved changes when closing - * Fixed bug where code generated for PosRelative could not compile due to statement sequence order - * Updated to use Terminal.Gui 1.7.1 package - v1.0.16 - * Support for designing ColorSchemes - * Ability to update the same field on multiple selected Views at once (e.g. set all width to 10) - * Selection color (defaults to green) is now applied to single selections (e.g. tab/mouse selection). Previously this scheme was only used for multi selection - * Lower right status text now shows when there is an ongoing multi selection (e.g. 'Selected: 3 objects') - * Fixed new FrameView instances being created with Text instead of Title set - * Added DrawMarginFrame as a designable property on Window allowing for removing Border from root view if desired - * Multi Copy/paste now preserves/maps PosRelative (e.g. view1.X = Pos.Right(otherView)) when all referenced views are in the copy/paste collection - v1.0.15 - * Fixed bug generating code for Pos/Dim values that had decimal places (e.g. Percent(60)) - v1.0.14 - * Improved MenuBar undo and fixed stability issues - * Deleting the last item on a MenuBar now removes the MenuBar too - * Added multi copy/paste (drag selection box and copy/paste). Still restricted to non container views (i.e. not TabView etc) - v1.0.13 - * Changing LineView Orientation now properly flips rune and Width/Height - * Fixed mouse drag moving and resizing container views (e.g. TabView) - * Fixed bug where you were able to copy/paste the root view - v1.0.12 - * Fixed bug multi selections including tab views - v1.0.11 - * Multi select support for delete and keyboard move - * Prevent Copy/Paste on root view - * Prevent changing Height on Buttons - * Added nuget icon - v1.0.10 - * Support for setting shortcuts in menus (defaults to Ctrl+T) - * Support for renaming menu items (defaults to Ctrl+R) - * Delete added to view context menu - * Blank values in Pos/Dim editor (e.g. Margin size) are now treated as 0 - v1.0.9 - * Support for adding menu separators by typing '---' - * Prevent deleting when another view has a RelativeTo specified on it - * Warn when overwritting a file when creating a new View - * Replaced generic code comments in files generated to indicate the tool and version used - * Added copy and paste of single views - * Support for dragging a control into a seperate container (e.g. into a tab view) - * Prevent illegal field `(Name)` values being entered - * Fixed bad code being generated if multiple tables/tab views had columns/tabs with the same name - v1.0.8 - * Fixed bug free typing new titles for checkboxes - * Fixed bug free typing new titles dropping last character typed - * Added keybinding (defaults to Enter) for opening context menu - * Changed context menu title from 'Properties' to the fieldname of the control being edited - v1.0.7 - * Support for editing MenuBars - v1.0.6 - * Fixed bug adding Views to newly created TabViews - * Fixed undo for operations performed via context menus - * Added Try/Catch to context menu operations - * Added support for TreeView - * Changed TableView example data to be all nulls - v1.0.5 - * Right click context menu support - * Increased mouse resizing click hit box - * Added progress indicator for creating new Views - * Fixed mouse dragging/resizing of views in subviews (e.g. TabViews) - - + + net8.0 + $(DefineConstants) + 12 + Exe + true + true + TerminalGuiDesigner + ./nupkg + enable + TerminalGuiDesigner + 2.0.0-alpha.2189 + Thomas Nind + enable + MIT + Command line visual designer tool for creating Terminal.Gui C# applications. Create and edit Termianl.Gui views as easily as you could with the Windows Forms Designer. All generated code is contained in a separate file (e.g. MyControl.Designer.cs). Run the tool by calling directly from the command line 'TerminalGuiDesigner' + Thomas Nind + TerminalGuiDesigner does for Terminal.Gui what the Windows Forms Designer does for WinForms + https://github.com/tznind/TerminalGuiDesigner/ + true + csharp, terminal, c#, gui, toolkit, designer, console + logo.png + README.md + + 2.0.0-alpha.2189 + * Support for targetting v2 pre-release Terminal.Gui packages. Note that v2 is not backwards compatible with v1. + v1.1.0-rc1 + * Prototype dual targeting dotnet 8 and 7 + v1.0.25 + * Upgrade dependencies, stability improvements + v1.0.24 + * Update Terminal.Gui dependency to 1.10.1 + v1.0.23 + * Fix bug changing list items + v1.0.22 + * Allow adding `Window` as a content view (handy when designing a Toplevel) + v1.0.21 + * Add Default (Blue/Yellow) ColorScheme + * Take `Toplevel` and `View` root Types out of Experimental mode + * Update Terminal.Gui dependency to 1.9.0 + v1.0.20 + * Ability to copy and paste containers + * StatusBar support (add, remove, rename etc) + * Fix right click on TableView headers + * Improve context menu layout for menus, status bars etc + * Improved private field naming conventions + * RadioGroup can now be set to horizontal + v1.0.19 + * Added `-e` experimental mode flag that lets you create new Toplevel and View classes + * Added dotted border around Views that do not have any visible boundary (e.g. `View`, `TabView` with ShowBorder off) + * Visual improvements in dialog boxes + * Fixed several issues around dragging and dropping (especially into containers) + * Fixed issue with color scheme designer not showing color swatches + * Fixed issue with sub-containers inside TabView not recursively saving all contents + v1.0.18 + * Fixed Checkbox ticking/unticking itself when typing 'space' (e.g. editing its name) + * Fixed issues where pasted/new items sometimes did not receive focus + * Fixed select all not working + v1.0.17 + * Added prompt if there are unsaved changes when closing + * Fixed bug where code generated for PosRelative could not compile due to statement sequence order + * Updated to use Terminal.Gui 1.7.1 package + v1.0.16 + * Support for designing ColorSchemes + * Ability to update the same field on multiple selected Views at once (e.g. set all width to 10) + * Selection color (defaults to green) is now applied to single selections (e.g. tab/mouse selection). Previously this scheme was only used for multi selection + * Lower right status text now shows when there is an ongoing multi selection (e.g. 'Selected: 3 objects') + * Fixed new FrameView instances being created with Text instead of Title set + * Added DrawMarginFrame as a designable property on Window allowing for removing Border from root view if desired + * Multi Copy/paste now preserves/maps PosRelative (e.g. view1.X = Pos.Right(otherView)) when all referenced views are in the copy/paste collection + v1.0.15 + * Fixed bug generating code for Pos/Dim values that had decimal places (e.g. Percent(60)) + v1.0.14 + * Improved MenuBar undo and fixed stability issues + * Deleting the last item on a MenuBar now removes the MenuBar too + * Added multi copy/paste (drag selection box and copy/paste). Still restricted to non container views (i.e. not TabView etc) + v1.0.13 + * Changing LineView Orientation now properly flips rune and Width/Height + * Fixed mouse drag moving and resizing container views (e.g. TabView) + * Fixed bug where you were able to copy/paste the root view + v1.0.12 + * Fixed bug multi selections including tab views + v1.0.11 + * Multi select support for delete and keyboard move + * Prevent Copy/Paste on root view + * Prevent changing Height on Buttons + * Added nuget icon + v1.0.10 + * Support for setting shortcuts in menus (defaults to Ctrl+T) + * Support for renaming menu items (defaults to Ctrl+R) + * Delete added to view context menu + * Blank values in Pos/Dim editor (e.g. Margin size) are now treated as 0 + v1.0.9 + * Support for adding menu separators by typing '---' + * Prevent deleting when another view has a RelativeTo specified on it + * Warn when overwritting a file when creating a new View + * Replaced generic code comments in files generated to indicate the tool and version used + * Added copy and paste of single views + * Support for dragging a control into a seperate container (e.g. into a tab view) + * Prevent illegal field `(Name)` values being entered + * Fixed bad code being generated if multiple tables/tab views had columns/tabs with the same name + v1.0.8 + * Fixed bug free typing new titles for checkboxes + * Fixed bug free typing new titles dropping last character typed + * Added keybinding (defaults to Enter) for opening context menu + * Changed context menu title from 'Properties' to the fieldname of the control being edited + v1.0.7 + * Support for editing MenuBars + v1.0.6 + * Fixed bug adding Views to newly created TabViews + * Fixed undo for operations performed via context menus + * Added Try/Catch to context menu operations + * Added support for TreeView + * Changed TableView example data to be all nulls + v1.0.5 + * Right click context menu support + * Increased mouse resizing click hit box + * Added progress indicator for creating new Views + * Fixed mouse dragging/resizing of views in subviews (e.g. TabViews) + + PreserveNewest @@ -133,18 +138,29 @@ PreserveNewest - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/TerminalGuiDesigner.sln b/src/TerminalGuiDesigner.sln index c42d58c7..a6ea7300 100644 --- a/src/TerminalGuiDesigner.sln +++ b/src/TerminalGuiDesigner.sln @@ -7,7 +7,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TerminalGuiDesigner", "Term EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C2EC393B-BA30-48A9-9C48-955BE931C8A4}" ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig + ..\.editorconfig = ..\.editorconfig ..\.github\workflows\build.yml = ..\.github\workflows\build.yml ..\tests\coverlet.runsettings = ..\tests\coverlet.runsettings .github\dependabot.yml = .github\dependabot.yml diff --git a/src/TerminalGuiDesigner.sln.DotSettings b/src/TerminalGuiDesigner.sln.DotSettings new file mode 100644 index 00000000..98557a3a --- /dev/null +++ b/src/TerminalGuiDesigner.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/ToCode/CodeDomArgs.cs b/src/ToCode/CodeDomArgs.cs index 1e9c48ab..ca5ac8e0 100644 --- a/src/ToCode/CodeDomArgs.cs +++ b/src/ToCode/CodeDomArgs.cs @@ -69,6 +69,7 @@ public CodeDomArgs(CodeTypeDeclaration rootClass, CodeMemberMethod initMethod) /// so should have a capital letter at start. for private members /// which should start with a lower case letter. /// . + /// TODO: Consider prefixing @ for reserved keywords, so casing rules can be preserved public static string MakeValidFieldName(string? name, bool isPublic = false) { name = string.IsNullOrWhiteSpace(name) ? "blank" : name; @@ -112,6 +113,11 @@ public static string MakeValidFieldName(string? name, bool isPublic = false) return name; } + internal static bool IsValidIdentifier(string name) + { + return cSharpCodeProvider.IsValidIdentifier(name); + } + /// /// Returns a unique field name based on the passed value. /// Removes non word characters and applies a numerical diff --git a/src/ToCode/ColorSchemeToCode.cs b/src/ToCode/ColorSchemeToCode.cs index 89b3e4f2..67912e76 100644 --- a/src/ToCode/ColorSchemeToCode.cs +++ b/src/ToCode/ColorSchemeToCode.cs @@ -33,13 +33,20 @@ public void ToCode(CodeDomArgs args) { this.AddFieldToClass(args, typeof(ColorScheme), this.scheme.Name); - this.AddConstructorCall(args, $"this.{this.scheme.Name}", typeof(ColorScheme)); + this.AddConstructorCall(args, $"this.{this.scheme.Name}", typeof(ColorScheme), + GetColorCode(this.scheme.Scheme.Normal), + GetColorCode(this.scheme.Scheme.Focus), + GetColorCode(this.scheme.Scheme.HotNormal), + GetColorCode(this.scheme.Scheme.Disabled), + GetColorCode(this.scheme.Scheme.HotFocus)); + } - this.AddColorSchemeField(args, this.scheme.Scheme.Normal, nameof(ColorScheme.Normal)); - this.AddColorSchemeField(args, this.scheme.Scheme.HotNormal, nameof(ColorScheme.HotNormal)); - this.AddColorSchemeField(args, this.scheme.Scheme.Focus, nameof(ColorScheme.Focus)); - this.AddColorSchemeField(args, this.scheme.Scheme.HotFocus, nameof(ColorScheme.HotFocus)); - this.AddColorSchemeField(args, this.scheme.Scheme.Disabled, nameof(ColorScheme.Disabled)); + private CodeObjectCreateExpression GetColorCode(Attribute attr) + { + return new CodeObjectCreateExpression( + typeof(Attribute), + new CodePrimitiveExpression(attr.Foreground.Argb), + new CodePrimitiveExpression(attr.Background.Argb)); } private void AddColorSchemeField(CodeDomArgs args, Attribute color, string colorSchemeSubfield) diff --git a/src/ToCode/DataTableToCode.cs b/src/ToCode/DataTableToCode.cs index c73214f2..9d5917c6 100644 --- a/src/ToCode/DataTableToCode.cs +++ b/src/ToCode/DataTableToCode.cs @@ -26,7 +26,7 @@ public DataTableToCode(Design design) throw new ArgumentException(nameof(design), $"{nameof(DataTableToCode)} can only be used with {nameof(TerminalGuiDesigner.Design)} that wrap {nameof(TableView)}"); } - this.table = tv.Table; + this.table = tv.GetDataTable(); } /// @@ -74,8 +74,10 @@ private void AddSetTableViewTableProperty(CodeDomArgs args, string tableFieldNam var setLhs = new CodeFieldReferenceExpression(); setLhs.FieldName = $"this.{this.design.FieldName}.Table"; - var setRhs = new CodeFieldReferenceExpression(); - setRhs.FieldName = $"{tableFieldName}"; + var arg0 = new CodeFieldReferenceExpression(); + arg0.FieldName = $"{tableFieldName}"; + + var setRhs = new CodeObjectCreateExpression(typeof(DataTableSource),arg0); var assignStatement = new CodeAssignStatement(); assignStatement.Left = setLhs; diff --git a/src/ToCode/EnumToCode.cs b/src/ToCode/EnumToCode.cs new file mode 100644 index 00000000..0e62dad2 --- /dev/null +++ b/src/ToCode/EnumToCode.cs @@ -0,0 +1,56 @@ +using System.CodeDom; + +namespace TerminalGuiDesigner.ToCode; + +public class EnumToCode : ToCodeBase +{ + private readonly Enum value; + private readonly Type enumType; + + + public EnumToCode(Enum value) + { + this.value = value; + this.enumType = value.GetType(); + } + + public CodeExpression ToCode() + { + var isFlags = enumType.IsDefined(typeof(FlagsAttribute), false); + + if (isFlags) + { + return FlagsToExpression(); + } + + return new CodeFieldReferenceExpression( + new CodeTypeReferenceExpression(enumType), + value.ToString()); + } + + private CodeExpression FlagsToExpression() + { + // Split the string representation of the Flags enum into individual values + var flagValues = value.ToString().Split(new[] { ", " }, StringSplitOptions.None); + + // Start by creating the first flag expression + CodeExpression expression = new CodeFieldReferenceExpression( + new CodeTypeReferenceExpression(enumType), + flagValues[0]); + + // Iterate through the remaining flags and combine them using bitwise OR + for (int i = 1; i < flagValues.Length; i++) + { + var nextFlag = new CodeFieldReferenceExpression( + new CodeTypeReferenceExpression(enumType), + flagValues[i]); + + expression = new CodeBinaryOperatorExpression( + expression, + CodeBinaryOperatorType.BitwiseOr, + nextFlag); + } + + return expression; + } +} \ No newline at end of file diff --git a/src/ToCode/ITreeObjectsProperty.cs b/src/ToCode/ITreeObjectsProperty.cs new file mode 100644 index 00000000..114eeb8f --- /dev/null +++ b/src/ToCode/ITreeObjectsProperty.cs @@ -0,0 +1,20 @@ +namespace TerminalGuiDesigner.ToCode; + +/// +/// Interface for generic class +/// +public interface ITreeObjectsProperty +{ + /// + /// Returns True if the T type the property was constructed with is well + /// supported by the designer (i.e. user can pick objects for their tree). + /// + /// + bool IsSupported(); + + /// + /// Returns true if the collection currently held on the property is empty + /// + /// + public bool IsEmpty(); +} diff --git a/src/ToCode/InstanceOfProperty.cs b/src/ToCode/InstanceOfProperty.cs new file mode 100644 index 00000000..f161f666 --- /dev/null +++ b/src/ToCode/InstanceOfProperty.cs @@ -0,0 +1,35 @@ +using System.CodeDom; +using System.Linq.Expressions; +using System.Reflection; +using Terminal.Gui; + +namespace TerminalGuiDesigner.ToCode; + +/// +/// which requires setting to one of several classes derrived from +/// a shared base. Classes must have a blank constructor e.g.. +/// +public class InstanceOfProperty : Property +{ + public Type MustBeDerrivedFrom { get; } + + public InstanceOfProperty(Design design, PropertyInfo property) + : base(design, property) + { + this.MustBeDerrivedFrom = property.PropertyType + ?? throw new Exception("Unable to determine property type"); + } + + public override CodeExpression GetRhs() + { + var instance = this.GetValue(); + if (instance != null) + { + return new CodeObjectCreateExpression(instance.GetType()); + } + else + { + return new CodeSnippetExpression("null"); + } + } +} diff --git a/src/ToCode/MenuBarItemsToCode.cs b/src/ToCode/MenuBarItemsToCode.cs index 042b59a6..818114d7 100644 --- a/src/ToCode/MenuBarItemsToCode.cs +++ b/src/ToCode/MenuBarItemsToCode.cs @@ -98,14 +98,14 @@ private void ToCode(CodeDomArgs args, MenuBarItem child, out string fieldName) this.AddPropertyAssignment(args, $"this.{subFieldName}.{nameof(MenuItem.Title)}", sub.Title); this.AddPropertyAssignment(args, $"this.{subFieldName}.{nameof(MenuItem.Data)}", subFieldName); - if (sub.Shortcut != Key.Null) + if (sub.ShortcutKey != KeyCode.Null) { this.AddPropertyAssignment( args, - $"this.{subFieldName}.{nameof(MenuItem.Shortcut)}", + $"this.{subFieldName}.{nameof(MenuItem.ShortcutKey)}", new CodeCastExpression( - new CodeTypeReference(typeof(Key)), - new CodePrimitiveExpression((uint)sub.Shortcut))); + new CodeTypeReference(typeof(KeyCode)), + new CodePrimitiveExpression((uint)sub.ShortcutKey))); } children.Add(subFieldName); diff --git a/src/ToCode/Property.cs b/src/ToCode/Property.cs index 0c1e3c3e..b46c8bdd 100644 --- a/src/ToCode/Property.cs +++ b/src/ToCode/Property.cs @@ -1,11 +1,12 @@ using System.CodeDom; +using System.Collections; +using System.Collections.ObjectModel; using System.Reflection; -using NStack; +using System.Text; using Terminal.Gui; -using Terminal.Gui.Graphs; using Terminal.Gui.TextValidateProviders; using TerminalGuiDesigner; -using static Terminal.Gui.TableView; +using YamlDotNet.Core.Tokens; using Attribute = Terminal.Gui.Attribute; namespace TerminalGuiDesigner.ToCode; @@ -95,59 +96,7 @@ public Property(Design design, PropertyInfo property, string subProperty, object /// Thrown if invalid values are passed. public virtual void SetValue(object? value) { - // handle type conversions - if (this.PropertyInfo.PropertyType == typeof(Rune)) - { - if (value is char ch) - { - value = new Rune(ch); - } - } - - if (this.PropertyInfo.PropertyType == typeof(Dim)) - { - if (value is int i) - { - value = Dim.Sized(i); - } - } - - if (this.PropertyInfo.PropertyType == typeof(ustring)) - { - if (value is string s) - { - value = ustring.Make(s); - - // TODO: This seems like something AutoSize should do automatically - // if renaming a button update its size to match - if (this.Design.View is Button b && this.PropertyInfo.Name.Equals("Text") && b.Width.IsAbsolute()) - { - b.Width = s.Length + (b.IsDefault ? 6 : 4); - } - } - - // some views don't like null and only work with "" e.g. TextView - // see https://github.com/gui-cs/TerminalGuiDesigner/issues/91 - if (value == null) - { - value = ustring.Make(string.Empty); - } - } - - if (this.PropertyInfo.PropertyType == typeof(IListDataSource)) - { - if (value != null && value is Array a) - { - // accept arrays as valid input values - // for setting an IListDataSource. Just - // convert them to ListWrappers - value = new ListWrapper(a.ToList()); - } - } - - // TODO: This hack gets around an ArgumentException that gets thrown when - // switching from Computed to Absolute values of Dim/Pos - this.Design.View.IsInitialized = false; + value = AdjustValueBeingSet(value); // if a LineView and changing Orientation then also flip // the Height/Width and set appropriate new rune @@ -159,13 +108,13 @@ public virtual void SetValue(object? value) case Orientation.Horizontal: v.Width = v.Height; v.Height = 1; - v.LineRune = Application.Driver.HLine; + v.LineRune = ConfigurationManager.Glyphs.HLine; break; case Orientation.Vertical: v.Height = v.Width; v.Width = 1; - v.LineRune = Application.Driver.VLine; + v.LineRune = ConfigurationManager.Glyphs.VLine; break; default: throw new ArgumentException($"Unknown Orientation {newOrientation}"); @@ -175,8 +124,6 @@ public virtual void SetValue(object? value) this.PropertyInfo.SetValue(this.DeclaringObject, value); this.CallRefreshMethodsIfAny(); - - this.Design.View.IsInitialized = true; } /// @@ -211,6 +158,31 @@ public virtual CodeExpression GetRhs() return new CodeSnippetExpression("null"); } + if(val is Rune rune) + { + char[] chars = new char[rune.Utf16SequenceLength]; + rune.EncodeToUtf16(chars); + + if(chars.Length == 1) + { + return new CodeObjectCreateExpression( + typeof(Rune), + new CodePrimitiveExpression(chars[0])); + } + else if (chars.Length == 2) + { + // User is setting to an emoticon or something + return new CodeObjectCreateExpression( + typeof(Rune), + new CodePrimitiveExpression(chars[0]), + new CodePrimitiveExpression(chars[1])); + } + else + { + throw new Exception($"Unexpected unicode character size. Rune was {rune}"); + } + } + if (val is Attribute attribute) { return new CodeSnippetExpression(attribute.ToCode()); @@ -241,33 +213,6 @@ public virtual CodeExpression GetRhs() regv.Pattern.ToCodePrimitiveExpression()); } - if (val is ListWrapper w) - { - /* Create an Expression like: - * new ListWrapper(new string[]{"bob","frank"}) - */ - - var a = new CodeArrayCreateExpression(); - Type? listType = null; - - foreach (var v in w.ToList()) - { - if (v != null && listType == null) - { - listType = v.GetType(); - } - - CodeExpression element = v == null ? new CodeDefaultValueExpression() : new CodePrimitiveExpression(v); - - a.Initializers.Add(element); - } - - a.CreateType = new CodeTypeReference(listType ?? typeof(string)); - var ctor = new CodeObjectCreateExpression(typeof(ListWrapper), a); - - return ctor; - } - if (val is Pos p) { // TODO: Get EVERYONE! not just siblings @@ -277,9 +222,8 @@ public virtual CodeExpression GetRhs() if (val is Enum e) { - return new CodeFieldReferenceExpression( - new CodeTypeReferenceExpression(e.GetType()), - e.ToString()); + var toCode = new EnumToCode(e); + return toCode.ToCode(); } var type = val.GetType(); @@ -294,9 +238,95 @@ public virtual CodeExpression GetRhs() values.Select(v => v.ToCodePrimitiveExpression()).ToArray()); } + if (val is IList valList) + { + var elementType = type.GetElementTypeEx() + ?? throw new Exception($"Type {type} was an IList but {nameof(Type.GetElementType)} returned null"); + + return new CodeObjectCreateExpression( + new CodeTypeReference(val.GetType()), + new CodeArrayCreateExpression( + elementType, + valList.Cast().Select(this.ValueFactory).ToArray()) + ); + } + + if (IsListWrapper(type)) + { + return CreateListWrapperExpression((IListDataSource)val); + } + return val.ToCodePrimitiveExpression(); } + private bool IsListWrapper(Type t) + { + return t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ListWrapper<>); + } + + private CodeObjectCreateExpression CreateListWrapperExpression(IListDataSource val) + { + var list = val.ToList(); + + var listWrapperType = val.GetType(); + + // Get the generic type argument (T) + var elementType = listWrapperType.GetGenericArguments()[0]; + + // Create an Expression like: + // new ListWrapper(new ObservableCollection(new string[]{"bob","frank"})) + + + var arrayCreateExpression = new CodeArrayCreateExpression(new CodeTypeReference(elementType)); + + if (list != null) + { + foreach (var v in list) + { + CodeExpression element = v == null + ? new CodeDefaultValueExpression(new CodeTypeReference(elementType)) + : new CodePrimitiveExpression(v); + arrayCreateExpression.Initializers.Add(element); + } + } + + // Create an expression for new ObservableCollection(new T[]{ ... }) + var observableCollectionType = typeof(ObservableCollection<>).MakeGenericType(elementType); + var observableCollectionCtor = new CodeObjectCreateExpression(observableCollectionType, arrayCreateExpression); + + + // Create the final expression for new ListWrapper(new ObservableCollection(...)) + var genericListWrapperType = typeof(ListWrapper<>).MakeGenericType(elementType); + var listWrapperCtor = new CodeObjectCreateExpression(genericListWrapperType, observableCollectionCtor); + + return listWrapperCtor; + } + private CodeExpression ValueFactory(object val) + { + + var type = val.GetType(); + + // TODO: Could move lots of logic in GetRHS into here + if (type.GetGenericTypeDefinition() == typeof(SliderOption<>)) + { + // TODO: this feels very brittle! + var a1 = type.GetProperty(nameof(SliderOption.Legend)).GetValue(val); + var a2 = (Rune)type.GetProperty(nameof(SliderOption.LegendAbbr)).GetValue(val); + var a3 = type.GetProperty(nameof(SliderOption.Data)).GetValue(val); + + + return new CodeObjectCreateExpression( + new CodeTypeReference(val.GetType()), + new CodePrimitiveExpression(a1), + new CodeObjectCreateExpression(typeof(Rune),new CodePrimitiveExpression(a2.ToString()[0])), + new CodePrimitiveExpression(a3)); + } + else + { + throw new NotSupportedException($"Cannot generate code for value '{val}'"); + } + } + /// /// Gets a CodeDOM code block for the left hand side of an assignment operation e.g.: /// this.label1.Text @@ -376,6 +406,56 @@ protected virtual string GetHumanReadableValue() return val.ToString() ?? string.Empty; } + /// + /// Adjust to match the expectations of + /// e.g. convert char to . + /// + /// + /// + protected object? AdjustValueBeingSet(object? value) + { + // handle type conversions + if (this.PropertyInfo.PropertyType == typeof(Rune)) + { + if (value is char ch) + { + value = new Rune(ch); + } + } + + if (this.PropertyInfo.PropertyType == typeof(Dim)) + { + if (value is int i) + { + value = Dim.Absolute(i); + } + } + // Some Terminal.Gui string properties get angry at null but are ok with empty strings + if (this.PropertyInfo.PropertyType == typeof(string)) + { + if (value == null) + { + value = string.Empty; + } + } + + if (this.PropertyInfo.PropertyType == typeof(IListDataSource)) + { + if (value is Array a) + { + + // accept arrays as valid input values + // for setting an IListDataSource. Just + // convert them to ListWrappers + value = a.ToListDataSource(); + } + } + + return value; + } + + + /// /// Calls any methods that update the state of the View /// and refresh it against its style e.g. . @@ -394,4 +474,4 @@ private void CallRefreshMethodsIfAny() this.Design.View.SetNeedsDisplay(); } -} +} \ No newline at end of file diff --git a/src/ToCode/StatusBarItemsToCode.cs b/src/ToCode/StatusBarItemsToCode.cs index 56814a8e..750b3de5 100644 --- a/src/ToCode/StatusBarItemsToCode.cs +++ b/src/ToCode/StatusBarItemsToCode.cs @@ -37,35 +37,40 @@ public void ToCode(CodeDomArgs args) // TODO: Let user name these List items = new(); - foreach (var child in this.statusBar.Items) + var statusBarFieldExpression = new CodeFieldReferenceExpression( + new CodeThisReferenceExpression(), design.FieldName); + + foreach (var child in this.statusBar.GetShortcuts()) { var name = args.GetUniqueFieldName(child.Title.ToString(), false); items.Add(name); - this.AddFieldToClass(args, typeof(StatusItem), name); + this.AddFieldToClass(args, typeof(Shortcut), name); var param1 = new CodeCastExpression( - new CodeTypeReference(typeof(Key)), - new CodePrimitiveExpression((uint)child.Shortcut)); + new CodeTypeReference(typeof(KeyCode)), + new CodePrimitiveExpression((uint)child.Key.KeyCode)); var param2 = child.Title.ToCodePrimitiveExpression(); var param3 = new CodePrimitiveExpression(/*null*/); this.AddConstructorCall( args, $"this.{name}", - typeof(StatusItem), + typeof(Shortcut), param1, param2, param3); - } - this.AddPropertyAssignment( - args, - $"this.{this.design.FieldName}.{nameof(this.statusBar.Items)}", - new CodeArrayCreateExpression( - typeof(StatusItem), - items.Select(c => - new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), c)) - .ToArray())); + var shortcutFieldExpression = new CodeFieldReferenceExpression( + new CodeThisReferenceExpression(), name); + + + // Create this.myBar.Add(sb1); + AddMethodCall(args, statusBarFieldExpression, + nameof(StatusBar.Add),new CodeExpression[] + { + shortcutFieldExpression + }); + } } } diff --git a/src/ToCode/SuppressedProperty.cs b/src/ToCode/SuppressedProperty.cs new file mode 100644 index 00000000..e8e6db74 --- /dev/null +++ b/src/ToCode/SuppressedProperty.cs @@ -0,0 +1,48 @@ +using System.Reflection; +using Terminal.Gui; + +namespace TerminalGuiDesigner.ToCode; + +/// +/// A whose value is stored but does not manifest within +/// the editor because it would make design time operation difficult e.g. . +/// +public class SuppressedProperty : Property +{ + private object? value; + + /// + public SuppressedProperty(Design design, PropertyInfo property, object? designTimeValue) + : base(design, property) + { + this.StoreInitialValueButUse(designTimeValue); + } + + /// + public SuppressedProperty(Design design, PropertyInfo property, string subProperty, object declaringObject, object? designTimeValue) + : base(design, property, subProperty, declaringObject) + { + this.StoreInitialValueButUse(designTimeValue); + } + + /// + public override object? GetValue() + { + return this.value; + } + + /// + public override void SetValue(object? value) + { + this.value = this.AdjustValueBeingSet(value); + } + + private void StoreInitialValueButUse(object? designTimeValue) + { + // Pull the compiled/created value from the view (e.g. Visible=false) + this.value = base.GetValue(); + + // But immediately override with the design time view (e.g. Visible=true) + base.SetValue(designTimeValue); + } +} diff --git a/src/ToCode/TabToCode.cs b/src/ToCode/TabToCode.cs index 0ebba1fb..84016457 100644 --- a/src/ToCode/TabToCode.cs +++ b/src/ToCode/TabToCode.cs @@ -5,7 +5,7 @@ namespace TerminalGuiDesigner.ToCode; /// -/// Handles generating code for a single into .Designer.cs +/// Handles generating code for a single into .Designer.cs /// file (See ). This will then be added to a /// via . /// @@ -42,9 +42,10 @@ public void ToCode(CodeDomArgs args) this.AddConstructorCall( args, tabName, - typeof(Tab), - this.tab.Text.ToCodePrimitiveExpression(), - new CodeSnippetExpression("new View()")); + typeof(Tab)); + + this.AddPropertyAssignment(args, $"{tabName}.{nameof(Tab.DisplayText)}", this.tab.DisplayText.ToCodePrimitiveExpression()); + this.AddPropertyAssignment(args, $"{tabName}.{nameof(Tab.View)}", new CodeSnippetExpression("new View()")); // make the Tab.View Dim.Fill this.AddPropertyAssignment(args, $"{tabName}.View.Width", new CodeSnippetExpression("Dim.Fill()")); @@ -60,7 +61,7 @@ public void ToCode(CodeDomArgs args) private string GetTabFieldName(CodeDomArgs args) { - var tabname = this.tab.Text?.ToString(); + var tabname = this.tab.DisplayText?.ToString(); if (string.IsNullOrWhiteSpace(tabname)) { throw new Exception("Could not generate Tab variable name because its Text was blank or null"); diff --git a/src/ToCode/TreeObjectsProperty.cs b/src/ToCode/TreeObjectsProperty.cs new file mode 100644 index 00000000..41b19235 --- /dev/null +++ b/src/ToCode/TreeObjectsProperty.cs @@ -0,0 +1,154 @@ +using System.CodeDom; +using Terminal.Gui; + +namespace TerminalGuiDesigner.ToCode; + +/// +/// for storing, editing and code gen for TreeView<T>.Options. +/// +/// +public class TreeObjectsProperty : Property, ITreeObjectsProperty where T : class +{ + private List value; + readonly TreeView treeView; + + Dictionary> treeBuilders = new Dictionary>() + { + { typeof(FileSystemInfo),TreeBuilderForFileSystemInfo} + }; + + /// + /// Creates a new instance + /// + /// + /// + public TreeObjectsProperty(Design design) + : base( + design, + typeof(TreeView).GetProperty(nameof(TreeView.Objects)) + ?? throw new MissingFieldException("Expected property was missing from TreeView")) + { + treeView = (TreeView)design.View; + value = new List(treeView.Objects); + } + + /// + public override string GetHumanReadableName() + { + return "Objects"; + } + + /// + public override string ToString() + { + return $"{this.GetHumanReadableName()}:{this.value.Count} objects"; + } + + /// + public override void SetValue(object? value) + { + this.value = (List)(value ?? new List()); + + treeView.ClearObjects(); + treeView.AddObjects(this.value); + + } + + /// + public override object GetValue() + { + return this.value; + } + + /// + /// Adds code to init method to populate TreeView Options and optionally TreeBuilder + /// (Depending on T type). + /// + public override void ToCode(CodeDomArgs args) + { + // Create statement like this + //tree1.AddObjects(new List() { new DirectoryInfo("c:\\") }); + + var call = new CodeMethodInvokeExpression(); + call.Method.TargetObject = new CodeFieldReferenceExpression( + new CodeThisReferenceExpression(), + this.Design.FieldName); + + call.Method.MethodName = nameof(TreeView.AddObjects); + + var newListStatement = + new CodeArrayCreateExpression( + new CodeTypeReference(typeof(T[])), + value.Select(v => TTypes.ToCode(args, Design, v)).ToArray()); + + call.Parameters.Add(newListStatement); + + args.InitMethod.Statements.Add(call); + + // Now also create TreeBuilder if its a known Type we can handle + if(treeBuilders.ContainsKey(typeof(T))) + { + + var setBuilderLhs = new CodeFieldReferenceExpression( + new CodeThisReferenceExpression(), $"{this.Design.FieldName}.{nameof(TreeView.TreeBuilder)}"); + var setBuilderRhs = treeBuilders[typeof(T)](); + + var assignStatement = new CodeAssignStatement + { + Left = setBuilderLhs, + Right = setBuilderRhs + }; + args.InitMethod.Statements.Add(assignStatement); + } + } + + private static CodeExpression TreeBuilderForFileSystemInfo() + { + return new CodeSnippetExpression(""" + + new Terminal.Gui.DelegateTreeBuilder((p) => + { + try + { + return p is System.IO.DirectoryInfo d ? d.GetFileSystemInfos() : System.Linq.Enumerable.Empty(); + } + catch (Exception) + { + return System.Linq.Enumerable.Empty(); + } + }); + """); + } + + /// + /// Not implemented by this class + /// + /// + /// Always thrown + public override string GetLhs() + { + throw new NotSupportedException("This property does its own code gen in ToCode"); + } + + /// + /// Not implemented by this class + /// + /// + /// Always thrown + public override CodeExpression GetRhs() + { + throw new NotSupportedException("This property does its own code gen in ToCode"); + } + + /// + public bool IsEmpty() + { + return !value.Any(); + } + + /// + public bool IsSupported() + { + return treeBuilders.ContainsKey(typeof(T)); + } +} diff --git a/src/ToCode/ViewToCode.cs b/src/ToCode/ViewToCode.cs index e4977d55..961ac05e 100644 --- a/src/ToCode/ViewToCode.cs +++ b/src/ToCode/ViewToCode.cs @@ -79,21 +79,14 @@ public Design GenerateNewView(FileInfo csFilePath, string namespaceName, Type vi var sourceFile = new SourceCodeFile(csFilePath); - var className = Path.GetFileNameWithoutExtension(sourceFile.CsFile.Name); + var className = sourceFile.ClassName; var csharpCode = GetGenerateNewViewCode(className, namespaceName); File.WriteAllText(sourceFile.CsFile.FullName, csharpCode); var prototype = (View)(Activator.CreateInstance(viewType) ?? throw new Exception($"Could not create instance of Type '{viewType}' ('Activator.CreateInstance' returned null)")); - // Unlike Window and Dialog the default constructor on - // View will be a size 0 view. Make it big so it can be - // edited - if (viewType == typeof(View)) - { - prototype.Width = Dim.Fill(); - prototype.Height = Dim.Fill(); - } + FixDimensionsForNewRootView(prototype, viewType); // use the prototype to create a designer cs file var design = new Design(sourceFile, Design.RootDesignName, prototype); @@ -109,6 +102,26 @@ public Design GenerateNewView(FileInfo csFilePath, string namespaceName, Type vi return decompiler.CreateInstance(); } + /// + /// Fixes any problematic default dimensions on specific types e.g. defaults to + /// > which means 1x1 when opened empty in designer. + /// + /// + /// + private void FixDimensionsForNewRootView(View prototype, Type viewType) + { + if (viewType == typeof(View)) + { + prototype.Width = Dim.Fill(); + prototype.Height = Dim.Fill(); + } + if (viewType == typeof(Dialog)) + { + prototype.Width = Dim.Percent(90); + prototype.Height = Dim.Percent(80); + } + } + /// /// Writes a .Designer.cs () to disk based on the /// current state of . @@ -125,6 +138,10 @@ public void GenerateDesignerCs(Design rootDesign, Type viewType) var ns = new CodeNamespace(rosylyn.Namespace); ns.Imports.Add(new CodeNamespaceImport("System")); ns.Imports.Add(new CodeNamespaceImport("Terminal.Gui")); + ns.Imports.Add(new CodeNamespaceImport("System.Collections")); + ns.Imports.Add(new CodeNamespaceImport("System.Collections.Generic")); + ns.Imports.Add(new CodeNamespaceImport("System.Collections.ObjectModel")); + ns.Imports.Add(new CodeNamespaceImport("System.Drawing")); this.AddCustomHeaderForDesignerCsFile(ns); diff --git a/src/TreeViewExtensions.cs b/src/TreeViewExtensions.cs new file mode 100644 index 00000000..f1af4bee --- /dev/null +++ b/src/TreeViewExtensions.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Terminal.Gui; +using TerminalGuiDesigner.ToCode; + +namespace TerminalGuiDesigner +{ + /// + /// Extension methods for + /// + public static class TreeViewExtensions + { + /// + /// For a given TreeView, returns true if there are any Objects defined in it. + /// + /// + /// + /// If is not a TreeView (or TreeView<> implementation) + /// If Terminal.Gui API changes have been made that break reflection + public static bool IsEmpty(this View tv) + { + if (tv is TreeView basicTreeView) + { + return !basicTreeView.Objects.Any(); + } + + if (!tv.GetType().IsGenericType(typeof(TreeView<>))) + { + throw new NotSupportedException("Expected Tree View Type to be an implementation of generic class TreeView<>"); + } + + + + var d = tv.Data as Design ?? throw new NotSupportedException("View does not have a Design"); + + var prop = (ITreeObjectsProperty?)d.GetDesignableProperty(nameof(TreeView.Objects)); + + if(prop == null) + { + // Options are not designable for this T type so TreeView must be empty + return true; + } + + return prop.IsEmpty(); + } + } +} diff --git a/src/TypeExtensions.cs b/src/TypeExtensions.cs new file mode 100644 index 00000000..53f0bb40 --- /dev/null +++ b/src/TypeExtensions.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections; + +namespace TerminalGuiDesigner +{ + /// + /// Extensions for the class. + /// + public static class TypeExtensions + { + /// + /// Implementation of that also works for + /// + /// + /// Element type of collection or . + public static Type? GetElementTypeEx(this Type type) + { + var elementType = type.GetElementType(); + + if (elementType != null) + { + return elementType; + } + + if (type.IsAssignableTo(typeof(IList)) && type.IsGenericType) + { + return type.GetGenericArguments().Single(); + } + + if (type.IsGenericType(typeof(IEnumerable<>))) + { + return type.GetGenericArguments().Single(); + } + + + return null; + } + + /// + /// Returns true if is an implementation of a generic parent + /// type . + /// + /// + /// Generic parent e.g. (TreeView<>) + /// + public static bool IsGenericType(this Type type, Type genericParentHypothesis) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == genericParentHypothesis; + } + } +} diff --git a/src/UI/ColorSchemeBlueprint.cs b/src/UI/ColorSchemeBlueprint.cs index 3637d23e..85e0760b 100644 --- a/src/UI/ColorSchemeBlueprint.cs +++ b/src/UI/ColorSchemeBlueprint.cs @@ -1,67 +1,83 @@ -using Terminal.Gui; +using System.Text.Json.Serialization; +using Terminal.Gui; +using YamlDotNet.Core; using YamlDotNet.Serialization; using Attribute = Terminal.Gui.Attribute; namespace TerminalGuiDesigner.UI; /// -/// Serialize-able version of . +/// Serializable version of . /// -public class ColorSchemeBlueprint +[YamlSerializable] +public record ColorSchemeBlueprint( ColorName NormalForeground, ColorName NormalBackground, ColorName HotNormalForeground, ColorName HotNormalBackground, ColorName FocusForeground, ColorName FocusBackground, ColorName HotFocusForeground, ColorName HotFocusBackground, ColorName DisabledForeground, ColorName DisabledBackground ) { /// - /// Gets or Sets the to use for . + /// Gets the to use for . /// - public Color NormalForeground { get; set; } + [JsonConverter(typeof(JsonStringEnumConverter ))] + public ColorName NormalForeground { get; init; } = NormalForeground; /// - /// Gets or Sets the to use for . + /// Gets the to use for . /// - public Color NormalBackground { get; set; } + [JsonConverter(typeof(JsonStringEnumConverter ))] + public ColorName NormalBackground { get; init; } = NormalBackground; /// - /// Gets or Sets the to use for . + /// Gets the to use for . /// - public Color HotNormalForeground { get; set; } + [JsonConverter(typeof(JsonStringEnumConverter ))] + public ColorName HotNormalForeground { get; init; } = HotNormalForeground; /// - /// Gets or Sets the to use for . + /// Gets the to use for . /// - public Color HotNormalBackground { get; set; } + [JsonConverter(typeof(JsonStringEnumConverter ))] + public ColorName HotNormalBackground { get; init; } = HotNormalBackground; /// - /// Gets or Sets the to use for . + /// Gets the to use for . /// - public Color FocusForeground { get; set; } + [JsonConverter(typeof(JsonStringEnumConverter ))] + public ColorName FocusForeground { get; init; } = FocusForeground; /// - /// Gets or Sets the to use for . + /// Gets the to use for . /// - public Color FocusBackground { get; set; } + [JsonConverter(typeof(JsonStringEnumConverter ))] + public ColorName FocusBackground { get; init; } = FocusBackground; /// - /// Gets or Sets the to use for . + /// Gets the to use for . /// - public Color HotFocusForeground { get; set; } + [JsonConverter(typeof(JsonStringEnumConverter ))] + public ColorName HotFocusForeground { get; init; } = HotFocusForeground; /// - /// Gets or Sets the to use for . + /// Gets the to use for . /// - public Color HotFocusBackground { get; set; } + [JsonConverter(typeof(JsonStringEnumConverter ))] + public ColorName HotFocusBackground { get; init; } = HotFocusBackground; /// - /// Gets or Sets the to use for . + /// Gets the to use for . /// - public Color DisabledForeground { get; set; } + [JsonConverter(typeof(JsonStringEnumConverter ))] + [YamlMember( typeof(ColorName), ScalarStyle = ScalarStyle.Plain, DefaultValuesHandling = DefaultValuesHandling.Preserve)] + public ColorName DisabledForeground { get; init; } = DisabledForeground; /// - /// Gets or Sets the to use for . + /// Gets the to use for . /// - public Color DisabledBackground { get; set; } + [JsonConverter(typeof(JsonStringEnumConverter ))] + [YamlMember( typeof(ColorName), ScalarStyle = ScalarStyle.Plain, DefaultValuesHandling = DefaultValuesHandling.Preserve)] + public ColorName DisabledBackground { get; init; } = DisabledBackground; /// /// Gets a new from the blueprint. /// + [JsonIgnore] [YamlIgnore] public ColorScheme Scheme => new ColorScheme { diff --git a/src/UI/Editor.cs b/src/UI/Editor.cs index d71a1889..4f0b63b1 100644 --- a/src/UI/Editor.cs +++ b/src/UI/Editor.cs @@ -1,10 +1,13 @@ -using System.Text; +using System.Collections.ObjectModel; +using System.Reflection; +using System.Text; +using Microsoft.Extensions.Configuration; using Terminal.Gui; using TerminalGuiDesigner.FromCode; using TerminalGuiDesigner.Operations; using TerminalGuiDesigner.ToCode; using TerminalGuiDesigner.UI.Windows; -using YamlDotNet.Serialization; +using Attribute = Terminal.Gui.Attribute; namespace TerminalGuiDesigner.UI; @@ -16,14 +19,14 @@ namespace TerminalGuiDesigner.UI; public class Editor : Toplevel { private readonly KeyMap keyMap; + private readonly KeyboardManager keyboardManager; + private readonly MouseManager mouseManager; private Design? viewBeingEdited; private bool enableDrag = true; private bool enableShowFocused = true; - private bool editting = false; + private bool editing = false; - private KeyboardManager keyboardManager; - private MouseManager mouseManager; private ListView? rootCommandsListView; private bool menuOpen; @@ -38,38 +41,26 @@ public class Editor : Toplevel /// operation at the time of the last save or null if no save or last save /// was before applying any operations. /// - private Guid? lastSavedOperation; + internal Guid? LastSavedOperation; /// /// Initializes a new instance of the class. /// public Editor() { + // Bug: This will have strange inheritance behavior if Editor is inherited from. this.CanFocus = true; - // If there are custom keybindings read those - if (File.Exists("Keys.yaml")) + try { - var d = new Deserializer(); - try - { - this.keyMap = d.Deserialize(File.ReadAllText("Keys.yaml")); + this.keyMap = new ConfigurationBuilder( ).AddYamlFile( "Keys.yaml", true ).Build( ).Get( ) ?? new( ); - if (this.keyMap.SelectionColor != null) - { - SelectionManager.Instance.SelectedScheme = this.keyMap.SelectionColor.Scheme; - } - } - catch (Exception ex) - { - // if there is bad yaml use the defaults - ExceptionViewer.ShowException("Failed to read keybindings", ex); - this.keyMap = new KeyMap(); - } + SelectionManager.Instance.SelectedScheme = this.keyMap.SelectionColor.Scheme; } - else + catch (Exception ex) { - // otherwise use the defaults + // if there is bad yaml use the defaults + ExceptionViewer.ShowException("Failed to read keybindings from configuration file", ex); this.keyMap = new KeyMap(); } @@ -83,13 +74,15 @@ public Editor() /// /// Gets or Sets a value indicating whether that do not have borders /// (e.g. ) should have a dotted line rendered around them so - /// users don't loose track of where they are on a same colored background. + /// users don't lose track of where they are on a same-colored background. /// + // BUG: Thread-safety public static bool ShowBorders { get; set; } = true; /// - /// Gets a value indicating whether true to enable experimental features. + /// Gets a value indicating whether to enable experimental features. /// + // BUG: Thread-safety public static bool Experimental { get; internal set; } /// @@ -115,7 +108,8 @@ public void Run(Options options) if (!string.IsNullOrWhiteSpace(options.ViewType)) { - toCreate = this.GetSupportedRootViews().FirstOrDefault(v => v.Name.Equals(options.ViewType)) ?? toCreate; + // TODO: We should probably use something like IsAssignableTo instead + toCreate = GetSupportedRootViews().FirstOrDefault(v => v.Name.Equals(options.ViewType)) ?? toCreate; } this.New(toLoadOrCreate, toCreate, options.Namespace); @@ -129,25 +123,27 @@ public void Run(Options options) } } - Application.RootKeyEvent += (k) => + Application.KeyDown += (_, k) => { - if (this.editting) + if (this.editing || this.viewBeingEdited == null) { - return false; + return; } try { - return this.HandleKey(k); + if (this.HandleKey(k)) + { + k.Handled = true; + } } - catch (System.Exception ex) + catch (Exception ex) { ExceptionViewer.ShowException("Error processing keystroke", ex); - return false; } }; - Application.RootMouseEvent += (m) => + Application.MouseEvent += (s, m) => { // if another window is showing don't respond to mouse if (!this.IsCurrentTop) @@ -155,7 +151,13 @@ public void Run(Options options) return; } - if (this.editting || !this.enableDrag || this.viewBeingEdited == null) + // If disabling drag we suppress all but right click (button 3) + if (!m.Flags.HasFlag(MouseFlags.Button3Clicked) && !this.enableDrag) + { + return; + } + + if (this.editing || this.viewBeingEdited == null) { return; } @@ -179,7 +181,7 @@ public void Run(Options options) } } } - catch (System.Exception ex) + catch (Exception ex) { ExceptionViewer.ShowException("Error processing mouse", ex); } @@ -190,12 +192,12 @@ public void Run(Options options) } /// - /// Tailors redrawing to add overlays (e.g. showing what is selected etc). + /// Tailors redrawing to add overlays (e.g. showing what is selected etc.). /// /// The view bounds. - public override void Redraw(Rect bounds) + public override void OnDrawContent(Rectangle bounds) { - base.Redraw(bounds); + base.OnDrawContent(bounds); // if we are editing a view if (this.viewBeingEdited != null) @@ -210,13 +212,13 @@ public override void Redraw(Rect bounds) if (toDisplay != null) { // write its name in the lower right - int y = this.Bounds.Height - 1; + int y = this.GetContentSize().Height - 1; int right = bounds.Width - 1; var len = toDisplay.Length; for (int i = 0; i < len; i++) { - this.AddRune(right - len + i, y, toDisplay[i]); + this.AddRune(right - len + i, y, new Rune(toDisplay[i])); } } } @@ -230,7 +232,7 @@ public override void Redraw(Rect bounds) { if (y == 0 || y == box.Height - 1 || x == 0 || x == box.Width - 1) { - this.AddRune(box.X + x, box.Y + y, '.'); + this.AddRune(box.X + x, box.Y + y, new Rune('.')); } } } @@ -238,14 +240,165 @@ public override void Redraw(Rect bounds) return; } + else + { + var top = new Rectangle(0, 0, bounds.Width, rootCommandsListView.Frame.Top - 1); + RenderTitle(top); + } } + private void RenderTitle(Rectangle inArea) + { + var assembly = typeof(Label).Assembly; + var informationalVersion = assembly + .GetCustomAttribute()?.InformationalVersion + ?? "unknown"; + + if (informationalVersion.Contains("+")) + { + informationalVersion = informationalVersion.Substring(0, informationalVersion.IndexOf('+')); + } + + // The main ASCII art text block + string artText = """ + ___________ .__ .__ + \__ ___/__________ _____ |__| ____ _____ | | + | |_/ __ \_ __ \/ \| |/ \\__ \ | | + | |\ ___/| | \/ Y Y \ | | \/ __ \| |__ + |____| \___ >__| |__|_| /__|___| (____ /____/ + \/ \/ \/ \/ + ________ .__ ________ .__ + / _____/ __ __|__| \______ \ ____ _____|__| ____ ____ ___________ + / \ ___| | \ | | | \_/ __ \ / ___/ |/ ___\ / \_/ __ \_ __ \ + \ \_\ \ | / | | ` \ ___/ \___ \| / /_/ > | \ ___/| | \/ + \______ /____/|__| /_______ /\___ >____ >__\___ /|___| /\___ >__| + \/ \/ \/ \/ /_____/ \/ \/ +"""; + + // Standardize the text + artText = artText.Replace("\r\n", "\n"); + + // The version information line + string versionLine = $"(Alpha - {informationalVersion} )"; + + // Split the ASCII art into lines + var artLines = artText.Split('\n'); + + // Calculate the starting point for centering the art text + int artHeight = artLines.Length; + int artWidth = artLines.Max(line => line.Length); + + // Check if there's enough space for the ASCII art and the version line + if (inArea.Width < artWidth || inArea.Height < (artHeight + 2)) // +2 allows space for version line + { + // Not enough space, render the simpler title + + // Simple title and version + string simpleTitle = "Terminal Gui Designer"; + int simpleTitleX = inArea.X + (inArea.Width - simpleTitle.Length) / 2; + int versionLineX = inArea.X + (inArea.Width - versionLine.Length) / 2; + + // Create the gradient + var gradient = new Gradient( + new[] + { + new Color("#FF0000"), // Red + new Color("#FF7F00"), // Orange + new Color("#FFFF00"), // Yellow + new Color("#00FF00"), // Green + new Color("#00FFFF"), // Cyan + new Color("#0000FF"), // Blue + new Color("#8B00FF") // Violet + }, + new[] { 10 } + ); + var fill = new GradientFill(inArea, gradient, GradientDirection.Diagonal); + + // Render the simple title + for (int i = 0; i < simpleTitle.Length; i++) + { + int x = simpleTitleX + i; + int y = inArea.Y + inArea.Height / 2 - 1; // Center the title vertically + + var colorAtPoint = fill.GetColor(new Point(x, y)); + Driver.SetAttribute(new Attribute(new Color(colorAtPoint), new Color(ColorName.Black))); + this.AddRune(x, y, (Rune)simpleTitle[i]); + } + + // Render the version line below the simple title + for (int i = 0; i < versionLine.Length; i++) + { + int x = versionLineX + i; + int y = inArea.Y + inArea.Height / 2; // Line below the title + + var colorAtPoint = fill.GetColor(new Point(x, y)); + Driver.SetAttribute(new Attribute(new Color(colorAtPoint), new Color(ColorName.Black))); + this.AddRune(x, y, (Rune)versionLine[i]); + } + } + else + { + // Enough space, render the ASCII art block + + int artStartX = inArea.X + (inArea.Width - artWidth) / 2; + int artStartY = inArea.Y + (inArea.Height - artHeight - 1) / 2; // -1 for the version line below + + // Create the gradient + var gradient = new Gradient( + new[] + { + new Color("#FF0000"), // Red + new Color("#FF7F00"), // Orange + new Color("#FFFF00"), // Yellow + new Color("#00FF00"), // Green + new Color("#00FFFF"), // Cyan + new Color("#0000FF"), // Blue + new Color("#8B00FF") // Violet + }, + new[] { 10 } + ); + var fill = new GradientFill(inArea, gradient, GradientDirection.Diagonal); + + // Render the ASCII art block + for (int i = 0; i < artLines.Length; i++) + { + string line = artLines[i]; + for (int j = 0; j < line.Length; j++) + { + int x = artStartX + j; + int y = artStartY + i; + + var colorAtPoint = fill.GetColor(new Point(x, y)); + Driver.SetAttribute(new Attribute(new Color(colorAtPoint), new Color(ColorName.Black))); + this.AddRune(x, y, (Rune)line[j]); + } + } + + // Render the version line below the ASCII art + int versionLineX = inArea.X + (inArea.Width - versionLine.Length) / 2; + int versionLineY = artStartY + artHeight; + + for (int i = 0; i < versionLine.Length; i++) + { + int x = versionLineX + i; + int y = versionLineY; + + var colorAtPoint = fill.GetColor(new Point(x, y)); + Driver.SetAttribute(new Attribute(new Color(colorAtPoint), new Color(ColorName.Black))); + this.AddRune(x, y, (Rune)versionLine[i]); + } + } + } + + + + /// - /// Event handler for . + /// Event handler for . /// - /// The key pressed. + /// The key pressed. /// True if key is handled. - public bool HandleKey(KeyEvent keyEvent) + public bool HandleKey(Key key) { // if another window is showing don't respond to hotkeys if (!this.IsCurrentTop) @@ -253,7 +406,7 @@ public bool HandleKey(KeyEvent keyEvent) return false; } - if (this.editting) + if (this.editing) { return false; } @@ -262,53 +415,54 @@ public bool HandleKey(KeyEvent keyEvent) // this key e.g. for typing into menus / reordering menus // etc if (this.keyboardManager.HandleKey( - SelectionManager.Instance.GetSingleSelectionOrNull()?.View ?? this, keyEvent)) + SelectionManager.Instance.GetSingleSelectionOrNull()?.View ?? this, key)) { return true; } try { - this.editting = true; + this.editing = true; SelectionManager.Instance.LockSelection = true; - if (keyEvent.Key == this.keyMap.ShowContextMenu && !this.menuOpen) + string keyString = key.ToString( ); + if (keyString == this.keyMap.ShowContextMenu && !this.menuOpen) { this.CreateAndShowContextMenu(null, null); return true; } - if (keyEvent.Key == this.keyMap.EditProperties) + if (keyString == this.keyMap.EditProperties) { this.ShowEditProperties(); return true; } - if (keyEvent.Key == this.keyMap.ShowColorSchemes) + if (keyString == this.keyMap.ShowColorSchemes) { this.ShowColorSchemes(); return true; } - if (keyEvent.Key == this.keyMap.Copy) + if (keyString == this.keyMap.Copy) { this.Copy(); return true; } - if (keyEvent.Key == this.keyMap.Paste) + if (keyString == this.keyMap.Paste) { this.Paste(); return true; } - if (keyEvent.Key == this.keyMap.ViewSpecificOperations) + if (keyString == this.keyMap.ViewSpecificOperations) { this.ShowViewSpecificOperations(); return true; } - if (keyEvent.Key == this.keyMap.EditRootProperties) + if (keyString == this.keyMap.EditRootProperties) { if (this.viewBeingEdited == null) { @@ -319,212 +473,217 @@ public bool HandleKey(KeyEvent keyEvent) return true; } - if (keyEvent.Key == this.keyMap.Open) + if (keyString == this.keyMap.Open) { this.Open(); return true; } - if (keyEvent.Key == this.keyMap.Save) + if (keyString == this.keyMap.Save) { this.Save(); return true; } - if (keyEvent.Key == this.keyMap.New) + if (keyString == this.keyMap.New) { this.New(); return true; } - if (keyEvent.Key == this.keyMap.ShowHelp) + if (keyString == this.keyMap.ShowHelp) { this.ShowHelp(); return true; } - if (keyEvent.Key == this.keyMap.AddView) + if (keyString == this.keyMap.AddView) { this.ShowAddViewWindow(); return true; } - if (keyEvent.Key == this.keyMap.ToggleDragging) + if (keyString == this.keyMap.ToggleDragging) { this.enableDrag = !this.enableDrag; return true; } - if (keyEvent.Key == this.keyMap.Undo) + if (keyString == this.keyMap.Undo) { OperationManager.Instance.Undo(); return true; } - if (keyEvent.Key == this.keyMap.Redo) + if (keyString == this.keyMap.Redo) { OperationManager.Instance.Redo(); return true; } - if (keyEvent.Key == this.keyMap.Delete) + if (keyString == this.keyMap.Delete) { this.Delete(); return true; } - if (keyEvent.Key == this.keyMap.ToggleShowFocused) + if (keyString == this.keyMap.ToggleShowFocused) { this.enableShowFocused = !this.enableShowFocused; this.SetNeedsDisplay(); return true; } - if (keyEvent.Key == this.keyMap.ToggleShowBorders) + if (keyString == this.keyMap.ToggleShowBorders) { ShowBorders = !ShowBorders; this.SetNeedsDisplay(); return true; } - if (keyEvent.Key == this.keyMap.SelectAll) + if (keyString == this.keyMap.SelectAll) { this.SelectAll(); return true; } - if (keyEvent.Key == this.keyMap.MoveUp) + if (keyString == this.keyMap.MoveUp) { this.MoveControl(0, -1); return true; } - if (keyEvent.Key == this.keyMap.MoveDown) + if (keyString == this.keyMap.MoveDown) { this.MoveControl(0, 1); return true; } - if (keyEvent.Key == this.keyMap.MoveLeft) + if (keyString == this.keyMap.MoveLeft) { this.MoveControl(-1, 0); return true; } - if (keyEvent.Key == this.keyMap.MoveRight) + if (keyString == this.keyMap.MoveRight) { this.MoveControl(1, 0); return true; } - if (keyEvent.Key == this.keyMap.MoveDown) + if (keyString == this.keyMap.MoveDown) { this.MoveControl(0, 1); return true; } - if (keyEvent.Key == this.keyMap.MoveLeft) + if (keyString == this.keyMap.MoveLeft) { this.MoveControl(-1, 0); return true; } - if (keyEvent.Key == this.keyMap.MoveRight) + if (keyString == this.keyMap.MoveRight) { this.MoveControl(1, 0); return true; } // Fast moving things - switch (keyEvent.Key) + switch (key.KeyCode) { - case Key.CursorUp | Key.CtrlMask: + case KeyCode.CursorUp | KeyCode.CtrlMask: this.MoveControl(0, -3); return true; - case Key.CursorDown | Key.CtrlMask: + case KeyCode.CursorDown | KeyCode.CtrlMask: this.MoveControl(0, 3); return true; - case Key.CursorLeft | Key.CtrlMask: + case KeyCode.CursorLeft | KeyCode.CtrlMask: this.MoveControl(-5, 0); return true; - case Key.CursorRight | Key.CtrlMask: + case KeyCode.CursorRight | KeyCode.CtrlMask: this.MoveControl(5, 0); return true; } } - catch (System.Exception ex) + catch (Exception ex) { ExceptionViewer.ShowException("Error", ex); } finally { SelectionManager.Instance.LockSelection = false; - this.editting = false; + this.editing = false; } return false; } /// - /// True if there have been tracked by + /// Gets a value indicating whether there have been any s tracked by the /// since the last save. /// - /// True if unsaved changes. - public bool HasUnsavedChanges() + /// if unsaved changes exist. + public bool HasUnsavedChanges { - var savedOp = this.lastSavedOperation; - var currentOp = OperationManager.Instance.GetLastAppliedOperation()?.UniqueIdentifier; - - // if we have nothing saved - if (savedOp == null) + get { - // then we must save if we have done something - return currentOp != null; - } + var savedOp = this.LastSavedOperation; + var currentOp = OperationManager.Instance.GetLastAppliedOperation( )?.UniqueIdentifier; - // we must save if the head of the operations stack doesn't match what we saved - // this lets us save, perform action, undo action and then still consider us saved - return savedOp != currentOp; + // if we have nothing saved + if ( savedOp == null ) + { + // then we must save if we have done something + return currentOp != null; + } + + // we must save if the head of the operations stack doesn't match what we saved + // this lets us save, perform action, undo action and then still consider us saved + return savedOp != currentOp; + } } private string GetHelpWithEmptyFormLoaded() { - return @$"{this.keyMap.AddView} to Add a View"; + return $"{this.keyMap.AddView} to Add a View"; } private string GetHelp() { - return @$" -{this.keyMap.ShowHelp} - Show Help -{this.keyMap.New} - New Window/Class -{this.keyMap.Open} - Open a .Designer.cs file -{this.keyMap.Save} - Save an opened .Designer.cs file -{this.keyMap.ShowContextMenu} - Show right click context menu; -{this.keyMap.AddView} - Add View -{this.keyMap.ShowColorSchemes} - Color Schemes -{this.keyMap.ToggleDragging} - Toggle mouse dragging on/off -{this.keyMap.ToggleShowFocused} - Toggle show focused view field name -{this.keyMap.ToggleShowBorders} - Toggle dotted borders for frameless views -{this.keyMap.EditProperties} - Edit View Properties -{this.keyMap.ViewSpecificOperations} - View Specific Operations -{this.keyMap.EditRootProperties} - Edit Root Properties -{this.keyMap.Delete} - Delete selected View -Shift+Cursor - Move focused View -Ctrl+Cursor - Move focused View quickly -Ctrl+Q - Quit -{this.keyMap.Undo} - Undo -{this.keyMap.Redo} - Redo"; + return $""" + + {this.keyMap.ShowHelp} - Show Help + {this.keyMap.New} - New Window/Class + {this.keyMap.Open} - Open a .Designer.cs file + {this.keyMap.Save} - Save an opened .Designer.cs file + {this.keyMap.ShowContextMenu} - Show right click context menu; + {this.keyMap.AddView} - Add View + {this.keyMap.ShowColorSchemes} - Color Schemes + {this.keyMap.ToggleDragging} - Toggle mouse dragging on/off + {this.keyMap.ToggleShowFocused} - Toggle show focused view field name + {this.keyMap.ToggleShowBorders} - Toggle dotted borders for frameless views + {this.keyMap.EditProperties} - Edit View Properties + {this.keyMap.ViewSpecificOperations} - View Specific Operations + {this.keyMap.EditRootProperties} - Edit Root Properties + {this.keyMap.Delete} - Delete selected View + Shift+Cursor - Move focused View + Ctrl+Cursor - Move focused View quickly + Esc - Quit + {this.keyMap.Undo} - Undo + {this.keyMap.Redo} - Redo + """; } private void BuildRootMenu() { /* setup views for when we are not editing a * view (nothing is loaded) so show the generic - * help (open, new etc) in the center of the + * help (open, new etc.) in the center of the * screen */ - var rootCommands = new List + var rootCommands = new ObservableCollection { $"{this.keyMap.ShowHelp} - Show Help", $"{this.keyMap.New} - New Window/Class", @@ -538,18 +697,27 @@ private void BuildRootMenu() rootCommands[i] = rootCommands[i].PadBoth(maxWidth); } - this.rootCommandsListView = new ListView(rootCommands) + this.rootCommandsListView = new ListView() { X = Pos.Center(), - Y = Pos.Center(), + Y = Pos.Percent(75), Width = maxWidth, Height = 3, - ColorScheme = new DefaultColorSchemes().GetDefaultScheme("greyOnBlack").Scheme, + ColorScheme = new ColorScheme + ( + new Attribute(new Color(Color.White),new Color(Color.Black)), + new Attribute(new Color(Color.Black),new Color(Color.White)), + new Attribute(new Color(Color.White), new Color(Color.Black)), + new Attribute(new Color(Color.White), new Color(Color.Black)), + new Attribute(new Color(Color.Black), new Color(Color.White)) + ), }; + this.rootCommandsListView.SetSource(rootCommands); + this.rootCommandsListView.SelectedItem = 0; - this.rootCommandsListView.KeyDown += (e) => + this.rootCommandsListView.KeyDown += (_, e) => { - if (e.KeyEvent.Key == Key.Enter) + if (e == Key.Enter) { e.Handled = true; @@ -566,19 +734,35 @@ private void BuildRootMenu() break; } } + + + if (e == this.keyMap.New) + { + this.New(); + } + + if (e == this.keyMap.ShowHelp) + { + this.ShowHelp(); + } + + if (e == this.keyMap.Open) + { + this.Open(); + } }; this.Add(this.rootCommandsListView); } - private void Editor_Closing(ToplevelClosingEventArgs obj) + private void Editor_Closing(object? sender, ToplevelClosingEventArgs obj) { if (this.viewBeingEdited == null) { return; } - if (this.HasUnsavedChanges()) + if (this.HasUnsavedChanges) { int answer = ChoicesDialog.Query("Unsaved Changes", $"You have unsaved changes to {this.viewBeingEdited.SourceCode.DesignerFile.Name}", "Save", "Don't Save", "Cancel"); @@ -607,12 +791,9 @@ private void CreateAndShowContextMenu(MouseEvent? m, Design? rightClicked) var selected = SelectionManager.Instance.Selected.ToArray(); + // BUG: This is an improper exception here and could have unexpected behavior if this method is ever called asynchronously. var factory = new OperationFactory( - (p, v) => - { - return EditDialog.GetNewValue(p.Design, p, v, out var newValue) ? newValue - : throw new OperationCanceledException(); - }); + (p, v) => ValueFactory.GetNewValue(p.Design, p, v, out var newValue) ? newValue : throw new OperationCanceledException() ); var operations = factory .CreateOperations(selected, m, rightClicked, out string name) @@ -622,9 +803,9 @@ private void CreateAndShowContextMenu(MouseEvent? m, Design? rightClicked) var setProps = operations.OfType(); var others = operations .Except(setProps) - .GroupBy(k => k.Category, this.ToMenuItem); + .GroupBy(k => k.Category, ToMenuItem); - var setPropsItems = setProps.Select(this.ToMenuItem).ToArray(); + var setPropsItems = setProps.Select(ToMenuItem).ToArray(); bool hasPropsItems = setPropsItems.Any(); var all = new List(); @@ -651,7 +832,7 @@ private void CreateAndShowContextMenu(MouseEvent? m, Design? rightClicked) if (string.IsNullOrWhiteSpace(g.Key)) { // add the operations with no category in alphabetical order - all.AddRange(g.OrderBy(g => g.Title)); + all.AddRange(g.OrderBy(mi => mi.Title)); } else { @@ -662,7 +843,7 @@ private void CreateAndShowContextMenu(MouseEvent? m, Design? rightClicked) } } - // theres nothing we can do + // there's nothing we can do if (all.Count == 0) { return; @@ -673,47 +854,56 @@ private void CreateAndShowContextMenu(MouseEvent? m, Design? rightClicked) if (m != null) { - menu.Position = new Point(m.X, m.Y); + menu.Position = m.Position; } else { var d = SelectionManager.Instance.Selected.FirstOrDefault() ?? this.viewBeingEdited; - d.View.ViewToScreenActual(0, 0, out var x, out var y); - menu.Position = new Point(x, y); + var pt = d.View.ContentToScreen(new Point(0, 0)); + menu.Position = new Point(pt.X, pt.Y); } this.menuOpen = true; SelectionManager.Instance.LockSelection = true; + + if(m != null) + { + m.Handled = true; + } + menu.Show(); - menu.MenuBar.MenuAllClosed += () => + menu.MenuBar.MenuAllClosed += (_, _) => { this.menuOpen = false; SelectionManager.Instance.LockSelection = false; }; } - private MenuItem ToMenuItem(IOperation operation) + private static MenuItem ToMenuItem(IOperation operation) { - return new MenuItem(operation.ToString(), string.Empty, () => this.Try(() => OperationManager.Instance.Do(operation))); - } + return new MenuItem(operation.ToString(), string.Empty, () => Try(() => OperationManager.Instance.Do(operation))); - private void Try(Action action) - { - try - { - SelectionManager.Instance.LockSelection = true; - action(); - } - catch (Exception ex) - { - ExceptionViewer.ShowException("Operation failed", ex); - } - finally + static void Try(Action action) { - SelectionManager.Instance.LockSelection = false; + try + { + // BUG: Thread-safety + // Race conditions because this is not a valid synchronization mechanism + SelectionManager.Instance.LockSelection = true; + action(); + } + catch (Exception ex) + { + ExceptionViewer.ShowException("Operation failed", ex); + } + finally + { + SelectionManager.Instance.LockSelection = false; + } } } + private string? GetLowerRightTextIfAny() { if (this.flashMessage != null) @@ -816,7 +1006,7 @@ private void Delete() } } - private void DoForSelectedViews(Func operationFuc, bool allowOnRoot = false) + private void DoForSelectedViews(Func operationFunc, bool allowOnRoot = false) { if (this.viewBeingEdited == null) { @@ -829,7 +1019,7 @@ private void DoForSelectedViews(Func operationFuc, bool allow { var op = new CompositeOperation( SelectionManager.Instance.Selected - .Select(operationFuc).ToArray()); + .Select(operationFunc).ToArray()); OperationManager.Instance.Do(op); } @@ -838,25 +1028,22 @@ private void DoForSelectedViews(Func operationFuc, bool allow var viewDesign = selected.Single(); // don't delete the root view - if (viewDesign != null) + if (viewDesign.IsRoot && !allowOnRoot) { - if (viewDesign.IsRoot && !allowOnRoot) - { - return; - } - - OperationManager.Instance.Do( - operationFuc(viewDesign)); + return; } + + OperationManager.Instance.Do(operationFunc(viewDesign)); } } private void Open() { - var ofd = new OpenDialog( - "Open", - $"Select {SourceCodeFile.ExpectedExtension} file", - new List(new[] { SourceCodeFile.ExpectedExtension })); + var ofd = new OpenDialog() + { + Title = "Open", + AllowedTypes = new List(new[] { new AllowedType("View", SourceCodeFile.ExpectedExtension) }) + }; Application.Run(ofd, this.ErrorHandler); @@ -864,7 +1051,7 @@ private void Open() { try { - var path = ofd.FilePath.ToString(); + var path = ofd.Path.ToString(); if (string.IsNullOrEmpty(path)) { @@ -875,7 +1062,7 @@ private void Open() } catch (Exception ex) { - ExceptionViewer.ShowException($"Failed to open '{ofd.FilePath}'", ex); + ExceptionViewer.ShowException($"Failed to open '{ofd.Path}'", ex); } } } @@ -901,14 +1088,14 @@ private void Open(FileInfo toOpen) var decompiler = new CodeToView(new SourceCodeFile(toOpen)); instance = decompiler.CreateInstance(); }).ContinueWith( - (t, o) => + (t, _) => { // no longer loading - Application.MainLoop.Invoke(() => Application.RequestStop()); + Application.Invoke(() => Application.RequestStop()); if (t.Exception != null) { - Application.MainLoop.Invoke(() => + Application.Invoke(() => ExceptionViewer.ShowException($"Failed to open '{toOpen.Name}'", t.Exception)); return; } @@ -926,20 +1113,16 @@ private void Open(FileInfo toOpen) private void New() { - if (!Modals.Get("Create New View", "Ok", this.GetSupportedRootViews(), null, out var selected)) + if (!Modals.Get("Create New View", "Ok", GetSupportedRootViews(), null, out var selected)) { return; } - var ofd = new SaveDialog( - "New", - $"Class file", - new List(new[] { ".cs" })) + var ofd = new SaveDialog() { - NameDirLabel = "Directory", - NameFieldLabel = "Class", - FilePath = "MyView.cs", - AllowsOtherFileTypes = false, + Title = "New", + AllowedTypes = new List() { new AllowedType("C# File", ".cs") }, + Path = "MyView.cs", }; Application.Run(ofd); @@ -948,7 +1131,7 @@ private void New() { try { - var path = ofd.FilePath.ToString(); + var path = ofd.Path; if (string.IsNullOrWhiteSpace(path) || selected == null) { @@ -960,6 +1143,13 @@ private void New() // Check if we are about to overwrite some files // and if so warn the user var files = new SourceCodeFile(file); + + if(!CodeDomArgs.IsValidIdentifier(files.ClassName)) + { + ChoicesDialog.Query("Invalid Name",$"Invalid class name '{files.ClassName}'","Ok"); + return; + } + var sb = new StringBuilder(); if (files.CsFile.Exists) @@ -984,15 +1174,15 @@ private void New() } catch (Exception ex) { - ExceptionViewer.ShowException($"Failed to create '{ofd.FilePath}'", ex); + ExceptionViewer.ShowException($"Failed to create '{ofd.Path}'", ex); throw; } } } - private Type[] GetSupportedRootViews() + private static Type[] GetSupportedRootViews() { - return new Type[] { typeof(Window), typeof(Dialog), typeof(View), typeof(Toplevel) }; + return new Type[] { typeof(Window), typeof(Dialog), typeof(View), typeof(Toplevel) }; } private void New(FileInfo toOpen, Type typeToCreate, string? explicitNamespace) @@ -1000,6 +1190,7 @@ private void New(FileInfo toOpen, Type typeToCreate, string? explicitNamespace) var viewToCode = new ViewToCode(); string? ns = explicitNamespace; + // TODO: The following two if statements can be combined and run in a loop until the user either cancels or gets it right // if no explicit one if (string.IsNullOrWhiteSpace(ns)) { @@ -1012,7 +1203,7 @@ private void New(FileInfo toOpen, Type typeToCreate, string? explicitNamespace) } // Validate the namespace - if (string.IsNullOrWhiteSpace(ns) || ns.Contains(" ") || char.IsDigit(ns.First())) + if (string.IsNullOrWhiteSpace(ns) || ns.Contains(' ') || char.IsDigit(ns.First())) { MessageBox.ErrorQuery("Invalid Namespace", "Namespace must not contain spaces, be empty or begin with a number", "Ok"); return; @@ -1026,19 +1217,20 @@ private void New(FileInfo toOpen, Type typeToCreate, string? explicitNamespace) var open = new LoadingDialog(toOpen); + // BUG: If this is not awaited, exceptions at any point of it can be thrown at an indeterminate place and time. Task.Run(() => { // Create the view files and compile instance = viewToCode.GenerateNewView(toOpen, ns ?? "YourNamespace", typeToCreate); }).ContinueWith( - (t, o) => + (t, _) => { // no longer loading - Application.MainLoop.Invoke(() => Application.RequestStop()); + Application.Invoke(() => Application.RequestStop()); if (t.Exception != null) { - Application.MainLoop.Invoke(() => + Application.Invoke(() => ExceptionViewer.ShowException($"Failed to create '{toOpen.Name}'", t.Exception)); return; } @@ -1056,7 +1248,7 @@ private void New(FileInfo toOpen, Type typeToCreate, string? explicitNamespace) private void ReplaceViewBeingEdited(Design design) { - Application.MainLoop.Invoke(() => + Application.Invoke(() => { // remove the old view if (this.viewBeingEdited != null) @@ -1097,7 +1289,7 @@ private void Save() this.flashMessage = $"Saved {this.viewBeingEdited.SourceCode.DesignerFile.Name}"; this.SetNeedsDisplay(); - this.lastSavedOperation = OperationManager.Instance.GetLastAppliedOperation()?.UniqueIdentifier; + this.LastSavedOperation = OperationManager.Instance.GetLastAppliedOperation()?.UniqueIdentifier; } private void ShowAddViewWindow() diff --git a/src/UI/KeyMap.cs b/src/UI/KeyMap.cs index a5e411fd..a4e30d54 100644 --- a/src/UI/KeyMap.cs +++ b/src/UI/KeyMap.cs @@ -1,3 +1,6 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Configuration; using Terminal.Gui; using TerminalGuiDesigner.Operations; using TerminalGuiDesigner.ToCode; @@ -5,167 +8,222 @@ namespace TerminalGuiDesigner.UI; -/// -/// Serializeable settings class for user keybinding/accessibility tailoring. -/// -public class KeyMap +/// Serializable settings class for user keybinding/accessibility tailoring. +[JsonSourceGenerationOptions( JsonSerializerDefaults.General, Converters = new[] { typeof( JsonStringEnumConverter ), typeof( JsonStringEnumConverter ) } )] +public sealed record KeyMap( + string EditProperties, + string ShowContextMenu, + string ViewSpecificOperations, + string EditRootProperties, + string ShowHelp, + string New, + string Open, + string Save, + string Redo, + string Undo, + string Delete, + string ToggleDragging, + string AddView, + string ToggleShowFocused, + string ToggleShowBorders, + MouseFlags RightClick, + string Copy, + string Paste, + string Rename, + string SetShortcut, + string SelectAll, + string MoveRight, + string MoveLeft, + string MoveUp, + string MoveDown, + string ShowColorSchemes ) { - /// - /// Gets or Sets key for editing all of selected . - /// - public Key EditProperties { get; set; } = Key.F4; + /// Initializes a new instance of the class. + public KeyMap( ) + : this( + Key.F4.ToString( ), + Key.Enter.ToString( ), + Key.F4.WithShift.ToString( ), + Key.F5.ToString( ), + Key.F1.ToString( ), + Key.N.WithCtrl.ToString( ), + Key.O.WithCtrl.ToString( ), + Key.S.WithCtrl.ToString( ), + Key.Y.WithCtrl.ToString( ), + Key.Z.WithCtrl.ToString( ), + Key.DeleteChar.ToString( ), + Key.F3.ToString( ), + Key.F2.ToString( ), + Key.L.WithCtrl.ToString( ), + Key.B.WithCtrl.ToString( ), + MouseFlags.Button3Clicked, + Key.C.WithCtrl.ToString( ), + Key.V.WithCtrl.ToString( ), + Key.R.WithCtrl.ToString( ), + Key.T.WithCtrl.ToString( ), + Key.A.WithCtrl.ToString( ), + Key.CursorRight.WithShift.ToString( ), + Key.CursorLeft.WithShift.ToString( ), + Key.CursorUp.WithShift.ToString( ), + Key.CursorDown.WithShift.ToString( ), + Key.F6.ToString( ) ) + { + // Empty - returns default instance from primary constructor. + } /// - /// Gets or Sets the key to pop up the right click context menu. + /// Gets the string to add a new (and its wrapper) to the currently focused container + /// view (or root). /// - public Key ShowContextMenu { get; set; } = Key.Enter; + public string AddView { get; init; } = AddView; - /// - /// Gets or Sets the key to pop up view specific operations (e.g. add column to ). - /// - public Key ViewSpecificOperations { get; set; } = Key.ShiftMask | Key.F4; + /// Gets the string to copy currently selected views. + public string Copy { get; init; } = Copy; - /// - /// Gets or Sets the key to edit the of the root view being edited. - /// - public Key EditRootProperties { get; set; } = Key.F5; + /// Gets the string to delete the currently selected . + public string Delete { get; init; } = Delete; /// - /// Gets or Sets the key to show pop up help. + /// Gets the string for editing all of selected s. /// - public Key ShowHelp { get; set; } = Key.F1; + public string EditProperties { get; init; } = EditProperties; /// - /// Gets or Sets the key to create a new .Designer.cs and .cs file. + /// Gets the string to edit the of the root view being edited. /// - public Key New { get; set; } = Key.CtrlMask | Key.N; + public string EditRootProperties { get; init; } = EditRootProperties; - /// - /// Gets or Sets the key to open an existing .Designer.cs file. - /// - public Key Open { get; set; } = Key.CtrlMask | Key.O; + /// Gets the string to nudge the currently focused view down one unit. + public string MoveDown { get; init; } = MoveDown; - /// - /// Gets or Sets the key to save the current changes to disk. - /// - public Key Save { get; set; } = Key.CtrlMask | Key.S; + /// Gets the string to nudge the currently focused view left one unit. + public string MoveLeft { get; init; } = MoveLeft; - /// - /// Gets or Sets the key to the last undone operation. - /// - public Key Redo { get; set; } = Key.CtrlMask | Key.Y; + /// Gets the string to nudge the currently focused view right one unit. + public string MoveRight { get; init; } = MoveRight; - /// - /// Gets or Sets the key to the last performed operation. - /// - public Key Undo { get; set; } = Key.CtrlMask | Key.Z; + /// Gets the string to nudge the currently focused view up one unit. + public string MoveUp { get; init; } = MoveUp; - /// - /// Gets or Sets the key to delete the currently selected . - /// - public Key Delete { get; set; } = Key.DeleteChar; + /// Gets the string to create a new .Designer.cs and .cs file. + public string New { get; init; } = New; - /// - /// Gets or Sets the key to turn mouse dragging on/of. - /// - public Key ToggleDragging { get; set; } = Key.F3; + /// Gets the string to open an existing .Designer.cs file. + public string Open { get; init; } = Open; - /// - /// Gets or Sets the key to add a new (and its wrapper) - /// to the currently focused container view (or root). - /// - public Key AddView { get; set; } = Key.F2; + /// Gets the string to paste the last copied views. + public string Paste { get; init; } = Paste; - /// - /// Gets or Sets the key to toggle showing an overlay with a description of the currently focused - /// view(s). - /// - public Key ToggleShowFocused { get; set; } = Key.CtrlMask | Key.L; + /// Gets the string to the last undone operation. + public string Redo { get; init; } = Redo; /// - /// Gets or Sets the key to toggle showing dotted borders around views that otherwise do not have - /// visible borders (e.g. . + /// Gets the string to rename the field name (when written to .Designer.cs) of the currently selected or + /// . /// - public Key ToggleShowBorders { get; set; } = Key.CtrlMask | Key.B; + public string Rename { get; init; } = Rename; /// - /// Gets or Sets the mouse button that opens the right click context menu. - /// Defaults to which is the right - /// mouse button. + /// Gets the mouse button that opens the right click context menu. Defaults to which is + /// the right mouse button. /// - public MouseFlags RightClick { get; set; } = MouseFlags.Button3Clicked; + [JsonConverter( typeof( JsonStringEnumConverter ) )] + public MouseFlags RightClick { get; init; } = RightClick; - /// - /// Gets or Sets the key to copy currently selected views. - /// - public Key Copy { get; set; } = Key.CtrlMask | Key.C; + /// Gets the string to save the current changes to disk. + public string Save { get; init; } = Save; /// - /// Gets or Sets the key to paste the last copied views. + /// Gets the string to select all in the . /// - public Key Paste { get; set; } = Key.CtrlMask | Key.V; + public string SelectAll { get; init; } = SelectAll; /// - /// Gets or Sets the key to rename the field name (when written to .Designer.cs) of - /// the currently selected or . + /// Gets a custom to apply to multi selections in designer. + /// + /// Default color is green, this is useful if you have a heavily green theme where it could get confusing what is multi selected + /// and what just has focus/uses your custom scheme + /// /// - public Key Rename { get; set; } = Key.CtrlMask | Key.R; + public ColorSchemeBlueprint SelectionColor { get; init; } = new( + Color.BrightGreen, + Color.Green, + Color.BrightGreen, + Color.Green, + Color.BrightYellow, + Color.Green, + Color.BrightYellow, + Color.Green, + Color.BrightGreen, + Color.Green ); - /// - /// Gets or Sets the key to assign a new shortcut to a . - /// - public Key SetShortcut { get; set; } = Key.CtrlMask | Key.T; + /// Gets the string to assign a new shortcut to a . + public string SetShortcut { get; init; } = SetShortcut; /// - /// Gets or Sets the key to select all in the . + /// Gets the string to open the window for creating/deleting that can be + /// used in the . /// - public Key SelectAll { get; set; } = Key.CtrlMask | Key.A; + public string ShowColorSchemes { get; init; } = ShowColorSchemes; - /// - /// Gets or Sets the key to nudge the currently focused view right one unit. - /// - public Key MoveRight { get; set; } = Key.ShiftMask | Key.CursorRight; + /// Gets the string to pop up the right click context menu. + public string ShowContextMenu { get; init; } = ShowContextMenu; + + /// Gets the string to show pop up help. + public string ShowHelp { get; init; } = ShowHelp; + + /// Gets the string to turn mouse dragging on/of. + public string ToggleDragging { get; init; } = ToggleDragging; /// - /// Gets or Sets the key to nudge the currently focused view left one unit. + /// Gets the string to toggle showing dotted borders around views that otherwise do not have visible borders (e.g. + /// ). /// - public Key MoveLeft { get; set; } = Key.ShiftMask | Key.CursorLeft; + public string ToggleShowBorders { get; init; } = ToggleShowBorders; /// - /// Gets or Sets the key to nudge the currently focused view up one unit. + /// Gets the string to toggle showing an overlay with a description of the currently focused view(s). /// - public Key MoveUp { get; set; } = Key.ShiftMask | Key.CursorUp; + public string ToggleShowFocused { get; init; } = ToggleShowFocused; + + /// Gets the string to the last performed operation. + public string Undo { get; init; } = Undo; /// - /// Gets or Sets the key to nudge the currently focused view down one unit. + /// Gets the string to pop up view specific operations (e.g. add column to ). /// - public Key MoveDown { get; set; } = Key.ShiftMask | Key.CursorDown; + + public string ViewSpecificOperations { get; init; } = ViewSpecificOperations; /// - /// Gets or Sets the key to open the window for - /// creating/deleting that can be used in the . + /// Builds a configuration from the specified yaml file and returns a new instance of a corresponding . /// - public Key ShowColorSchemes { get; set; } = Key.F6; + /// Relative or absolute path to the configuration file to load. + /// Whether this file is optional and can be ignored if missing. + /// A new instance of a from the specified file. + /// If is a null or empty string. + public static KeyMap LoadFromYamlConfigurationFile( string configurationFile = "Keys.yaml", bool optional = true ) + { + KeyMap? map = new ConfigurationBuilder( ).AddYamlFile( configurationFile, optional, false ).Build( ).Get( ); + return map ?? new( ); + } /// - /// Gets or Sets a custom to apply to multi - /// selections in designer. - /// Default color is green, this is useful if you have a heavily - /// green theme where it could get confusing what is multi selected and what - /// just has focus/uses your custom scheme + /// Builds a configuration from the specified yaml file and returns a new instance of a corresponding . /// - public ColorSchemeBlueprint SelectionColor { get; set; } = new - ColorSchemeBlueprint + /// Relative or absolute path to the configuration file to load. + /// Whether this file is optional and can be ignored if missing. + /// An optional section to load the KeyMap configuration from, in the given file. If null or empty, the root node will be used. + /// A new instance of a from the specified file. + /// If is a null or empty string. + public static KeyMap LoadFromJsonConfigurationFile( string configurationFile = "Keys.json", bool optional = true, string? section = null ) { - NormalForeground = Color.BrightGreen, - NormalBackground = Color.Green, - HotNormalForeground = Color.BrightGreen, - HotNormalBackground = Color.Green, - FocusForeground = Color.BrightYellow, - FocusBackground = Color.Green, - HotFocusForeground = Color.BrightYellow, - HotFocusBackground = Color.Green, - DisabledForeground = Color.BrightGreen, - DisabledBackground = Color.Green, - }; -} + IConfigurationRoot configurationRoot = new ConfigurationBuilder( ).AddJsonFile( configurationFile, optional, false ).Build( ); + return section switch + { + null => configurationRoot.Get( ), + { Length: > 0 } => configurationRoot.GetRequiredSection( section ).Get( ), + _ => null + } ?? new( ); + } +} \ No newline at end of file diff --git a/src/UI/KeyboardManager.cs b/src/UI/KeyboardManager.cs index d474198d..da969137 100644 --- a/src/UI/KeyboardManager.cs +++ b/src/UI/KeyboardManager.cs @@ -1,318 +1,352 @@ -using Terminal.Gui; -using TerminalGuiDesigner.Operations; -using TerminalGuiDesigner.Operations.MenuOperations; -using TerminalGuiDesigner.ToCode; -using TerminalGuiDesigner.UI.Windows; - -namespace TerminalGuiDesigner.UI; - -/// -/// Manager for acting on global key presses before they are passed to other -/// controls while has an open . -/// -public class KeyboardManager -{ - private readonly KeyMap keyMap; - private SetPropertyOperation? currentOperation; - - /// - /// Initializes a new instance of the class. - /// - /// User configurable keybindings for class functionality. - public KeyboardManager(KeyMap keyMap) - { - this.keyMap = keyMap; - } - - /// - /// Evaluates when has - /// focus and orders any based on it or lets it pass through - /// to the rest of the regular Terminal.Gui API layer. - /// - /// The that currently holds focus in . - /// The key that has been reported by . - /// if should be suppressed. - public bool HandleKey(View focusedView, KeyEvent keystroke) - { - var menuItem = MenuTracker.Instance.CurrentlyOpenMenuItem; - - // if we are in a menu - if (menuItem != null) - { - return this.HandleKeyPressInMenu(focusedView, menuItem, keystroke); - } - - var d = focusedView.GetNearestDesign(); - - // if we are no longer focused - if (d == null) - { - // if there is another operation underway - if (this.currentOperation != null) - { - this.FinishOperation(); - } - - // do not swallow this keystroke - return false; - } - - // if we have changed focus - if (this.currentOperation != null && !this.currentOperation.Designs.Contains(d)) - { - this.FinishOperation(); - } - - if (keystroke.Key == this.keyMap.Rename) - { - var nameProp = d.GetDesignableProperties().OfType().FirstOrDefault(); - if (nameProp != null) - { - EditDialog.SetPropertyToNewValue(d, nameProp, nameProp.GetValue()); - return true; - } - } - - if (!this.IsActionableKey(keystroke)) - { - // we can't do anything with this keystroke - return false; - } - - // if we are not currently doing anything - if (this.currentOperation == null) - { - // start a new operation - this.StartOperation(d); - } - - this.ApplyKeystrokeToTextProperty(keystroke); - - return false; - } - - private bool HandleKeyPressInMenu(View focusedView, MenuItem menuItem, KeyEvent keystroke) - { - if (keystroke.Key == this.keyMap.Rename) - { - OperationManager.Instance.Do( - new RenameMenuItemOperation(menuItem)); - return true; - } - - if (keystroke.Key == Key.Enter) - { - OperationManager.Instance.Do( - new AddMenuItemOperation(menuItem)); - - keystroke.Key = Key.CursorDown; - return false; - } - - if (keystroke.Key == this.keyMap.SetShortcut) - { - menuItem.Shortcut = Modals.GetShortcut(); - - focusedView.SetNeedsDisplay(); - return false; - } - - if (keystroke.Key == this.keyMap.MoveRight) - { - OperationManager.Instance.Do( - new MoveMenuItemRightOperation(menuItem)); - - keystroke.Key = Key.CursorUp; - return true; - } - - if (keystroke.Key == this.keyMap.MoveLeft) - { - OperationManager.Instance.Do( - new MoveMenuItemLeftOperation(menuItem)); - - keystroke.Key = Key.CursorDown; - return false; - } - - if (keystroke.Key == this.keyMap.MoveUp) - { - OperationManager.Instance.Do( - new MoveMenuItemOperation(menuItem, true)); - keystroke.Key = Key.CursorUp; - return false; - } - - if (keystroke.Key == this.keyMap.MoveDown) - { - OperationManager.Instance.Do( - new MoveMenuItemOperation(menuItem, false)); - keystroke.Key = Key.CursorDown; - return false; - } - - if ((keystroke.Key == Key.DeleteChar) - || - (keystroke.Key == Key.Backspace && string.IsNullOrWhiteSpace(menuItem.Title.ToString()))) - { - // deleting the menu item using backspace to - // remove all characters in the title or the Del key - var remove = new RemoveMenuItemOperation(menuItem); - if (OperationManager.Instance.Do(remove)) - { - // if we are removing the last item - if (remove.PrunedTopLevelMenu) - { - // if we deleted the last menu item - if (remove.Bar?.Menus.Length == 0) - { - remove.Bar.CloseMenu(); - return true; - } - - // convert keystroke to left - // so we move to the next menu - keystroke.Key = Key.CursorLeft; - return false; - } - - // otherwise convert keystroke to up - // so that focus now sits nicely on the - // menu item above the deleted one - keystroke.Key = Key.CursorUp; - return false; - } - } - - // Allow typing but also Enter to create a new subitem - if (!this.IsActionableKey(keystroke)) - { - return false; - } - - // TODO: This probably lets us edit the Editors own context menus lol - - // TODO once https://github.com/migueldeicaza/gui.cs/pull/1689 is merged and published - // we can integrate this into the Design undo/redo systems - if (this.ApplyKeystrokeToString(menuItem.Title.ToString() ?? string.Empty, keystroke, out var newValue)) - { - // convert to a separator by typing three hyphens - if (newValue.Equals("---")) - { - if (OperationManager.Instance.Do( - new ConvertMenuItemToSeperatorOperation(menuItem))) - { - return true; - } - } - else - { - // changing the title - menuItem.Title = newValue; - } - - focusedView.SetNeedsDisplay(); - - return true; - } - - return false; - } - - private void StartOperation(Design d) - { - // these can already handle editing themselves - if (d.View is DateField || d.View is TextField || d.View is TextView) - { - return; - } - - var textProp = d.GetDesignableProperty("Text"); - - if (textProp != null) - { - this.currentOperation = new SetPropertyOperation(d, textProp, d.View.Text, d.View.Text); - } - } - - private void FinishOperation() - { - if (this.currentOperation == null) - { - return; - } - - // finish it and clear it - OperationManager.Instance.Do(this.currentOperation); - this.currentOperation = null; - } - - private bool ApplyKeystrokeToTextProperty(KeyEvent keystroke) - { - if (this.currentOperation == null || this.currentOperation.Designs.Count != 1) - { - return false; - } - - var design = this.currentOperation.Designs.Single(); - - var str = design.View.GetActualText(); - - if (!this.ApplyKeystrokeToString(str, keystroke, out var newStr)) - { - // not a keystroke we can act upon - return false; - } - - design.View.SetActualText(newStr); - design.View.SetNeedsDisplay(); - this.currentOperation.NewValue = newStr; - - return true; - } - - private bool ApplyKeystrokeToString(string str, KeyEvent keystroke, out string newString) - { - newString = str; - - if (keystroke.Key == Key.Backspace) - { - // no change - if (str == null || str.Length == 0) - { - return false; - } - - // chop off a letter - newString = str.Length == 1 ? string.Empty : str.Substring(0, str.Length - 1); - return true; - } - else - { - var ch = (char)keystroke.KeyValue; - newString += ch; - - return true; - } - } - - private bool IsActionableKey(KeyEvent keystroke) - { - if (keystroke.Key == Key.Backspace) - { - return true; - } - - // Don't let Ctrl+Q add a Q! - if (keystroke.Key.HasFlag(Key.CtrlMask)) - { - return false; - } - - var punctuation = "\"\\/':;%^&*~`!@#.,? ()-+{}<>=_][|"; - - var ch = (char)keystroke.KeyValue; - - return punctuation.Contains(ch) || char.IsLetterOrDigit(ch); - } +using Terminal.Gui; +using TerminalGuiDesigner.Operations; +using TerminalGuiDesigner.Operations.MenuOperations; +using TerminalGuiDesigner.ToCode; +using TerminalGuiDesigner.UI.Windows; + +namespace TerminalGuiDesigner.UI; + +/// +/// Manager for acting on global key presses before they are passed to other +/// controls while has an open . +/// +public class KeyboardManager +{ + private readonly KeyMap keyMap; + private SetPropertyOperation? currentOperation; + + /// + /// Initializes a new instance of the class. + /// + /// User configurable keybindings for class functionality. + public KeyboardManager(KeyMap keyMap) + { + this.keyMap = keyMap; + } + + /// + /// Evaluates when has + /// focus and orders any based on it or lets it pass through + /// to the rest of the regular Terminal.Gui API layer. + /// + /// The that currently holds focus in . + /// The key that has been reported by . + /// if should be suppressed. + public bool HandleKey(View focusedView, Key keystroke) + { + var menuItem = MenuTracker.Instance.CurrentlyOpenMenuItem; + + // if we are in a menu + if (menuItem != null) + { + return this.HandleKeyPressInMenu(focusedView, menuItem, keystroke); + } + + var d = focusedView.GetNearestDesign(); + + // if we are no longer focused + if (d == null) + { + // if there is another operation underway + if (this.currentOperation != null) + { + this.FinishOperation(); + } + + // do not swallow this keystroke + return false; + } + + // if we have changed focus + if (this.currentOperation != null && !this.currentOperation.Designs.Contains(d)) + { + this.FinishOperation(); + } + + if (keystroke.ToString( ) == this.keyMap.Rename) + { + var nameProp = d.GetDesignableProperties().OfType().FirstOrDefault(); + if (nameProp != null) + { + EditDialog.SetPropertyToNewValue(d, nameProp, nameProp.GetValue()); + return true; + } + } + + if (!this.IsActionableKey(keystroke)) + { + // we can't do anything with this keystroke + return false; + } + + // if we are not currently doing anything + if (this.currentOperation == null) + { + // start a new operation + this.StartOperation(d); + } + + return this.ApplyKeystrokeToTextProperty(keystroke); + } + + private bool HandleKeyPressInMenu(View focusedView, MenuItem menuItem, Key keystroke) + { + if (keystroke.ToString( ) == this.keyMap.Rename) + { + OperationManager.Instance.Do( + new RenameMenuItemOperation(menuItem)); + return true; + } + + if (keystroke == Key.Enter) + { + OperationManager.Instance.Do( + new AddMenuItemOperation(menuItem)); + + ChangeKeyTo(keystroke, Key.CursorDown); + return false; + } + + if (keystroke.ToString( ) == this.keyMap.SetShortcut) + { + menuItem.ShortcutKey = Modals.GetShortcut().KeyCode; + + focusedView.SetNeedsDisplay(); + return false; + } + + if (keystroke.ToString( ) == this.keyMap.MoveRight) + { + OperationManager.Instance.Do( + new MoveMenuItemRightOperation(menuItem)); + + ChangeKeyTo(keystroke, Key.CursorUp); + return true; + } + + if (keystroke.ToString( ) == this.keyMap.MoveLeft) + { + OperationManager.Instance.Do( + new MoveMenuItemLeftOperation(menuItem)); + + ChangeKeyTo(keystroke, Key.CursorDown); + return false; + } + + if (keystroke.ToString( ) == this.keyMap.MoveUp) + { + OperationManager.Instance.Do( + new MoveMenuItemOperation(menuItem, true)); + ChangeKeyTo(keystroke, Key.CursorUp); + return false; + } + + if (keystroke.ToString( ) == this.keyMap.MoveDown) + { + OperationManager.Instance.Do( + new MoveMenuItemOperation(menuItem, false)); + ChangeKeyTo(keystroke, Key.CursorDown); + return false; + } + + if ((keystroke == Key.DeleteChar) + || + (keystroke == Key.Backspace && string.IsNullOrWhiteSpace(menuItem.Title.ToString()))) + { + // deleting the menu item using backspace to + // remove all characters in the title or the Del key + var remove = new RemoveMenuItemOperation(menuItem); + if (OperationManager.Instance.Do(remove)) + { + // if we are removing the last item + if (remove.PrunedTopLevelMenu) + { + // if we deleted the last menu item + if (remove.Bar?.Menus.Length == 0) + { + remove.Bar.CloseMenu(false); + return true; + } + + // convert keystroke to left, + // so we move to the next menu + ChangeKeyTo(keystroke, Key.CursorLeft); + return false; + } + + // otherwise convert keystroke to up + // so that focus now sits nicely on the + // menu item above the deleted one + ChangeKeyTo(keystroke, Key.CursorUp); + return false; + } + } + + // Allow typing but also Enter to create a new sub-item + if ( !this.IsActionableKey( keystroke ) ) + { + return false; + } + + // TODO: This probably lets us edit the Editors own context menus lol + + // TODO once https://github.com/migueldeicaza/gui.cs/pull/1689 is merged and published + // we can integrate this into the Design undo/redo systems + if (this.ApplyKeystrokeToString(menuItem.Title.ToString() ?? string.Empty, keystroke, out var newValue)) + { + // convert to a separator by typing three hyphens + if (newValue.Equals("---")) + { + if (OperationManager.Instance.Do( + new ConvertMenuItemToSeperatorOperation(menuItem))) + { + return true; + } + } + else + { + // changing the title + menuItem.Title = newValue; + } + + focusedView.SetNeedsDisplay(); + + return true; + } + + return false; + } + + private void ChangeKeyTo(Key keystroke, Key newKey) + { + Type t = typeof(Key); + var p = t.GetProperty("KeyCode") ?? throw new Exception("Property somehow doesn't exist"); + p.SetValue(keystroke, newKey.KeyCode); + } + + private void StartOperation(Design d) + { + // these can already handle editing themselves + if (d.View is DateField || d.View is TextField || d.View is TextView) + { + return; + } + + var textProp = d.GetDesignableProperty("Text"); + + if (textProp != null) + { + this.currentOperation = new SetPropertyOperation(d, textProp, d.View.Text, d.View.Text); + } + } + + private void FinishOperation() + { + if (this.currentOperation == null) + { + return; + } + + // finish it and clear it + OperationManager.Instance.Do(this.currentOperation); + this.currentOperation = null; + } + + private bool ApplyKeystrokeToTextProperty(Key keystroke) + { + if (this.currentOperation == null || this.currentOperation.Designs.Count != 1) + { + return false; + } + + var design = this.currentOperation.Designs.Single(); + + var str = design.View.GetActualText(); + + if (!this.ApplyKeystrokeToString(str, keystroke, out var newStr)) + { + // not a keystroke we can act upon + return false; + } + + design.View.SetActualText(newStr); + design.View.SetNeedsDisplay(); + this.currentOperation.NewValue = newStr; + + return true; + } + + private bool ApplyKeystrokeToString(string? str, Key keystroke, out string newString) + { + newString = str; + + if (keystroke == Key.Backspace) + { + // no change + if ( string.IsNullOrEmpty( str ) ) + { + return false; + } + + // chop off a letter + newString = str.Length == 1 ? string.Empty : str.Substring(0, str.Length - 1); + return true; + } + + var ch = KeyToLetter(keystroke); + + + + newString += ch; + + return true; + } + + private char KeyToLetter(Key keystroke) + { + if(keystroke == Key.Space) + { + return ' '; + } + + var ch = (char)Key.ToRune(keystroke.KeyCode).Value; + + if(ch >= 'A' && ch <= 'Z' && !keystroke.IsShift) + { + return char.ToLower(ch); + } + + return ch; + } + + private bool IsActionableKey(Key keystroke) + { + + if (keystroke == Key.Backspace) + { + return true; + } + if(keystroke == Key.Delete || keystroke.KeyCode == KeyCode.ShiftMask) + { + return false; + } + // Don't let Ctrl+Q add a Q! + if (keystroke.IsCtrl) + { + return false; + } + + if (keystroke >= Key.A && keystroke <= Key.Z) { + return true; + } + + var punctuation = "\"\\/':;%^&*~`!@#.,? ()-+{}<>=_][|"; + + var ch = KeyToLetter(keystroke); + + + + return punctuation.Contains(ch) || char.IsLetterOrDigit(ch); + } + } \ No newline at end of file diff --git a/src/UI/MouseManager.cs b/src/UI/MouseManager.cs index ddb78845..0144ad58 100644 --- a/src/UI/MouseManager.cs +++ b/src/UI/MouseManager.cs @@ -28,7 +28,7 @@ public class MouseManager /// /// Gets the current 'drag a box' selection area that is ongoing (if any). /// - public Rect? SelectionBox => RectExtensions.FromBetweenPoints(this.selectionStart, this.selectionEnd); + public Rectangle? SelectionBox => RectExtensions.FromBetweenPoints(this.selectionStart, this.selectionEnd); /// /// Responds to (by changing a 'drag a box' selection area @@ -72,7 +72,7 @@ public void HandleMouse(MouseEvent m, Design viewBeingEdited) { // start dragging a selection box this.selectionContainer = drag; - this.selectionStart = new Point(m.X, m.Y); + this.selectionStart = m.Position; } // if nothing is going on yet @@ -81,7 +81,7 @@ public void HandleMouse(MouseEvent m, Design viewBeingEdited) { var parent = drag.SuperView; - var dest = parent.ScreenToView(m.X, m.Y); + var dest = parent.ScreenToContent(m.Position); if (isLowerRight) { @@ -121,22 +121,25 @@ public void HandleMouse(MouseEvent m, Design viewBeingEdited) if (m.Flags.HasFlag(MouseFlags.Button1Pressed) && this.selectionStart != null) { // move selection box to new mouse position - this.selectionEnd = new Point(m.X, m.Y); + this.selectionEnd = m.Position; viewBeingEdited.View.SetNeedsDisplay(); - Application.DoEvents(); + + // BUG: Method is gone, will this functionality work still without it? + // Application.DoEvents(); return; } // continue dragging a view if (m.Flags.HasFlag(MouseFlags.Button1Pressed) && this.dragOperation?.BeingDragged.View?.SuperView != null) { - var dest = this.dragOperation?.BeingDragged.View.SuperView.ScreenToView(m.X, m.Y); + var dest = this.dragOperation?.BeingDragged.View.SuperView.ScreenToContent(m.Position); if (dest != null && this.dragOperation != null) { this.dragOperation.ContinueDrag(dest.Value); viewBeingEdited.View.SetNeedsDisplay(); - Application.DoEvents(); + // BUG: Method is gone, will this functionality work still without it? + // Application.DoEvents(); } } @@ -145,12 +148,13 @@ public void HandleMouse(MouseEvent m, Design viewBeingEdited) && this.resizeOperation != null && this.resizeOperation.BeingResized.View.SuperView != null) { - var dest = this.resizeOperation.BeingResized.View.SuperView.ScreenToView(m.X, m.Y); + var dest = this.resizeOperation.BeingResized.View.SuperView.ScreenToContent(m.Position); this.resizeOperation.ContinueResize(dest); viewBeingEdited.View.SetNeedsDisplay(); - Application.DoEvents(); + // BUG: Method is gone, will this functionality work still without it? + // Application.DoEvents(); } // end things (because mouse released) @@ -171,7 +175,8 @@ public void HandleMouse(MouseEvent m, Design viewBeingEdited) this.selectionEnd = null; this.selectionContainer = null; viewBeingEdited.View.SetNeedsDisplay(); - Application.DoEvents(); + // BUG: Method is gone, will this functionality work still without it? + // Application.DoEvents(); } // end dragging diff --git a/src/UI/ValueFactory.cs b/src/UI/ValueFactory.cs new file mode 100644 index 00000000..eda75e2d --- /dev/null +++ b/src/UI/ValueFactory.cs @@ -0,0 +1,421 @@ +using System.Collections; +using System.Text; +using Terminal.Gui.TextValidateProviders; +using Terminal.Gui; +using TerminalGuiDesigner.ToCode; +using TerminalGuiDesigner.UI.Windows; +using ColorPicker = TerminalGuiDesigner.UI.Windows.ColorPicker; +using Attribute = Terminal.Gui.Attribute; + +namespace TerminalGuiDesigner.UI +{ + static class ValueFactory + { + internal static bool GetNewValue(string propertyName, Design design, Type type, object? oldValue, out object? newValue, bool allowMultiLine) + { + newValue = null; + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(SliderOption<>)) + { + var designer = new SliderOptionEditor(type.GetGenericArguments()[0], oldValue); + Application.Run(designer); + + if (!designer.Cancelled) + { + newValue = designer.Result; + return true; + } + else + { + // user canceled designing the Option + return false; + } + } + else + if (type== typeof(ColorScheme)) + { + return GetNewColorSchemeValue(design, out newValue); + } + else + if (type == typeof(Attribute) || + type == typeof(Attribute?)) + { + // if its an Attribute or nullableAttribute + var picker = new ColorPicker((Attribute?)oldValue); + Application.Run(picker); + + if (!picker.Cancelled) + { + newValue = picker.Result; + return true; + } + else + { + // user cancelled designing the Color + newValue = null; + return false; + } + } + else + if (type== typeof(ITextValidateProvider)) + { + string? oldPattern = oldValue is TextRegexProvider r ? (string?)r.Pattern.ToPrimitive() : null; + if (Modals.GetString("New Validation Pattern", "Regex Pattern", oldPattern, out var newPattern)) + { + newValue = string.IsNullOrWhiteSpace(newPattern) ? null : new TextRegexProvider(newPattern); + return true; + } + + // user cancelled entering a pattern + newValue = null; + return false; + } + else + if (type== typeof(Pos)) + { + // user is editing a Pos + var designer = new PosEditor(design, (Pos)oldValue?? throw new Exception("Pos property was unexpectedly null")); + + Application.Run(designer); + + if (!designer.Cancelled) + { + newValue = designer.Result; + return true; + } + else + { + // user cancelled designing the Pos + newValue = null; + return false; + } + } + else + if (type== typeof(Size)) + { + // user is editing a Size + var oldSize = (Size)(oldValue ?? throw new Exception($"Property {propertyName} is of Type Size but it's current value is null")); + var designer = new SizeEditor(oldSize); + + Application.Run(designer); + + if (!designer.Cancelled) + { + newValue = designer.Result; + return true; + } + else + { + // user cancelled designing the Pos + newValue = null; + return false; + } + } + else + if (type== typeof(PointF)) + { + // user is editing a PointF + var oldPointF = (PointF)(oldValue ?? throw new Exception($"Property {propertyName} is of Type PointF but it's current value is null")); + var designer = new PointEditor(oldPointF.X, oldPointF.Y); + + Application.Run(designer); + + if (!designer.Cancelled) + { + newValue = new PointF(designer.ResultX, designer.ResultY); + return true; + } + else + { + // user cancelled designing the Pos + newValue = null; + return false; + } + } + else + if (type== typeof(Dim)) + { + // user is editing a Dim + var designer = new DimEditor(design, (Dim)oldValue); + Application.Run(designer); + + if (!designer.Cancelled) + { + newValue = designer.Result; + return true; + } + else + { + // user cancelled designing the Dim + newValue = null; + return false; + } + } + else + if (type== typeof(bool)) + { + int answer = ChoicesDialog.Query(propertyName, $"New value for {type}", "Yes", "No"); + + newValue = answer == 0 ? true : false; + return answer != -1; + } + else + if ( + type.IsGenericType(typeof(IEnumerable<>)) || + type.IsAssignableTo(typeof(IList)) + ) + { + var elementType = type.GetElementTypeEx() + ?? throw new Exception($"Property {propertyName} was array but had no element type"); ; + + if (elementType.IsValueType || elementType == typeof(string)) + { + if (Modals.GetArray( + propertyName, + "New Array Value", + type.GetElementType() ?? throw new Exception("Property was an Array but GetElementType returned null"), + (Array?)oldValue, + out Array? resultArray)) + { + newValue = resultArray; + return true; + } + } + else + { + var designer = new ArrayEditor(design,type.GetElementTypeEx(), (IList)oldValue); + Application.Run(designer); + + if (!designer.Cancelled) + { + newValue = designer.Result; + return true; + } + else + { + // user cancelled designing the Dim + newValue = null; + return false; + } + } + + } + else + if (type== typeof(IListDataSource)) + { + // TODO : Make this work with non strings e.g. + // if user types a bunch of numbers in or dates + var oldValueAsArrayOfStrings = oldValue == null ? + Array.Empty() : + ((IListDataSource)oldValue).ToList() + .Cast() + .Select(o => o?.ToString()) + .ToArray(); + + if (Modals.TryGetArray( + propertyName, + "New List Value", + oldValueAsArrayOfStrings, + out Array? resultArray)) + { + newValue = resultArray; + return true; + } + } + else + if (type.IsEnum) + { + if (Modals.GetEnum(propertyName, "New Enum Value", type, (Enum?)oldValue, out var resultEnum)) + { + newValue = resultEnum; + return true; + } + } + else + if (type== typeof(int) + || type== typeof(int?) + || type== typeof(uint) + || type== typeof(uint?)) + { + // deals with null, int and uint + var v = oldValue == null ? null : (int?)Convert.ToInt32(oldValue); + + if (Modals.GetInt(propertyName, "New Int Value", v, out var resultInt)) + { + // change back to uint/int/null + newValue = resultInt == null ? null : Convert.ChangeType(resultInt, type); + return true; + } + } + else + if (type== typeof(int) + || type== typeof(int?)) + { + if (Modals.Getint(propertyName, "New int Value", (int?)oldValue, out var resultInt)) + { + newValue = resultInt; + return true; + } + } + else + if (type== typeof(char?) + || type== typeof(char)) + { + if (Modals.GetChar(propertyName, "New Single Character", oldValue is null ? null : (char?)oldValue.ToPrimitive() ?? null, out var resultChar)) + { + newValue = resultChar; + return true; + } + } + else + if (type== typeof(Rune) + || type== typeof(Rune?)) + { + if (Modals.GetChar(propertyName, "New Single Character", oldValue is null ? null : (char?)oldValue.ToPrimitive() ?? null, out var resultChar)) + { + newValue = resultChar == null ? null : new Rune(resultChar.Value); + return true; + } + } + else + if (type == typeof(FileSystemInfo)) + { + var fd = new FileDialog(); + fd.AllowsMultipleSelection = false; + + + int answer = ChoicesDialog.Query(propertyName, $"Directory or File?", "Directory", "File", "Cancel"); + + if (answer < 0 || answer >= 2) + { + return false; + } + bool pickDir = answer == 0; + + fd.OpenMode = pickDir ? OpenMode.Directory : OpenMode.File; + + Application.Run(fd); + if (fd.Canceled || string.IsNullOrWhiteSpace(fd.Path)) + { + return false; + } + else + { + newValue = pickDir ? + new DirectoryInfo(fd.Path) : new FileInfo(fd.Path); + return true; + } + } + else + if (Modals.GetString(propertyName, "New String Value", oldValue?.ToString(), out var result, allowMultiLine)) + { + newValue = result; + return true; + } + + newValue = null; + return false; + } + internal static bool GetNewValue(Design design, Property property, object? oldValue, out object? newValue) + { + if (property is InstanceOfProperty inst) + { + if (Modals.Get( + property.PropertyInfo.Name, + "New Value", + typeof(Label).Assembly.GetTypes().Where(inst.MustBeDerrivedFrom.IsAssignableFrom).ToArray(), + inst.GetValue()?.GetType(), + out Type? typeChosen)) + { + if (typeChosen == null) + { + newValue = null; + return false; + } + + newValue = Activator.CreateInstance(typeChosen); + return true; + } + + // User cancelled dialog + newValue = null; + return false; + } + else + { + return GetNewValue(property.PropertyInfo.Name, design, property.PropertyInfo.PropertyType,oldValue, out newValue, ValueFactory.AllowMultiLine(property)); + } + + } + public static bool AllowMultiLine(Property property) + { + // for the text editor control let them put multiple lines in + if (property.PropertyInfo.Name.Equals("Text") && property.Design.View is TextView tv && tv.Multiline) + { + return true; + } + + return false; + } + + private static bool GetNewColorSchemeValue(Design design, out object? newValue) + { + const string custom = "Edit Color Schemes..."; + List offer = new(); + + var defaults = new DefaultColorSchemes(); + var schemes = ColorSchemeManager.Instance.Schemes.ToList(); + + offer.AddRange(schemes); + + foreach (var d in defaults.GetDefaultSchemes()) + { + // user is already explicitly using this default and may even have modified it + if (offer.OfType().Any(s => s.Name.Equals(d.Name))) + { + continue; + } + + offer.Add(d); + } + + // add the option to jump to custom colors + offer.Add(custom); + + if (Modals.Get("Color Scheme", "Ok", offer.ToArray(), design.View.ColorScheme, out var selected)) + { + // if user clicked "Custom..." + if (selected is string s && string.Equals(s, custom)) + { + // show the custom colors dialog + var colorSchemesUI = new ColorSchemesUI(design); + Application.Run(colorSchemesUI); + newValue = null; + return false; + } + + if (selected is NamedColorScheme ns) + { + newValue = ns.Scheme; + + // if it was a default one, tell ColorSchemeManager we are now using it + if (!schemes.Contains(ns)) + { + ColorSchemeManager.Instance.AddOrUpdateScheme(ns.Name, ns.Scheme, design.GetRootDesign()); + } + + return true; + } + + newValue = null; + return false; + } + else + { + // user cancelled selecting scheme + newValue = null; + return false; + } + } + } +} diff --git a/src/UI/Windows/ArrayEditor.Designer.cs b/src/UI/Windows/ArrayEditor.Designer.cs new file mode 100644 index 00000000..7a388dbc --- /dev/null +++ b/src/UI/Windows/ArrayEditor.Designer.cs @@ -0,0 +1,167 @@ + +//------------------------------------------------------------------------------ + +// +// This code was generated by: +// TerminalGuiDesigner v1.1.0.0 +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ----------------------------------------------------------------------------- + +using System.Collections.ObjectModel; + +namespace TerminalGuiDesigner.UI.Windows { + using System; + using Terminal.Gui; + using System.Collections; + using System.Collections.Generic; + + + public partial class ArrayEditor : Terminal.Gui.Dialog { + + private Terminal.Gui.FrameView frameView; + + private Terminal.Gui.ListView lvElements; + + private Terminal.Gui.Button btnAddElement; + + private Terminal.Gui.Button btnDelete; + + private Terminal.Gui.Button btnMoveUp; + + private Terminal.Gui.Button btnMoveDown; + + private Terminal.Gui.Button btnEdit; + + private Terminal.Gui.LineView lineView; + + private Terminal.Gui.Button btnOk; + + private Terminal.Gui.Button btnCancel; + + private void InitializeComponent() { + this.btnCancel = new Terminal.Gui.Button(); + this.btnOk = new Terminal.Gui.Button(); + this.lineView = new Terminal.Gui.LineView(); + this.btnEdit = new Terminal.Gui.Button(); + this.btnMoveDown = new Terminal.Gui.Button(); + this.btnMoveUp = new Terminal.Gui.Button(); + this.btnDelete = new Terminal.Gui.Button(); + this.btnAddElement = new Terminal.Gui.Button(); + this.lvElements = new Terminal.Gui.ListView(); + this.frameView = new Terminal.Gui.FrameView(); + this.Width = Dim.Percent(85); + this.Height = Dim.Percent(85); + this.X = Pos.Center(); + this.Y = Pos.Center(); + this.Visible = true; + this.Modal = true; + this.TextAlignment = Terminal.Gui.Alignment.Start; + this.Title = "Array Editor"; + this.frameView.Width = Dim.Fill(0); + this.frameView.Height = Dim.Fill(3); + this.frameView.X = 0; + this.frameView.Y = 0; + this.frameView.Visible = true; + this.frameView.Data = "frameView"; + this.frameView.TextAlignment = Terminal.Gui.Alignment.Start; + this.frameView.Title = "Elements"; + this.Add(this.frameView); + this.lvElements.Width = Dim.Fill(0); + this.lvElements.Height = Dim.Fill(0); + this.lvElements.X = 1; + this.lvElements.Y = 0; + this.lvElements.Visible = true; + this.lvElements.Data = "lvElements"; + this.lvElements.TextAlignment = Terminal.Gui.Alignment.Start; + this.lvElements.Source = new Terminal.Gui.ListWrapper(new ObservableCollection(new string[] { + "Item1", + "Item2", + "Item3"})); + this.lvElements.AllowsMarking = false; + this.lvElements.AllowsMultipleSelection = true; + this.frameView.Add(this.lvElements); + this.btnAddElement.Width = 8; + this.btnAddElement.Height = 1; + this.btnAddElement.X = 1; + this.btnAddElement.Y = Pos.AnchorEnd(3); + this.btnAddElement.Visible = true; + this.btnAddElement.Data = "btnAddElement"; + this.btnAddElement.Text = "Add"; + this.btnAddElement.TextAlignment = Terminal.Gui.Alignment.Center; + this.btnAddElement.IsDefault = false; + this.Add(this.btnAddElement); + this.btnDelete.Width = 8; + this.btnDelete.Height = 1; + this.btnDelete.X = 9; + this.btnDelete.Y = Pos.AnchorEnd(3); + this.btnDelete.Visible = true; + this.btnDelete.Data = "btnDelete"; + this.btnDelete.Text = "Delete"; + this.btnDelete.TextAlignment = Terminal.Gui.Alignment.Center; + this.btnDelete.IsDefault = false; + this.Add(this.btnDelete); + this.btnMoveUp.Width = 8; + this.btnMoveUp.Height = 1; + this.btnMoveUp.X = 20; + this.btnMoveUp.Y = Pos.AnchorEnd(3); + this.btnMoveUp.Visible = true; + this.btnMoveUp.Data = "btnMoveUp"; + this.btnMoveUp.Text = "Move Up"; + this.btnMoveUp.TextAlignment = Terminal.Gui.Alignment.Center; + this.btnMoveUp.IsDefault = false; + this.Add(this.btnMoveUp); + this.btnMoveDown.Width = 8; + this.btnMoveDown.Height = 1; + this.btnMoveDown.X = 32; + this.btnMoveDown.Y = Pos.AnchorEnd(3); + this.btnMoveDown.Visible = true; + this.btnMoveDown.Data = "btnMoveDown"; + this.btnMoveDown.Text = "Move Down"; + this.btnMoveDown.TextAlignment = Terminal.Gui.Alignment.Center; + this.btnMoveDown.IsDefault = false; + this.Add(this.btnMoveDown); + this.btnEdit.Width = 8; + this.btnEdit.Height = 1; + this.btnEdit.X = 46; + this.btnEdit.Y = Pos.AnchorEnd(3); + this.btnEdit.Visible = true; + this.btnEdit.Data = "btnEdit"; + this.btnEdit.Text = "Edit"; + this.btnEdit.TextAlignment = Terminal.Gui.Alignment.Center; + this.btnEdit.IsDefault = false; + this.Add(this.btnEdit); + this.lineView.Width = Dim.Fill(1); + this.lineView.Height = 1; + this.lineView.X = -1; + this.lineView.Y = Pos.AnchorEnd(2); + this.lineView.Visible = true; + this.lineView.Data = "lineView"; + this.lineView.TextAlignment = Terminal.Gui.Alignment.Start; + this.lineView.LineRune = new System.Text.Rune('─'); + this.lineView.Orientation = Terminal.Gui.Orientation.Horizontal; + this.Add(this.lineView); + this.btnOk.Width = 8; + this.btnOk.Height = 1; + this.btnOk.X = 0; + this.btnOk.Y = Pos.AnchorEnd(1); + this.btnOk.Visible = true; + this.btnOk.Data = "btnOk"; + this.btnOk.Text = "Ok"; + this.btnOk.TextAlignment = Terminal.Gui.Alignment.Center; + this.btnOk.IsDefault = false; + this.Add(this.btnOk); + this.btnCancel.Width = 8; + this.btnCancel.Height = 1; + this.btnCancel.X = 9; + this.btnCancel.Y = Pos.AnchorEnd(1); + this.btnCancel.Visible = true; + this.btnCancel.Data = "btnCancel"; + this.btnCancel.Text = "Cancel"; + this.btnCancel.TextAlignment = Terminal.Gui.Alignment.Center; + this.btnCancel.IsDefault = false; + this.Add(this.btnCancel); + } + } +} diff --git a/src/UI/Windows/ArrayEditor.cs b/src/UI/Windows/ArrayEditor.cs new file mode 100644 index 00000000..5d493ef7 --- /dev/null +++ b/src/UI/Windows/ArrayEditor.cs @@ -0,0 +1,168 @@ + +//------------------------------------------------------------------------------ + +// +// This code was generated by: +// TerminalGuiDesigner v1.1.0.0 +// You can make changes to this file and they will not be overwritten when saving. +// +// ----------------------------------------------------------------------------- + +using System.Collections.ObjectModel; + +namespace TerminalGuiDesigner.UI.Windows { + using System.Collections; + using Terminal.Gui; + using TerminalGuiDesigner.ToCode; + + public partial class ArrayEditor { + + /// + /// True if the editing was aborted. + /// + public bool Cancelled { get; private set; } = true; + + private readonly Design design; + private Type elementType; + + /// + /// The new array + /// + public IList Result { get; private set; } + + /// + /// Creates a new instance of the editor configured to build lists of + /// and showing initial values held in (if any). + /// + /// + /// + /// + public ArrayEditor(Design design, Type elementType, IList oldValue) { + InitializeComponent(); + this.design = design; + this.elementType = elementType; + + Type listType = typeof(List<>).MakeGenericType(elementType); + Result = (IList)Activator.CreateInstance(listType); + + foreach(var e in oldValue) + { + Result.Add(e); + } + + lvElements.Source = Result.ToListDataSource(); + lvElements.KeyDown += LvElements_KeyDown; + btnOk.Accept += BtnOk_Clicked; + btnCancel.Accept += BtnCancel_Clicked; + btnAddElement.Accept += BtnAddElement_Clicked; + btnDelete.Accept += (s, e) => DeleteSelectedItem(); + btnMoveDown.Accept += BtnMoveDown_Clicked; + btnMoveUp.Accept += BtnMoveUp_Clicked; + btnEdit.Accept += BtnEdit_Clicked; + } + + + private void BtnMoveUp_Clicked(object sender, EventArgs e) + { + // Moving up means reducing the index by 1 + var idx = lvElements.SelectedItem; + + if (idx >= 1 && idx < Result.Count) + { + var toMove = Result[idx]; + var newIndex = idx - 1; + Result.RemoveAt(idx); + Result.Insert(newIndex, toMove); + + lvElements.Source = Result.ToListDataSource(); + lvElements.SelectedItem = newIndex; + lvElements.SetNeedsDisplay(); + } + } + + private void BtnMoveDown_Clicked(object sender, EventArgs e) + { + // Moving up means increasing the index by 1 + var idx = lvElements.SelectedItem; + + if (idx >= 0 && idx < Result.Count-1) + { + var toMove = Result[idx]; + var newIndex = idx + 1; + Result.RemoveAt(idx); + Result.Insert(newIndex, toMove); + + lvElements.Source = Result.ToListDataSource(); + lvElements.SelectedItem = newIndex; + lvElements.SetNeedsDisplay(); + } + } + + private void LvElements_KeyDown(object sender, Key e) + { + if(e == Key.DeleteChar) + { + DeleteSelectedItem(); + e.Handled = true; + } + } + + private void DeleteSelectedItem() + { + var idx = lvElements.SelectedItem; + + if (idx >= 0 && idx < Result.Count) + { + Result.RemoveAt(idx); + + lvElements.Source = Result.ToListDataSource(); + lvElements.SetNeedsDisplay(); + lvElements.SelectedItem = 0; + } + } + + private void BtnAddElement_Clicked(object sender, EventArgs e) + { + if(ValueFactory.GetNewValue("Element Value", design, this.elementType,null, out var newValue,true)) + { + Result.Add(newValue); + } + + lvElements.Source = Result.ToListDataSource(); + lvElements.SelectedItem = Result.Count - 1; + lvElements.SetNeedsDisplay(); + } + private void BtnEdit_Clicked(object sender, EventArgs e) + { + var idx = lvElements.SelectedItem; + + if (idx >= 0 && idx < Result.Count) + { + var toEdit = Result[idx]; + + if (ValueFactory.GetNewValue("Element Value", design, this.elementType, toEdit, out var newValue, true)) + { + // Replace old with new + Result.RemoveAt(idx); + Result.Insert(idx, newValue); + } + + lvElements.Source = Result.ToListDataSource(); + lvElements.SelectedItem = idx; + lvElements.SetNeedsDisplay(); + } + } + + private void BtnCancel_Clicked(object sender, EventArgs e) + { + Cancelled = true; + Application.RequestStop(); + } + + private void BtnOk_Clicked(object sender, EventArgs e) + { + Cancelled = false; + Application.RequestStop(); + } + } +} diff --git a/src/UI/Windows/BigListBox.cs b/src/UI/Windows/BigListBox.cs index 4571934a..aabb9429 100644 --- a/src/UI/Windows/BigListBox.cs +++ b/src/UI/Windows/BigListBox.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Collections.ObjectModel; using Terminal.Gui; namespace TerminalGuiDesigner.UI.Windows; @@ -10,31 +11,30 @@ namespace TerminalGuiDesigner.UI.Windows; /// Class Type for the objects the user is to pick from. public class BigListBox { - private readonly string okText; - private readonly bool addSearch; - private readonly string prompt; - private IList> collection; + private static readonly string[ ] ErrorStringArray = new[] { "Error" }; + private readonly object taskCancellationLock = new(); + private readonly ConcurrentBag cancelFiltering = new(); + private readonly Window win; + private readonly ListView listView; + private readonly bool addNull; + private readonly TextField? searchBox; + private readonly object callback; /// /// If the public constructor was used then this is the fixed list we were initialized with. /// - private IList publicCollection; + private readonly IList publicCollection; + + private IList> collection; - private bool addNull; /// /// Ongoing filtering of a large collection should be cancelled when the user changes the filter even if it is not completed yet. /// - private ConcurrentBag cancelFiltering = new ConcurrentBag(); - private object taskCancellationLock = new object(); - private Window win; - private ListView listView; private bool changes; - private TextField? searchBox; private DateTime lastKeypress = DateTime.Now; - private object callback; private bool okClicked = false; /// @@ -50,30 +50,22 @@ public class BigListBox public BigListBox( string prompt, string okText, - bool addSearch, + in bool addSearch, IList collection, Func displayMember, bool addNull, T? currentSelection) { - this.okText = okText; - this.addSearch = addSearch; - this.prompt = prompt; - this.AspectGetter = displayMember ?? (arg => arg?.ToString() ?? string.Empty); - if (collection == null) - { - throw new ArgumentNullException("collection"); - } - - this.publicCollection = collection; + this.publicCollection = collection ?? throw new ArgumentNullException( nameof( collection ) ); this.addNull = addNull; - this.win = new Window(this.prompt) + this.win = new Window() { X = 0, Y = 0, + Title = prompt, // By using Dim.Fill(), it will automatically resize without manual intervention Width = Dim.Fill(), @@ -81,46 +73,59 @@ public BigListBox( Modal = true, }; - this.listView = new ListView(new List(new[] { "Error" })) + this.listView = new ListView() { X = 0, Y = 0, Height = Dim.Fill(2), Width = Dim.Fill(2), + SelectedItem = 0 }; - this.listView.KeyPress += this.ListView_KeyPress; + listView.SetSource(new ObservableCollection(ErrorStringArray)); + + this.listView.KeyDown += this.ListView_KeyPress; this.listView.MouseClick += this.ListView_MouseClick; - this.listView.SetSource((this.collection = this.BuildList(this.GetInitialSource())).ToList()); + + this.collection = this.BuildList(this.GetInitialSource()).ToList(); + + this.listView.SetSource( + new ObservableCollection(this.collection.Select(o=>o.Object).ToArray()) + ); this.win.Add(this.listView); - var btnOk = new Button(this.okText, true) + var btnOk = new Button() { + Text = okText, + IsDefault = true, Y = Pos.Bottom(this.listView), }; - btnOk.Clicked += () => + btnOk.Accept += (s, e) => { this.Accept(); }; - var btnCancel = new Button("Cancel") + var btnCancel = new Button() { + Text = "Cancel", Y = Pos.Bottom(this.listView), }; - btnCancel.Clicked += () => Application.RequestStop(); + btnCancel.Accept += (s, e) => Application.RequestStop(); - if (this.addSearch) + if (addSearch) { - var searchLabel = new Label("Search:") + var searchLabel = new Label() { + Text = "Search:", X = 0, Y = Pos.Bottom(this.listView), }; this.win.Add(searchLabel); - this.searchBox = new TextField(string.Empty) + this.searchBox = new TextField() { + Text = string.Empty, X = Pos.Right(searchLabel), Y = Pos.Bottom(this.listView), Width = 30, @@ -132,7 +137,7 @@ public BigListBox( this.win.Add(this.searchBox); this.searchBox.SetFocus(); - this.searchBox.TextChanged += (s) => + this.searchBox.TextChanged += (s, e) => { // Don't update the UI while user is hammering away on the keyboard this.lastKeypress = DateTime.Now; @@ -148,13 +153,21 @@ public BigListBox( this.win.Add(btnOk); this.win.Add(btnCancel); - this.SetCurrentSelection(currentSelection); + if (currentSelection != null) + { + this.SetCurrentSelection(currentSelection); + } + else + { + this.listView.SelectedItem = 0; + } - this.callback = Application.MainLoop.AddTimeout(TimeSpan.FromMilliseconds(100), this.Timer); + this.callback = Application.AddTimeout(TimeSpan.FromMilliseconds(100), this.Timer); - this.listView.FocusFirst(); + this.listView.FocusFirst(TabBehavior.TabStop); } + /// /// Gets the value the user chose upon exiting. /// @@ -173,24 +186,25 @@ public bool ShowDialog() { Application.Run(this.win); - Application.MainLoop.RemoveTimeout(this.callback); + Application.RemoveTimeout(this.callback); return this.okClicked; } private void Accept() { - if (this.listView.SelectedItem >= this.collection.Count) + var selected = this.listView.SelectedItem; + if (selected < 0 || selected >= this.collection.Count) { return; } this.okClicked = true; Application.RequestStop(); - this.Selected = this.collection[this.listView.SelectedItem].Object; + this.Selected = this.collection[selected].Object; } - private void ListView_MouseClick(View.MouseEventArgs obj) + private void ListView_MouseClick(object? sender, MouseEventEventArgs obj) { if (obj.MouseEvent.Flags.HasFlag(MouseFlags.Button1DoubleClicked)) { @@ -199,26 +213,31 @@ private void ListView_MouseClick(View.MouseEventArgs obj) } } - private void ListView_KeyPress(View.KeyEventEventArgs obj) + private void ListView_KeyPress(object? sender, Key key) { // if user types in some text change the focus to the text box to enable searching - var c = (char)obj.KeyEvent.KeyValue; + var c = (char)key; // backspace or letter/numbers - if (obj.KeyEvent.Key == Key.Backspace || char.IsLetterOrDigit(c)) + if (key == Key.Backspace || char.IsLetterOrDigit(c)) { - this.searchBox?.FocusFirst(); + this.searchBox?.FocusFirst(TabBehavior.TabStop); + this.searchBox?.NewKeyDownEvent(key); + key.Handled = true; } } - private bool Timer(MainLoop caller) + private bool Timer() { if (this.changes && DateTime.Now.Subtract(this.lastKeypress) > TimeSpan.FromMilliseconds(100)) { lock (this.taskCancellationLock) { var oldSelected = this.listView.SelectedItem; - this.listView.SetSource(this.collection.ToList()); + this.listView.SetSource( + new ObservableCollection( + this.collection.Select(l=>l.Object) + )); if (oldSelected < this.collection.Count) { diff --git a/src/UI/Windows/ChoicesDialog.Designer.cs b/src/UI/Windows/ChoicesDialog.Designer.cs index f0b0593f..ca55da23 100644 --- a/src/UI/Windows/ChoicesDialog.Designer.cs +++ b/src/UI/Windows/ChoicesDialog.Designer.cs @@ -38,31 +38,30 @@ private void InitializeComponent() { this.btn1 = new Terminal.Gui.Button(); this.buttonPanel = new Terminal.Gui.View(); this.label1 = new Terminal.Gui.Label(); - this.dialogBackground = new Terminal.Gui.ColorScheme(); - this.dialogBackground.Normal = new Terminal.Gui.Attribute(Terminal.Gui.Color.White, Terminal.Gui.Color.DarkGray); - this.dialogBackground.HotNormal = new Terminal.Gui.Attribute(Terminal.Gui.Color.White, Terminal.Gui.Color.DarkGray); - this.dialogBackground.Focus = new Terminal.Gui.Attribute(Terminal.Gui.Color.White, Terminal.Gui.Color.DarkGray); - this.dialogBackground.HotFocus = new Terminal.Gui.Attribute(Terminal.Gui.Color.White, Terminal.Gui.Color.DarkGray); - this.dialogBackground.Disabled = new Terminal.Gui.Attribute(Terminal.Gui.Color.Black, Terminal.Gui.Color.Black); - this.buttons = new Terminal.Gui.ColorScheme(); - this.buttons.Normal = new Terminal.Gui.Attribute(Terminal.Gui.Color.DarkGray, Terminal.Gui.Color.White); - this.buttons.HotNormal = new Terminal.Gui.Attribute(Terminal.Gui.Color.Black, Terminal.Gui.Color.White); - this.buttons.Focus = new Terminal.Gui.Attribute(Terminal.Gui.Color.Red, Terminal.Gui.Color.Brown); - this.buttons.HotFocus = new Terminal.Gui.Attribute(Terminal.Gui.Color.Black, Terminal.Gui.Color.Brown); - this.buttons.Disabled = new Terminal.Gui.Attribute(Terminal.Gui.Color.Black, Terminal.Gui.Color.Black); - this.Width = Dim.Percent(85f); - this.Height = Dim.Percent(85f); + this.dialogBackground = new Terminal.Gui.ColorScheme( + new Terminal.Gui.Attribute(Terminal.Gui.Color.White, Terminal.Gui.Color.DarkGray), + new Terminal.Gui.Attribute(Terminal.Gui.Color.White, Terminal.Gui.Color.DarkGray), + new Terminal.Gui.Attribute(Terminal.Gui.Color.White, Terminal.Gui.Color.DarkGray), + new Terminal.Gui.Attribute(Terminal.Gui.Color.Black, Terminal.Gui.Color.Black), + new Terminal.Gui.Attribute(Terminal.Gui.Color.White, Terminal.Gui.Color.DarkGray) + ); + this.buttons = new Terminal.Gui.ColorScheme( + new Terminal.Gui.Attribute(Terminal.Gui.Color.DarkGray, Terminal.Gui.Color.White), + new Terminal.Gui.Attribute(Terminal.Gui.Color.Red, Terminal.Gui.Color.Yellow), + new Terminal.Gui.Attribute(Terminal.Gui.Color.Black, Terminal.Gui.Color.White), + new Terminal.Gui.Attribute(Terminal.Gui.Color.Black, Terminal.Gui.Color.Black), + new Terminal.Gui.Attribute(Terminal.Gui.Color.Black, Terminal.Gui.Color.Yellow) + ); + + this.Width = Dim.Percent(85); + this.Height = Dim.Percent(85); this.X = Pos.Center(); this.Y = Pos.Center(); this.ColorScheme = this.dialogBackground; this.Modal = true; this.Text = ""; - this.Border.BorderStyle = Terminal.Gui.BorderStyle.Double; - this.Border.BorderBrush = Terminal.Gui.Color.Blue; - this.Border.Effect3D = true; - this.Border.Effect3DBrush = Terminal.Gui.Attribute.Make(Color.Black,Color.Black); - this.Border.DrawMarginFrame = true; - this.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.Border.BorderStyle = Terminal.Gui.LineStyle.Double; + this.TextAlignment = Terminal.Gui.Alignment.Start; this.Title = ""; this.label1.Width = Dim.Fill(0); this.label1.Height = Dim.Fill(0); @@ -70,7 +69,7 @@ private void InitializeComponent() { this.label1.Y = 1; this.label1.Data = "label1"; this.label1.Text = "lblMessage"; - this.label1.TextAlignment = Terminal.Gui.TextAlignment.Centered; + this.label1.TextAlignment = Terminal.Gui.Alignment.Center; this.Add(this.label1); this.buttonPanel.Width = 50; this.buttonPanel.Height = 2; @@ -78,7 +77,7 @@ private void InitializeComponent() { this.buttonPanel.Y = Pos.AnchorEnd(2); this.buttonPanel.Data = "buttonPanel"; this.buttonPanel.Text = ""; - this.buttonPanel.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.buttonPanel.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.buttonPanel); this.btn1.Width = 10; this.btn1.Height = 2; @@ -87,7 +86,7 @@ private void InitializeComponent() { this.btn1.ColorScheme = this.buttons; this.btn1.Data = "btn1"; this.btn1.Text = "btn1"; - this.btn1.TextAlignment = Terminal.Gui.TextAlignment.Centered; + this.btn1.TextAlignment = Terminal.Gui.Alignment.Center; this.btn1.IsDefault = true; this.buttonPanel.Add(this.btn1); this.btn2.Width = 9; @@ -97,7 +96,7 @@ private void InitializeComponent() { this.btn2.ColorScheme = this.buttons; this.btn2.Data = "btn2"; this.btn2.Text = "btn2"; - this.btn2.TextAlignment = Terminal.Gui.TextAlignment.Centered; + this.btn2.TextAlignment = Terminal.Gui.Alignment.Center; this.btn2.IsDefault = false; this.buttonPanel.Add(this.btn2); this.btn3.Width = 8; @@ -107,7 +106,7 @@ private void InitializeComponent() { this.btn3.ColorScheme = this.buttons; this.btn3.Data = "btn3"; this.btn3.Text = "btn3"; - this.btn3.TextAlignment = Terminal.Gui.TextAlignment.Centered; + this.btn3.TextAlignment = Terminal.Gui.Alignment.Center; this.btn3.IsDefault = false; this.buttonPanel.Add(this.btn3); this.btn4.Width = 8; @@ -117,7 +116,7 @@ private void InitializeComponent() { this.btn4.ColorScheme = this.buttons; this.btn4.Data = "btn4"; this.btn4.Text = "btn4"; - this.btn4.TextAlignment = Terminal.Gui.TextAlignment.Centered; + this.btn4.TextAlignment = Terminal.Gui.Alignment.Center; this.btn4.IsDefault = false; this.buttonPanel.Add(this.btn4); } diff --git a/src/UI/Windows/ChoicesDialog.cs b/src/UI/Windows/ChoicesDialog.cs index d0cdfe34..d91eb39d 100644 --- a/src/UI/Windows/ChoicesDialog.cs +++ b/src/UI/Windows/ChoicesDialog.cs @@ -7,8 +7,9 @@ // You can make changes to this file and they will not be overwritten when saving. // // ----------------------------------------------------------------------------- -namespace TerminalGuiDesigner.UI.Windows; -using NStack; +namespace TerminalGuiDesigner.UI.Windows; + +using System.Text; using Terminal.Gui; @@ -53,16 +54,17 @@ public ChoicesDialog(string title, string message, params string[] options) { buttons[i].Text = options[i] + " "; // TODO think it depends if it is default if we have to do this hack - buttons[i].Width = options[i].Length + 1; + buttons[i].Width = Dim.Auto(); + var i2 = i; - buttons[i].Clicked += () => { + buttons[i].Accept += (s,e) => { Result = i2; Application.RequestStop(); }; - buttons[i].DrawContentComplete += (r) => + buttons[i].DrawContentComplete += (s,r) => ChoicesDialog.PaintShadow(buttons[i2], ColorScheme); } @@ -79,8 +81,8 @@ public ChoicesDialog(string title, string message, params string[] options) { // align buttons bottom of dialog buttonPanel.Width = buttonWidth = buttons.Sum(b=>buttonPanel.Subviews.Contains(b) ? b.Frame.Width : 0) + 1; - - int maxWidthLine = TextFormatter.MaxWidthLine(message); + + int maxWidthLine = TextFormatter.GetSumMaxCharWidth(message); if (maxWidthLine > Application.Driver.Cols) { maxWidthLine = Application.Driver.Cols; @@ -89,27 +91,28 @@ public ChoicesDialog(string title, string message, params string[] options) { maxWidthLine = Math.Max(maxWidthLine, defaultWidth); - int textWidth = Math.Min(TextFormatter.MaxWidth(message, maxWidthLine), Application.Driver.Cols); - int textHeight = TextFormatter.MaxLines(message, textWidth) + 2; // message.Count (ustring.Make ('\n')) + 1; + int textWidth = Math.Min(TextFormatter.GetSumMaxCharWidth(message, maxWidthLine), Application.Driver.Cols); + int textHeight = message.Count (c=>c=='\n') + 4; int msgboxHeight = Math.Min(Math.Max(1, textHeight) + 4, Application.Driver.Rows); // textHeight + (top + top padding + buttons + bottom) - Width = Math.Min(Math.Max(maxWidthLine, Math.Max(Title.ConsoleWidth, Math.Max(textWidth + 2, buttonWidth))), Application.Driver.Cols); + Width = Math.Min(Math.Max(maxWidthLine, Math.Max(Title.GetColumns(), Math.Max(textWidth + 2, buttonWidth))), Application.Driver.Cols); Height = msgboxHeight; } /// - public override void Redraw(Rect bounds) + public override void OnDrawContentComplete(Rectangle bounds) { - base.Redraw(bounds); - - Move(1, 0, false); + base.OnDrawContentComplete(bounds); - var padding = ((bounds.Width - _title.Sum(v=>Rune.ColumnWidth(v))) / 2) - 1; + var screenTopLeft = FrameToScreen(); + Driver.Move(screenTopLeft.X+2, screenTopLeft.Y); + + var padding = ((bounds.Width - _title.EnumerateRunes().Sum(v=>v.GetColumns())) / 2) - 1; Driver.SetAttribute( new Attribute(ColorScheme.Normal.Foreground, ColorScheme.Normal.Background)); - Driver.AddStr(ustring.Make(Enumerable.Repeat(Driver.HDLine,padding))); + Driver.AddStr(string.Join("",Enumerable.Repeat(ConfigurationManager.Glyphs.HLineHv, padding))); Driver.SetAttribute( new Attribute(ColorScheme.Normal.Background, ColorScheme.Normal.Foreground)); @@ -117,7 +120,11 @@ public override void Redraw(Rect bounds) Driver.SetAttribute( new Attribute(ColorScheme.Normal.Foreground, ColorScheme.Normal.Background)); - Driver.AddStr(ustring.Make(Enumerable.Repeat(Driver.HDLine, padding))); + + StringBuilder sb = new StringBuilder(); + sb.Append(ConfigurationManager.Glyphs.HLineHv); + + Driver.AddStr(string.Join("", Enumerable.Repeat(ConfigurationManager.Glyphs.HLineHv.ToString(), padding))); } internal static int Query(string title, string message, params string[] options) @@ -129,7 +136,7 @@ internal static int Query(string title, string message, params string[] options) internal static void PaintShadow(Button btn, ColorScheme backgroundScheme) { - var bounds = btn.Bounds; + var bounds = btn.GetContentSize(); Attribute buttonColor = btn.HasFocus ? new Terminal.Gui.Attribute(btn.ColorScheme.Focus.Foreground, btn.ColorScheme.Focus.Background): @@ -139,13 +146,14 @@ internal static void PaintShadow(Button btn, ColorScheme backgroundScheme) if (btn.IsDefault) { - var rightDefault = new Rune(Driver != null ? Driver.RightDefaultIndicator : '>'); + var rightDefault = Driver != null ? ConfigurationManager.Glyphs.RightDefaultIndicator : new Rune('>'); // draw the 'end' button symbol one in btn.AddRune(bounds.Width - 3, 0, rightDefault); } - btn.AddRune(bounds.Width - 2, 0, ']'); + btn.AddRune(bounds.Width - 2, 0, new System.Text.Rune(']')); + btn.AddRune(0, 0, new System.Text.Rune('[')); var backgroundColor = backgroundScheme.Normal.Background; @@ -153,11 +161,11 @@ internal static void PaintShadow(Button btn, ColorScheme backgroundScheme) Driver.SetAttribute(new Terminal.Gui.Attribute(Color.Black, backgroundColor)); // end shadow (right) - btn.AddRune(bounds.Width - 1, 0, '▄'); + btn.AddRune(bounds.Width - 1, 0, new System.Text.Rune('▄')); // leave whitespace in lower left in parent/default background color Driver.SetAttribute(new Terminal.Gui.Attribute(Color.Black, backgroundColor)); - btn.AddRune(0, 1, ' '); + btn.AddRune(0, 1, new System.Text.Rune(' ')); // The color for rendering shadow is 'black' + parent/default background color Driver.SetAttribute(new Terminal.Gui.Attribute(backgroundColor, Color.Black)); @@ -165,7 +173,7 @@ internal static void PaintShadow(Button btn, ColorScheme backgroundScheme) // underline shadow for (int x = 1; x < bounds.Width; x++) { - btn.AddRune(x, 1, '▄'); + btn.AddRune(x, 1, new System.Text.Rune('▄')); } } diff --git a/src/UI/Windows/ColorPicker.Designer.cs b/src/UI/Windows/ColorPicker.Designer.cs index aac907cb..6ac2f7c0 100644 --- a/src/UI/Windows/ColorPicker.Designer.cs +++ b/src/UI/Windows/ColorPicker.Designer.cs @@ -3,151 +3,138 @@ // // This code was generated by: -// TerminalGuiDesigner v1.0.18.0 +// TerminalGuiDesigner v1.1.0.0 // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ----------------------------------------------------------------------------- -namespace TerminalGuiDesigner.UI.Windows; -using System; -using Terminal.Gui; - - -public partial class ColorPicker : Terminal.Gui.Dialog { - - private Terminal.Gui.Label lblForeground; - - private Terminal.Gui.Label lblBackground; - - private Terminal.Gui.Label lblResult; - - private Terminal.Gui.RadioGroup radiogroup1; - - private Terminal.Gui.RadioGroup radiogroup2; - - private Terminal.Gui.Label lblPreview; - - private Terminal.Gui.Button btnOk; +namespace TerminalGuiDesigner.UI.Windows { + using System; + using Terminal.Gui; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Drawing; - private Terminal.Gui.Button btnCancel; - private void InitializeComponent() { - this.btnCancel = new Terminal.Gui.Button(); - this.btnOk = new Terminal.Gui.Button(); - this.lblPreview = new Terminal.Gui.Label(); - this.radiogroup2 = new Terminal.Gui.RadioGroup(); - this.radiogroup1 = new Terminal.Gui.RadioGroup(); - this.lblResult = new Terminal.Gui.Label(); - this.lblBackground = new Terminal.Gui.Label(); - this.lblForeground = new Terminal.Gui.Label(); - this.Width = 52; - this.Height = 20; - this.X = Pos.Center(); - this.Y = Pos.Center(); - this.Modal = true; - this.Text = ""; - this.Border.BorderStyle = Terminal.Gui.BorderStyle.Single; - this.Border.Effect3D = true; - this.Border.DrawMarginFrame = true; - this.TextAlignment = Terminal.Gui.TextAlignment.Left; - this.Title = "Color Picker"; - this.lblForeground.Width = 11; - this.lblForeground.Height = 1; - this.lblForeground.X = 1; - this.lblForeground.Y = 0; - this.lblForeground.Data = "lblForeground"; - this.lblForeground.Text = "Foreground:"; - this.lblForeground.TextAlignment = Terminal.Gui.TextAlignment.Left; - this.Add(this.lblForeground); - this.lblBackground.Width = 11; - this.lblBackground.Height = 1; - this.lblBackground.X = 17; - this.lblBackground.Y = 0; - this.lblBackground.Data = "lblBackground"; - this.lblBackground.Text = "Background:"; - this.lblBackground.TextAlignment = Terminal.Gui.TextAlignment.Left; - this.Add(this.lblBackground); - this.lblResult.Width = 7; - this.lblResult.Height = 1; - this.lblResult.X = 38; - this.lblResult.Y = 0; - this.lblResult.Data = "lblResult"; - this.lblResult.Text = "Result:"; - this.lblResult.TextAlignment = Terminal.Gui.TextAlignment.Left; - this.Add(this.lblResult); - this.radiogroup1.Width = 17; - this.radiogroup1.Height = 16; - this.radiogroup1.X = 3; - this.radiogroup1.Y = 1; - this.radiogroup1.Data = "radiogroup1"; - this.radiogroup1.Text = ""; - this.radiogroup1.TextAlignment = Terminal.Gui.TextAlignment.Left; - this.radiogroup1.RadioLabels = new NStack.ustring[] { - "Black", - "Blue", - "Green", - "Cyan", - "Red", - "Magenta", - "Brown", - "Gray", - "DarkGray", - "BrightBlue", - "BrightGreen", - "BrightCyan", - "BrightRed", - "BrightMagenta", - "BrightYellow", - "White"}; - this.Add(this.radiogroup1); - this.radiogroup2.Width = 17; - this.radiogroup2.Height = 16; - this.radiogroup2.X = 20; - this.radiogroup2.Y = 1; - this.radiogroup2.Data = "radiogroup2"; - this.radiogroup2.Text = ""; - this.radiogroup2.TextAlignment = Terminal.Gui.TextAlignment.Left; - this.radiogroup2.RadioLabels = new NStack.ustring[] { - "Black", - "Blue", - "Green", - "Cyan", - "Red", - "Magenta", - "Brown", - "Gray", - "DarkGray", - "BrightBlue", - "BrightGreen", - "BrightCyan", - "BrightRed", - "BrightMagenta", - "BrightYellow", - "White"}; - this.Add(this.radiogroup2); - this.lblPreview.Width = 13; - this.lblPreview.Height = 1; - this.lblPreview.X = 37; - this.lblPreview.Y = 1; - this.lblPreview.Data = "lblPreview"; - this.lblPreview.Text = "\"Sample Text\""; - this.lblPreview.TextAlignment = Terminal.Gui.TextAlignment.Left; - this.Add(this.lblPreview); - this.btnOk.Width = 8; - this.btnOk.X = 10; - this.btnOk.Y = 17; - this.btnOk.Data = "btnOk"; - this.btnOk.Text = "Ok"; - this.btnOk.TextAlignment = Terminal.Gui.TextAlignment.Centered; - this.btnOk.IsDefault = true; - this.Add(this.btnOk); - this.btnCancel.Width = 10; - this.btnCancel.X = 28; - this.btnCancel.Y = 17; - this.btnCancel.Data = "btnCancel"; - this.btnCancel.Text = "Cancel"; - this.btnCancel.TextAlignment = Terminal.Gui.TextAlignment.Centered; - this.btnCancel.IsDefault = false; - this.Add(this.btnCancel); + public partial class ColorPicker : Terminal.Gui.Dialog { + + private Terminal.Gui.Label lblForeground; + + private Terminal.Gui.ColorPicker cpForeground; + + private Terminal.Gui.Label lblBackground; + + private Terminal.Gui.ColorPicker cpBackground; + + private Terminal.Gui.Label lblResult; + + private Terminal.Gui.Label lblPreview; + + private Terminal.Gui.Button btnOk; + + private Terminal.Gui.Button btnCancel; + + private void InitializeComponent() { + this.btnCancel = new Terminal.Gui.Button(); + this.btnOk = new Terminal.Gui.Button(); + this.lblPreview = new Terminal.Gui.Label(); + this.lblResult = new Terminal.Gui.Label(); + this.cpBackground = new Terminal.Gui.ColorPicker(); + this.lblBackground = new Terminal.Gui.Label(); + this.cpForeground = new Terminal.Gui.ColorPicker(); + this.lblForeground = new Terminal.Gui.Label(); + this.Width = Dim.Fill(2); + this.Height = 20; + this.X = Pos.Center(); + this.Y = Pos.Center(); + this.Visible = true; + this.Arrangement = Terminal.Gui.ViewArrangement.Movable; + this.Modal = true; + this.TextAlignment = Terminal.Gui.Alignment.Start; + this.Title = "Color Picker"; + this.lblForeground.Width = 11; + this.lblForeground.Height = 1; + this.lblForeground.X = 1; + this.lblForeground.Y = 0; + this.lblForeground.Visible = true; + this.lblForeground.Arrangement = Terminal.Gui.ViewArrangement.Fixed; + this.lblForeground.Data = "lblForeground"; + this.lblForeground.Text = "Foreground:"; + this.lblForeground.TextAlignment = Terminal.Gui.Alignment.Start; + this.Add(this.lblForeground); + this.cpForeground.Width = 32; + this.cpForeground.Height = 4; + this.cpForeground.X = 2; + this.cpForeground.Y = 1; + this.cpForeground.Visible = true; + this.cpForeground.Arrangement = Terminal.Gui.ViewArrangement.Fixed; + this.cpForeground.Data = "cpForeground"; + this.cpForeground.Text = ""; + this.cpForeground.TextAlignment = Terminal.Gui.Alignment.Start; + this.Add(this.cpForeground); + this.lblBackground.Width = 11; + this.lblBackground.Height = 1; + this.lblBackground.X = 1; + this.lblBackground.Y = 5; + this.lblBackground.Visible = true; + this.lblBackground.Arrangement = Terminal.Gui.ViewArrangement.Fixed; + this.lblBackground.Data = "lblBackground"; + this.lblBackground.Text = "Background:"; + this.lblBackground.TextAlignment = Terminal.Gui.Alignment.Start; + this.Add(this.lblBackground); + this.cpBackground.Width = 32; + this.cpBackground.Height = 4; + this.cpBackground.X = 2; + this.cpBackground.Y = 6; + this.cpBackground.Visible = true; + this.cpBackground.Arrangement = Terminal.Gui.ViewArrangement.Fixed; + this.cpBackground.Data = "cpBackground"; + this.cpBackground.Text = ""; + this.cpBackground.TextAlignment = Terminal.Gui.Alignment.Start; + this.Add(this.cpBackground); + this.lblResult.Width = Dim.Auto(); + this.lblResult.Height = 1; + this.lblResult.X = 1; + this.lblResult.Y = 11; + this.lblResult.Visible = true; + this.lblResult.Arrangement = Terminal.Gui.ViewArrangement.Fixed; + this.lblResult.Data = "lblResult"; + this.lblResult.Text = "Preview:"; + this.lblResult.TextAlignment = Terminal.Gui.Alignment.Start; + this.Add(this.lblResult); + this.lblPreview.Width = Dim.Fill(0); + this.lblPreview.Height = 1; + this.lblPreview.X = 1; + this.lblPreview.Y = 12; + this.lblPreview.Visible = true; + this.lblPreview.Arrangement = Terminal.Gui.ViewArrangement.Fixed; + this.lblPreview.Data = "lblPreview"; + this.lblPreview.Text = "\"Sample Text\""; + this.lblPreview.TextAlignment = Terminal.Gui.Alignment.Center; + this.Add(this.lblPreview); + this.btnOk.Width = 8; + this.btnOk.Height = Dim.Auto(); + this.btnOk.X = 10; + this.btnOk.Y = 17; + this.btnOk.Visible = true; + this.btnOk.Arrangement = Terminal.Gui.ViewArrangement.Fixed; + this.btnOk.Data = "btnOk"; + this.btnOk.Text = "Ok"; + this.btnOk.TextAlignment = Terminal.Gui.Alignment.Center; + this.btnOk.IsDefault = true; + this.Add(this.btnOk); + this.btnCancel.Width = 10; + this.btnCancel.Height = Dim.Auto(); + this.btnCancel.X = 28; + this.btnCancel.Y = 17; + this.btnCancel.Visible = true; + this.btnCancel.Arrangement = Terminal.Gui.ViewArrangement.Fixed; + this.btnCancel.Data = "btnCancel"; + this.btnCancel.Text = "Cancel"; + this.btnCancel.TextAlignment = Terminal.Gui.Alignment.Center; + this.btnCancel.IsDefault = false; + this.Add(this.btnCancel); + } } } diff --git a/src/UI/Windows/ColorPicker.cs b/src/UI/Windows/ColorPicker.cs index 1b686105..ee47c5f0 100644 --- a/src/UI/Windows/ColorPicker.cs +++ b/src/UI/Windows/ColorPicker.cs @@ -37,18 +37,21 @@ public ColorPicker(Attribute? currentValue) if (currentValue != null) { - radiogroup1.SelectedItem = (int)currentValue.Value.Foreground; - radiogroup2.SelectedItem = (int)currentValue.Value.Background; + // TODO: Enable again once true color nuget package available and designer supported + // see https://github.com/gui-cs/Terminal.Gui/pull/3604 + + // cpForeground.SelectedColor = currentValue.Value.Foreground; + // cpBackground.SelectedColor = currentValue.Value.Background; } lblPreview.ColorScheme = new ColorScheme(); UpdatePreview(); - radiogroup1.SelectedItemChanged += (s) => UpdatePreview(); - radiogroup2.SelectedItemChanged += (s) => UpdatePreview(); + cpForeground.ColorChanged += (s,e) => UpdatePreview(); + cpBackground.ColorChanged += (s,e) => UpdatePreview(); - btnOk.Clicked += () => Ok(); - btnCancel.Clicked += () => Cancel(); + btnOk.Accept += (s, e) => Ok(); + btnCancel.Accept += (s, e) => Cancel(); } private void Ok() @@ -66,12 +69,12 @@ private void Cancel() private void UpdatePreview() { - lblPreview.ColorScheme.Normal = GetColor(); + lblPreview.ColorScheme = new ColorScheme(GetColor()); lblPreview.SetNeedsDisplay(); } - + private Attribute GetColor() { - return Attribute.Make((Color)radiogroup1.SelectedItem, (Color)radiogroup2.SelectedItem); + return new Attribute(new Color(cpForeground.SelectedColor),new Color(cpBackground.SelectedColor)); } } \ No newline at end of file diff --git a/src/UI/Windows/ColorSchemeEditor.Designer.cs b/src/UI/Windows/ColorSchemeEditor.Designer.cs index 7718747f..30cf4fc4 100644 --- a/src/UI/Windows/ColorSchemeEditor.Designer.cs +++ b/src/UI/Windows/ColorSchemeEditor.Designer.cs @@ -103,10 +103,8 @@ private void InitializeComponent() { this.Y = Pos.Center(); this.Modal = true; this.Text = ""; - this.Border.BorderStyle = Terminal.Gui.BorderStyle.Single; - this.Border.Effect3D = true; - this.Border.DrawMarginFrame = true; - this.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.Border.BorderStyle = Terminal.Gui.LineStyle.Single; + this.TextAlignment = Terminal.Gui.Alignment.Start; this.Title = "Color Scheme Editor"; this.label2.Width = 10; this.label2.Height = 1; @@ -114,7 +112,7 @@ private void InitializeComponent() { this.label2.Y = 0; this.label2.Data = "label2"; this.label2.Text = "Normal :"; - this.label2.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.label2.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.label2); this.lblForegroundNormal.Width = 1; this.lblForegroundNormal.Height = 1; @@ -122,7 +120,7 @@ private void InitializeComponent() { this.lblForegroundNormal.Y = 0; this.lblForegroundNormal.Data = "lblForegroundNormal"; this.lblForegroundNormal.Text = " "; - this.lblForegroundNormal.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblForegroundNormal.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblForegroundNormal); this.label1.Width = 1; this.label1.Height = 1; @@ -130,7 +128,7 @@ private void InitializeComponent() { this.label1.Y = 0; this.label1.Data = "label1"; this.label1.Text = "\\"; - this.label1.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.label1.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.label1); this.lblBackgroundNormal.Width = 1; this.lblBackgroundNormal.Height = 1; @@ -138,14 +136,14 @@ private void InitializeComponent() { this.lblBackgroundNormal.Y = 0; this.lblBackgroundNormal.Data = "lblBackgroundNormal"; this.lblBackgroundNormal.Text = " "; - this.lblBackgroundNormal.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblBackgroundNormal.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblBackgroundNormal); this.btnEditNormal.Width = 13; this.btnEditNormal.X = 15; this.btnEditNormal.Y = 0; this.btnEditNormal.Data = "btnEditNormal"; this.btnEditNormal.Text = "Choose..."; - this.btnEditNormal.TextAlignment = Terminal.Gui.TextAlignment.Centered; + this.btnEditNormal.TextAlignment = Terminal.Gui.Alignment.Center; this.btnEditNormal.IsDefault = false; this.Add(this.btnEditNormal); this.label22.Width = 10; @@ -154,7 +152,7 @@ private void InitializeComponent() { this.label22.Y = 1; this.label22.Data = "label22"; this.label22.Text = "HotNormal:"; - this.label22.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.label22.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.label22); this.lblForegroundHotNormal.Width = 1; this.lblForegroundHotNormal.Height = 1; @@ -162,7 +160,7 @@ private void InitializeComponent() { this.lblForegroundHotNormal.Y = 1; this.lblForegroundHotNormal.Data = "lblForegroundHotNormal"; this.lblForegroundHotNormal.Text = " "; - this.lblForegroundHotNormal.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblForegroundHotNormal.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblForegroundHotNormal); this.lblHotNormalSlash.Width = 1; this.lblHotNormalSlash.Height = 1; @@ -170,7 +168,7 @@ private void InitializeComponent() { this.lblHotNormalSlash.Y = 1; this.lblHotNormalSlash.Data = "lblHotNormalSlash"; this.lblHotNormalSlash.Text = "\\"; - this.lblHotNormalSlash.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblHotNormalSlash.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblHotNormalSlash); this.lblBackgroundHotNormal.Width = 1; this.lblBackgroundHotNormal.Height = 1; @@ -178,14 +176,14 @@ private void InitializeComponent() { this.lblBackgroundHotNormal.Y = 1; this.lblBackgroundHotNormal.Data = "lblBackgroundHotNormal"; this.lblBackgroundHotNormal.Text = " "; - this.lblBackgroundHotNormal.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblBackgroundHotNormal.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblBackgroundHotNormal); this.btnEditHotNormal.Width = 13; this.btnEditHotNormal.X = 15; this.btnEditHotNormal.Y = 1; this.btnEditHotNormal.Data = "btnEditHotNormal"; this.btnEditHotNormal.Text = "Choose..."; - this.btnEditHotNormal.TextAlignment = Terminal.Gui.TextAlignment.Centered; + this.btnEditHotNormal.TextAlignment = Terminal.Gui.Alignment.Center; this.btnEditHotNormal.IsDefault = false; this.Add(this.btnEditHotNormal); this.lblFocus.Width = 10; @@ -194,7 +192,7 @@ private void InitializeComponent() { this.lblFocus.Y = 2; this.lblFocus.Data = "lblFocus"; this.lblFocus.Text = "Focus :"; - this.lblFocus.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblFocus.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblFocus); this.lblForegroundFocus.Width = 1; this.lblForegroundFocus.Height = 1; @@ -202,7 +200,7 @@ private void InitializeComponent() { this.lblForegroundFocus.Y = 2; this.lblForegroundFocus.Data = "lblForegroundFocus"; this.lblForegroundFocus.Text = " "; - this.lblForegroundFocus.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblForegroundFocus.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblForegroundFocus); this.lblHotNormalSlash2.Width = 1; this.lblHotNormalSlash2.Height = 1; @@ -210,7 +208,7 @@ private void InitializeComponent() { this.lblHotNormalSlash2.Y = 2; this.lblHotNormalSlash2.Data = "lblHotNormalSlash2"; this.lblHotNormalSlash2.Text = "\\"; - this.lblHotNormalSlash2.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblHotNormalSlash2.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblHotNormalSlash2); this.lblBackgroundFocus.Width = 1; this.lblBackgroundFocus.Height = 1; @@ -218,14 +216,14 @@ private void InitializeComponent() { this.lblBackgroundFocus.Y = 2; this.lblBackgroundFocus.Data = "lblBackgroundFocus"; this.lblBackgroundFocus.Text = " "; - this.lblBackgroundFocus.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblBackgroundFocus.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblBackgroundFocus); this.btnEditFocus.Width = 13; this.btnEditFocus.X = 15; this.btnEditFocus.Y = 2; this.btnEditFocus.Data = "btnEditFocus"; this.btnEditFocus.Text = "Choose..."; - this.btnEditFocus.TextAlignment = Terminal.Gui.TextAlignment.Centered; + this.btnEditFocus.TextAlignment = Terminal.Gui.Alignment.Center; this.btnEditFocus.IsDefault = false; this.Add(this.btnEditFocus); this.label223.Width = 10; @@ -234,7 +232,7 @@ private void InitializeComponent() { this.label223.Y = 3; this.label223.Data = "label223"; this.label223.Text = "HotFocus :"; - this.label223.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.label223.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.label223); this.lblForegroundHotFocus.Width = 1; this.lblForegroundHotFocus.Height = 1; @@ -242,7 +240,7 @@ private void InitializeComponent() { this.lblForegroundHotFocus.Y = 3; this.lblForegroundHotFocus.Data = "lblForegroundHotFocus"; this.lblForegroundHotFocus.Text = " "; - this.lblForegroundHotFocus.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblForegroundHotFocus.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblForegroundHotFocus); this.lblHotNormalSlash3.Width = 1; this.lblHotNormalSlash3.Height = 1; @@ -250,7 +248,7 @@ private void InitializeComponent() { this.lblHotNormalSlash3.Y = 3; this.lblHotNormalSlash3.Data = "lblHotNormalSlash3"; this.lblHotNormalSlash3.Text = "\\"; - this.lblHotNormalSlash3.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblHotNormalSlash3.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblHotNormalSlash3); this.lblBackgroundHotFocus.Width = 1; this.lblBackgroundHotFocus.Height = 1; @@ -258,14 +256,14 @@ private void InitializeComponent() { this.lblBackgroundHotFocus.Y = 3; this.lblBackgroundHotFocus.Data = "lblBackgroundHotFocus"; this.lblBackgroundHotFocus.Text = " "; - this.lblBackgroundHotFocus.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblBackgroundHotFocus.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblBackgroundHotFocus); this.btnEditHotFocus.Width = 13; this.btnEditHotFocus.X = 15; this.btnEditHotFocus.Y = 3; this.btnEditHotFocus.Data = "btnEditHotFocus"; this.btnEditHotFocus.Text = "Choose..."; - this.btnEditHotFocus.TextAlignment = Terminal.Gui.TextAlignment.Centered; + this.btnEditHotFocus.TextAlignment = Terminal.Gui.Alignment.Center; this.btnEditHotFocus.IsDefault = false; this.Add(this.btnEditHotFocus); this.label2232.Width = 10; @@ -274,7 +272,7 @@ private void InitializeComponent() { this.label2232.Y = 4; this.label2232.Data = "label2232"; this.label2232.Text = "Disabled :"; - this.label2232.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.label2232.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.label2232); this.lblForegroundDisabled.Width = 1; this.lblForegroundDisabled.Height = 1; @@ -282,7 +280,7 @@ private void InitializeComponent() { this.lblForegroundDisabled.Y = 4; this.lblForegroundDisabled.Data = "lblForegroundDisabled"; this.lblForegroundDisabled.Text = " "; - this.lblForegroundDisabled.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblForegroundDisabled.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblForegroundDisabled); this.lblHotNormalSlash32.Width = 1; this.lblHotNormalSlash32.Height = 1; @@ -290,7 +288,7 @@ private void InitializeComponent() { this.lblHotNormalSlash32.Y = 4; this.lblHotNormalSlash32.Data = "lblHotNormalSlash32"; this.lblHotNormalSlash32.Text = "\\"; - this.lblHotNormalSlash32.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblHotNormalSlash32.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblHotNormalSlash32); this.lblBackgroundDisabled.Width = 1; this.lblBackgroundDisabled.Height = 1; @@ -298,14 +296,14 @@ private void InitializeComponent() { this.lblBackgroundDisabled.Y = 4; this.lblBackgroundDisabled.Data = "lblBackgroundDisabled"; this.lblBackgroundDisabled.Text = " "; - this.lblBackgroundDisabled.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblBackgroundDisabled.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblBackgroundDisabled); this.btnEditDisabled.Width = 13; this.btnEditDisabled.X = 15; this.btnEditDisabled.Y = 4; this.btnEditDisabled.Data = "btnEditDisabled"; this.btnEditDisabled.Text = "Choose..."; - this.btnEditDisabled.TextAlignment = Terminal.Gui.TextAlignment.Centered; + this.btnEditDisabled.TextAlignment = Terminal.Gui.Alignment.Center; this.btnEditDisabled.IsDefault = false; this.Add(this.btnEditDisabled); this.btnOk.Width = 6; @@ -313,7 +311,7 @@ private void InitializeComponent() { this.btnOk.Y = 6; this.btnOk.Data = "btnOk"; this.btnOk.Text = "Ok"; - this.btnOk.TextAlignment = Terminal.Gui.TextAlignment.Centered; + this.btnOk.TextAlignment = Terminal.Gui.Alignment.Center; this.btnOk.IsDefault = false; this.Add(this.btnOk); this.btnCancel.Width = 10; @@ -321,7 +319,7 @@ private void InitializeComponent() { this.btnCancel.Y = 6; this.btnCancel.Data = "btnCancel"; this.btnCancel.Text = "Cancel"; - this.btnCancel.TextAlignment = Terminal.Gui.TextAlignment.Centered; + this.btnCancel.TextAlignment = Terminal.Gui.Alignment.Center; this.btnCancel.IsDefault = false; this.Add(this.btnCancel); } diff --git a/src/UI/Windows/ColorSchemeEditor.cs b/src/UI/Windows/ColorSchemeEditor.cs index ed560969..3903fc24 100644 --- a/src/UI/Windows/ColorSchemeEditor.cs +++ b/src/UI/Windows/ColorSchemeEditor.cs @@ -17,11 +17,33 @@ namespace TerminalGuiDesigner.UI.Windows; /// public partial class ColorSchemeEditor { + class MutableColorScheme + { + public Attribute Disabled { get; set; } + public Attribute Focus { get; set; } + public Attribute HotFocus { get; set; } + public Attribute HotNormal { get; set; } + public Attribute Normal { get; set; } + + internal ColorScheme ToColorScheme() + { + return new ColorScheme + { + Normal = Normal, + HotNormal = HotNormal, + Focus = Focus, + HotFocus = HotFocus, + Disabled = Disabled, + }; + } + } /// /// All colors to use in all states (focused, normal etc). /// - public ColorScheme Result {get;} - + public ColorScheme Result => _result.ToColorScheme(); + + MutableColorScheme _result; + /// /// True if dialog was closed without clicking Ok (e.g. Cancel or Ctrl+Q). /// @@ -34,54 +56,55 @@ public partial class ColorSchemeEditor { public ColorSchemeEditor(ColorScheme scheme) { InitializeComponent(); - Result = Clone(scheme); + _result = Clone(scheme); SetColorPatches(); - btnEditNormal.Clicked += ()=>{ - Result.Normal = PickNewColorsFor(Result.Normal); + btnEditNormal.Accept += (s, e)=>{ + _result.Normal = PickNewColorsFor(Result.Normal); SetColorPatches(); }; - btnEditHotNormal.Clicked += ()=>{ - Result.HotNormal = PickNewColorsFor(Result.HotNormal); + btnEditHotNormal.Accept += (s, e)=>{ + _result.HotNormal = PickNewColorsFor(Result.HotNormal); SetColorPatches(); }; - btnEditFocus.Clicked += ()=>{ - Result.Focus = PickNewColorsFor(Result.Focus); + btnEditFocus.Accept += (s, e)=>{ + _result.Focus = PickNewColorsFor(Result.Focus); SetColorPatches(); }; - btnEditHotFocus.Clicked += ()=>{ - Result.HotFocus = PickNewColorsFor(Result.HotFocus); + btnEditHotFocus.Accept += (s, e)=>{ + _result.HotFocus = PickNewColorsFor(Result.HotFocus); SetColorPatches(); }; - btnEditDisabled.Clicked += ()=>{ - Result.Disabled = PickNewColorsFor(Result.Disabled); + btnEditDisabled.Accept += (s, e)=>{ + _result.Disabled = PickNewColorsFor(Result.Disabled); SetColorPatches(); }; - btnCancel.Clicked += ()=>{ + btnCancel.Accept += (s, e)=>{ Cancelled = true; Application.RequestStop(); }; - btnOk.Clicked += ()=>{ + btnOk.Accept += (s, e)=>{ Cancelled = false; Application.RequestStop(); }; } - private ColorScheme Clone(ColorScheme scheme) + private MutableColorScheme Clone(ColorScheme scheme) { - return new ColorScheme{ + return new MutableColorScheme + { Normal = new Attribute(scheme.Normal.Foreground,scheme.Normal.Background), HotNormal = new Attribute(scheme.HotNormal.Foreground,scheme.HotNormal.Background), Focus = new Attribute(scheme.Focus.Foreground,scheme.Focus.Background), diff --git a/src/UI/Windows/ColorSchemesUI.Designer.cs b/src/UI/Windows/ColorSchemesUI.Designer.cs index a83a08ab..de327d51 100644 --- a/src/UI/Windows/ColorSchemesUI.Designer.cs +++ b/src/UI/Windows/ColorSchemesUI.Designer.cs @@ -16,7 +16,8 @@ namespace TerminalGuiDesigner.UI.Windows; public partial class ColorSchemesUI : Terminal.Gui.Window { private Terminal.Gui.TableView tvColorSchemes; - + private System.Data.DataTable tvColorSchemesTable; + private void InitializeComponent() { this.Width = Dim.Fill(0); this.Height = Dim.Fill(0); @@ -24,11 +25,8 @@ private void InitializeComponent() { this.Y = 0; this.Modal = false; this.Text = ""; - this.Border.BorderStyle = Terminal.Gui.BorderStyle.Single; - this.Border.BorderBrush = Terminal.Gui.Color.Blue; - this.Border.Effect3D = false; - this.TextAlignment = Terminal.Gui.TextAlignment.Left; - this.Title = "Color Schemes (Ctrl+Q to exit)"; + this.TextAlignment = Terminal.Gui.Alignment.Start; + this.Title = "Color Schemes (Esc to exit)"; this.tvColorSchemes = new Terminal.Gui.TableView(); this.tvColorSchemes.Width = Dim.Fill(0); this.tvColorSchemes.Height = Dim.Fill(0); @@ -36,7 +34,7 @@ private void InitializeComponent() { this.tvColorSchemes.Y = 0; this.tvColorSchemes.Data = "tvColorSchemes"; this.tvColorSchemes.Text = ""; - this.tvColorSchemes.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.tvColorSchemes.TextAlignment = Terminal.Gui.Alignment.Start; this.tvColorSchemes.FullRowSelect = false; this.tvColorSchemes.Style.AlwaysShowHeaders = false; this.tvColorSchemes.Style.ExpandLastColumn = false; @@ -45,7 +43,6 @@ private void InitializeComponent() { this.tvColorSchemes.Style.ShowHorizontalHeaderUnderline = true; this.tvColorSchemes.Style.ShowVerticalCellLines = true; this.tvColorSchemes.Style.ShowVerticalHeaderLines = true; - System.Data.DataTable tvColorSchemesTable; tvColorSchemesTable = new System.Data.DataTable(); System.Data.DataColumn tvColorSchemesTableName; tvColorSchemesTableName = new System.Data.DataColumn(); @@ -99,7 +96,7 @@ private void InitializeComponent() { tvColorSchemesTable9 = new System.Data.DataColumn(); tvColorSchemesTable9.ColumnName = "9"; tvColorSchemesTable.Columns.Add(tvColorSchemesTable9); - this.tvColorSchemes.Table = tvColorSchemesTable; + this.tvColorSchemes.Table = new DataTableSource(tvColorSchemesTable); this.Add(this.tvColorSchemes); } } diff --git a/src/UI/Windows/ColorSchemesUI.cs b/src/UI/Windows/ColorSchemesUI.cs index ba27ff57..44f2855c 100644 --- a/src/UI/Windows/ColorSchemesUI.cs +++ b/src/UI/Windows/ColorSchemesUI.cs @@ -41,21 +41,21 @@ public ColorSchemesUI(Design design) { tvColorSchemes.NullSymbol = " "; - var tbl = tvColorSchemes.Table; - + var tbl = tvColorSchemesTable; + foreach(DataColumn col in tbl.Columns) { col.DataType = typeof(int); } - var sName = tvColorSchemes.Style.GetOrCreateColumnStyle(tbl.Columns[NameColumn]); + var sName = tvColorSchemes.Style.GetOrCreateColumnStyle(tbl.Columns[NameColumn].Ordinal); sName.RepresentationGetter = GetName; - var sEdit = tvColorSchemes.Style.GetOrCreateColumnStyle(tbl.Columns[EditColumnName]); + var sEdit = tvColorSchemes.Style.GetOrCreateColumnStyle(tbl.Columns[EditColumnName].Ordinal); sEdit.RepresentationGetter = GetEditString; sEdit.MinWidth = 7; - var sDelete = tvColorSchemes.Style.GetOrCreateColumnStyle(tbl.Columns[DeleteColumnName]); + var sDelete = tvColorSchemes.Style.GetOrCreateColumnStyle(tbl.Columns[DeleteColumnName].Ordinal); sDelete.RepresentationGetter = GetDeleteString; sDelete.MinWidth = 8; @@ -78,7 +78,7 @@ public ColorSchemesUI(Design design) { tvColorSchemes.SelectedCellChanged += CellChanged; // When entering control for the first time ensure a valid selection - CellChanged( + CellChanged(this, new SelectedCellChangedEventArgs( tvColorSchemes.Table, tvColorSchemes.SelectedColumn, @@ -87,14 +87,14 @@ public ColorSchemesUI(Design design) { tvColorSchemes.SelectedRow)); } - private void CellChanged(SelectedCellChangedEventArgs e) + private void CellChanged(object sender, SelectedCellChangedEventArgs e) { // don't let user select the color swatches if(e.NewCol > 2) tvColorSchemes.SelectedColumn = 2; // if selecting last row in the table - if(e.NewRow == tvColorSchemes.Table.Rows.Count-1) + if(e.NewRow == tvColorSchemes.Table.Rows-1) { // only let them press the Add button tvColorSchemes.SelectedColumn = 2; @@ -103,7 +103,7 @@ private void CellChanged(SelectedCellChangedEventArgs e) private void SetupSwatchColumn(DataTable tbl, DataColumn col, Func func) { - var colStyle = tvColorSchemes.Style.GetOrCreateColumnStyle(col); + var colStyle = tvColorSchemes.Style.GetOrCreateColumnStyle(col.Ordinal); colStyle.RepresentationGetter = (o)=> " "; colStyle.ColorGetter = (e)=>(int)e.CellValue == int.MaxValue ? null : ColorToScheme(func(_schemes[(int)e.CellValue].Scheme)); @@ -120,10 +120,11 @@ private ColorScheme ColorToScheme(Color color) }; } - private void CellActivated(TableView.CellActivatedEventArgs e) + private void CellActivated(object sender, CellActivatedEventArgs e) { - var col = e.Table.Columns[e.Col]; - var val = (int)e.Table.Rows[e.Row][e.Col]; + + var col = tvColorSchemesTable.Columns[e.Col]; + var val = (int)tvColorSchemesTable.Rows[e.Row][e.Col]; if(col.ColumnName == EditColumnName && val < _schemes.Length) { @@ -197,7 +198,7 @@ private string GetName(object arg) private void BuildDataTableRows() { - var tbl = tvColorSchemes.Table; + var tbl = tvColorSchemesTable; tbl.Rows.Clear(); _schemes = ColorSchemeManager.Instance.Schemes.ToArray(); @@ -214,7 +215,7 @@ private void BuildDataTableRows() private void AddRowToTableWithAllCellsHavingValue(int i) { - var tbl = tvColorSchemes.Table; + var tbl = tvColorSchemesTable; var r = tbl.Rows.Add(); for(int j = 0;j // This code was generated by: -// TerminalGuiDesigner v1.0.18.0 +// TerminalGuiDesigner v1.1.0.0 // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ----------------------------------------------------------------------------- -namespace TerminalGuiDesigner.UI.Windows; -using System; -using Terminal.Gui; - - -public partial class DimEditor : Terminal.Gui.Dialog { - - private Terminal.Gui.RadioGroup rgDimType; - - private Terminal.Gui.LineView lineview1; - - private Terminal.Gui.Label lblValue; - - private Terminal.Gui.TextField tbValue; - - private Terminal.Gui.Label lblOffset; - - private Terminal.Gui.TextField tbOffset; - - private Terminal.Gui.Button btnOk; +namespace TerminalGuiDesigner.UI.Windows { + using System; + using Terminal.Gui; + using System.Collections; + using System.Collections.Generic; + using System.Drawing; - private Terminal.Gui.Button btnCancel; - private void InitializeComponent() { - this.btnCancel = new Terminal.Gui.Button(); - this.btnOk = new Terminal.Gui.Button(); - this.tbOffset = new Terminal.Gui.TextField(); - this.lblOffset = new Terminal.Gui.Label(); - this.tbValue = new Terminal.Gui.TextField(); - this.lblValue = new Terminal.Gui.Label(); - this.lineview1 = new Terminal.Gui.LineView(); - this.rgDimType = new Terminal.Gui.RadioGroup(); - this.Width = 40; - this.Height = 9; - this.X = Pos.Center(); - this.Y = Pos.Center(); - this.Modal = true; - this.Text = ""; - this.Border.BorderStyle = Terminal.Gui.BorderStyle.Single; - this.Border.Effect3D = true; - this.Border.DrawMarginFrame = true; - this.TextAlignment = Terminal.Gui.TextAlignment.Left; - this.Title = ""; - this.rgDimType.Width = 11; - this.rgDimType.Height = 3; - this.rgDimType.X = 1; - this.rgDimType.Y = 1; - this.rgDimType.Data = "rgDimType"; + public partial class DimEditor : Terminal.Gui.Dialog { + + private Terminal.Gui.RadioGroup rgDimType; + + private Terminal.Gui.LineView lineview1; + + private Terminal.Gui.Label lblValue; + + private Terminal.Gui.TextField tbValue; + + private Terminal.Gui.Label lblOffset; + + private Terminal.Gui.TextField tbOffset; + + private Terminal.Gui.Button btnOk; + + private Terminal.Gui.Button btnCancel; + + private void InitializeComponent() { + this.btnCancel = new Terminal.Gui.Button(); + this.btnOk = new Terminal.Gui.Button(); + this.tbOffset = new Terminal.Gui.TextField(); + this.lblOffset = new Terminal.Gui.Label(); + this.tbValue = new Terminal.Gui.TextField(); + this.lblValue = new Terminal.Gui.Label(); + this.lineview1 = new Terminal.Gui.LineView(); + this.rgDimType = new Terminal.Gui.RadioGroup(); + this.Width = 40; + this.Height = 10; + this.X = Pos.Center(); + this.Y = Pos.Center(); + this.Visible = true; + this.Arrangement = Terminal.Gui.ViewArrangement.Movable; + this.Modal = true; + this.TextAlignment = Terminal.Gui.Alignment.Start; + this.Title = ""; + this.rgDimType.Width = 11; + this.rgDimType.Height = 4; + this.rgDimType.X = 1; + this.rgDimType.Y = 1; + this.rgDimType.Visible = true; + this.rgDimType.Arrangement = Terminal.Gui.ViewArrangement.Fixed; + this.rgDimType.Data = "rgDimType"; this.rgDimType.Text = ""; - this.rgDimType.TextAlignment = Terminal.Gui.TextAlignment.Left; - this.rgDimType.RadioLabels = new NStack.ustring[] { - "Absolute", - "Percent", - "Fill"}; - this.Add(this.rgDimType); - this.lineview1.Width = 1; - this.lineview1.Height = 3; - this.lineview1.X = 12; - this.lineview1.Y = 1; - this.lineview1.Data = "lineview1"; - this.lineview1.Text = ""; - this.lineview1.TextAlignment = Terminal.Gui.TextAlignment.Left; - this.lineview1.LineRune = '│'; - this.lineview1.Orientation = Terminal.Gui.Graphs.Orientation.Vertical; - this.Add(this.lineview1); - this.lblValue.Width = 6; - this.lblValue.Height = 1; - this.lblValue.X = 14; - this.lblValue.Y = 1; - this.lblValue.Data = "lblValue"; - this.lblValue.Text = "Value:"; - this.lblValue.TextAlignment = Terminal.Gui.TextAlignment.Left; - this.Add(this.lblValue); - this.tbValue.Width = 15; - this.tbValue.Height = 1; - this.tbValue.X = Pos.Right(lblValue) + 2; - this.tbValue.Y = Pos.Top(lblValue); - this.tbValue.Secret = false; - this.tbValue.Data = "tbValue"; - this.tbValue.Text = ""; - this.tbValue.TextAlignment = Terminal.Gui.TextAlignment.Left; - this.Add(this.tbValue); - this.lblOffset.Width = 7; - this.lblOffset.Height = 1; - this.lblOffset.X = 14; - this.lblOffset.Y = 3; - this.lblOffset.Data = "lblOffset"; - this.lblOffset.Text = "Offset:"; - this.lblOffset.TextAlignment = Terminal.Gui.TextAlignment.Left; - this.Add(this.lblOffset); - this.tbOffset.Width = 15; - this.tbOffset.Height = 1; - this.tbOffset.X = Pos.Right(lblOffset) + 1; - this.tbOffset.Y = Pos.Top(lblOffset); - this.tbOffset.Secret = false; - this.tbOffset.Data = "tbOffset"; - this.tbOffset.Text = ""; - this.tbOffset.TextAlignment = Terminal.Gui.TextAlignment.Left; - this.Add(this.tbOffset); - this.btnOk.Width = 8; - this.btnOk.X = 5; - this.btnOk.Y = 5; - this.btnOk.Data = "btnOk"; - this.btnOk.Text = "Ok"; - this.btnOk.TextAlignment = Terminal.Gui.TextAlignment.Centered; - this.btnOk.IsDefault = true; - this.Add(this.btnOk); - this.btnCancel.Width = 10; - this.btnCancel.X = 16; - this.btnCancel.Y = 5; - this.btnCancel.Data = "btnCancel"; - this.btnCancel.Text = "Cancel"; - this.btnCancel.TextAlignment = Terminal.Gui.TextAlignment.Centered; - this.btnCancel.IsDefault = false; - this.Add(this.btnCancel); + this.rgDimType.TextAlignment = Terminal.Gui.Alignment.Start; + this.rgDimType.RadioLabels = new string[] { + "Absolute", + "Percent", + "Fill", + "Auto"}; + this.Add(this.rgDimType); + this.lineview1.Width = 1; + this.lineview1.Height = 3; + this.lineview1.X = 12; + this.lineview1.Y = 1; + this.lineview1.Visible = true; + this.lineview1.Arrangement = Terminal.Gui.ViewArrangement.Fixed; + this.lineview1.Data = "lineview1"; + this.lineview1.TextAlignment = Terminal.Gui.Alignment.Start; + this.lineview1.LineRune = new System.Text.Rune('│'); + this.lineview1.Orientation = Terminal.Gui.Orientation.Vertical; + this.Add(this.lineview1); + this.lblValue.Width = 6; + this.lblValue.Height = 1; + this.lblValue.X = 14; + this.lblValue.Y = 1; + this.lblValue.Visible = true; + this.lblValue.Arrangement = Terminal.Gui.ViewArrangement.Fixed; + this.lblValue.Data = "lblValue"; + this.lblValue.Text = "Value:"; + this.lblValue.TextAlignment = Terminal.Gui.Alignment.Start; + this.Add(this.lblValue); + this.tbValue.Width = 15; + this.tbValue.Height = 1; + this.tbValue.X = Pos.Right(lblValue) + 2; + this.tbValue.Y = Pos.Top(lblValue); + this.tbValue.Visible = true; + this.tbValue.Arrangement = Terminal.Gui.ViewArrangement.Fixed; + this.tbValue.Secret = false; + this.tbValue.Data = "tbValue"; + this.tbValue.Text = ""; + this.tbValue.TextAlignment = Terminal.Gui.Alignment.Start; + this.Add(this.tbValue); + this.lblOffset.Width = 7; + this.lblOffset.Height = 1; + this.lblOffset.X = 14; + this.lblOffset.Y = 3; + this.lblOffset.Visible = true; + this.lblOffset.Arrangement = Terminal.Gui.ViewArrangement.Fixed; + this.lblOffset.Data = "lblOffset"; + this.lblOffset.Text = "Offset:"; + this.lblOffset.TextAlignment = Terminal.Gui.Alignment.Start; + this.Add(this.lblOffset); + this.tbOffset.Width = 15; + this.tbOffset.Height = 1; + this.tbOffset.X = Pos.Right(lblOffset) + 1; + this.tbOffset.Y = Pos.Top(lblOffset); + this.tbOffset.Visible = true; + this.tbOffset.Arrangement = Terminal.Gui.ViewArrangement.Fixed; + this.tbOffset.Secret = false; + this.tbOffset.Data = "tbOffset"; + this.tbOffset.Text = ""; + this.tbOffset.TextAlignment = Terminal.Gui.Alignment.Start; + this.Add(this.tbOffset); + this.btnOk.Width = 8; + this.btnOk.Height = Dim.Auto(); + this.btnOk.X = 5; + this.btnOk.Y = 6; + this.btnOk.Visible = true; + this.btnOk.Arrangement = Terminal.Gui.ViewArrangement.Fixed; + this.btnOk.Data = "btnOk"; + this.btnOk.Text = "Ok"; + this.btnOk.TextAlignment = Terminal.Gui.Alignment.Center; + this.btnOk.IsDefault = true; + this.Add(this.btnOk); + this.btnCancel.Width = 10; + this.btnCancel.Height = Dim.Auto(); + this.btnCancel.X = 16; + this.btnCancel.Y = 6; + this.btnCancel.Visible = true; + this.btnCancel.Arrangement = Terminal.Gui.ViewArrangement.Fixed; + this.btnCancel.Data = "btnCancel"; + this.btnCancel.Text = "Cancel"; + this.btnCancel.TextAlignment = Terminal.Gui.Alignment.Center; + this.btnCancel.IsDefault = false; + this.Add(this.btnCancel); + } } } diff --git a/src/UI/Windows/DimEditor.cs b/src/UI/Windows/DimEditor.cs index 46b19f89..1697ed5d 100644 --- a/src/UI/Windows/DimEditor.cs +++ b/src/UI/Windows/DimEditor.cs @@ -19,7 +19,6 @@ namespace TerminalGuiDesigner.UI.Windows; public partial class DimEditor : Dialog { private Design design; - private Property property { get; } /// /// The final value user has configured based on @@ -36,26 +35,23 @@ public partial class DimEditor : Dialog /// Creates a new instance of the class. /// /// - /// - public DimEditor(Design design, Property property) { + /// Old value (if editing an existing instance) + public DimEditor(Design design, Dim oldValue) { InitializeComponent(); this.design = design; - this.property = property; Title = "Dim Designer"; - Border.BorderStyle = BorderStyle.Double; + Border.BorderStyle = LineStyle.Double; - btnOk.Clicked += BtnOk_Clicked; - btnCancel.Clicked += BtnCancel_Clicked; + btnOk.Accept += BtnOk_Clicked; + btnCancel.Accept += BtnCancel_Clicked; Cancelled = true; Modal = true; - rgDimType.KeyPress += RgDimType_KeyPress; + rgDimType.KeyDown += RgDimType_KeyPress; - - var val = (Dim)property.GetValue(); - if(val.GetDimType(out var type,out var value, out var offset)) + if(oldValue.GetDimType(out var type,out var value, out var offset)) { switch(type) { @@ -68,6 +64,9 @@ public DimEditor(Design design, Property property) { case DimType.Fill: rgDimType.SelectedItem = 2; break; + case DimType.Auto: + rgDimType.SelectedItem = 3; + break; } tbValue.Text = value.ToString("G5"); @@ -79,18 +78,18 @@ public DimEditor(Design design, Property property) { rgDimType.SelectedItemChanged += DdType_SelectedItemChanged; } - private void RgDimType_KeyPress(KeyEventEventArgs obj) + private void RgDimType_KeyPress(object sender, Key obj) { - var c = (char)obj.KeyEvent.KeyValue; + var c = (char)obj; // if user types in some text change the focus to the text box to enable entering digits - if ((obj.KeyEvent.Key == Key.Backspace || char.IsDigit(c)) && tbValue.Visible) + if ((obj == Key.Backspace || char.IsDigit(c)) && tbValue.Visible) { - tbValue?.FocusFirst(); + tbValue?.FocusFirst(TabBehavior.TabStop); } } - private void DdType_SelectedItemChanged(SelectedItemChangedArgs obj) + private void DdType_SelectedItemChanged(object sender, SelectedItemChangedArgs obj) { SetupForCurrentDimType(); } @@ -104,34 +103,50 @@ private void SetupForCurrentDimType() { case DimType.Absolute: lblValue.Text = "Value"; + lblValue.Visible = true; + tbValue.Visible = true; + lblOffset.Visible = false; tbOffset.Visible = false; SetNeedsDisplay(); break; case DimType.Fill: + lblValue.Text = "Margin"; + lblValue.Visible = true; + tbValue.Visible = true; + lblOffset.Visible = false; tbOffset.Visible = false; - lblValue.Text = "Margin"; SetNeedsDisplay(); break; case DimType.Percent: lblValue.Text = "Factor"; + lblValue.Visible = true; + tbValue.Visible = true; + lblOffset.Visible = true; tbOffset.Visible = true; SetNeedsDisplay(); break; + case DimType.Auto: + lblValue.Visible = false; + tbValue.Visible = false; + lblOffset.Visible = false; + tbOffset.Visible = false; + SetNeedsDisplay(); + break; default: throw new ArgumentOutOfRangeException(); } } - private void BtnCancel_Clicked() + private void BtnCancel_Clicked(object sender, EventArgs e) { Cancelled = true; Application.RequestStop(); } - private void BtnOk_Clicked() + private void BtnOk_Clicked(object sender, EventArgs e) { Cancelled = false; Result = BuildResult(); @@ -142,17 +157,19 @@ private Dim BuildResult() { // pick what type of Pos they want var type = GetDimType(); - var val = string.IsNullOrWhiteSpace(tbValue.Text.ToString()) ? 0 : float.Parse(tbValue.Text.ToString()); + var val = string.IsNullOrWhiteSpace(tbValue.Text.ToString()) ? 0 : int.Parse(tbValue.Text.ToString()); var offset = string.IsNullOrWhiteSpace(tbOffset.Text.ToString()) ? 0 : int.Parse(tbOffset.Text.ToString()); switch (type) { case DimType.Absolute: - return Dim.Sized((int)val); + return Dim.Absolute(val); case DimType.Percent: return offset == 0? Dim.Percent(val) : Dim.Percent(val) + offset; case DimType.Fill: - return Dim.Fill((int)val); + return Dim.Fill(val); + case DimType.Auto: + return Dim.Auto(); default: throw new ArgumentOutOfRangeException(nameof(type)); diff --git a/src/UI/Windows/EditDialog.cs b/src/UI/Windows/EditDialog.cs index 7e53a3b5..4b362450 100644 --- a/src/UI/Windows/EditDialog.cs +++ b/src/UI/Windows/EditDialog.cs @@ -1,4 +1,7 @@ -using Terminal.Gui; +using System.Collections; +using System.ComponentModel.Design; +using System.Text; +using Terminal.Gui; using Terminal.Gui.TextValidateProviders; using TerminalGuiDesigner.Operations; using TerminalGuiDesigner.ToCode; @@ -12,9 +15,9 @@ namespace TerminalGuiDesigner.UI.Windows; /// public class EditDialog : Window { - private List collection; - private ListView list; - private Design design; + private readonly ListView list; + private readonly Design design; + private readonly List collection = new(); /// /// Initializes a new instance of the class. @@ -23,67 +26,69 @@ public class EditDialog : Window public EditDialog(Design design) { this.design = design; - this.collection = this.design.GetDesignableProperties() - .OrderByDescending(p => p is NameProperty) - .ThenBy(p => p.ToString()) - .ToList(); + this.collection.Clear( ); + this.collection.AddRange( this.design.GetDesignableProperties( ) + .OrderByDescending( p => p is NameProperty ) + .ThenBy( p => p.ToString( ) ) ); // Don't let them rename the root view that would go badly // See `const string RootDesignName` if (design.IsRoot) { - this.collection = this.collection.Where(p => p is not NameProperty).ToList(); + this.collection.RemoveAll( p => p is NameProperty ); } - this.list = new ListView(this.collection) + this.list = new ListView() { X = 0, Y = 0, Width = Dim.Fill(2), Height = Dim.Fill(2), }; - this.list.KeyPress += this.List_KeyPress; + this.list.Source = this.collection.ToListDataSource(); + this.list.KeyDown += this.List_KeyPress; - var btnSet = new Button("Set") + var btnSet = new Button() { + Text = "Set", X = 0, Y = Pos.Bottom(this.list), IsDefault = true, }; - btnSet.Clicked += () => + btnSet.Accept += (s, e) => { this.SetProperty(false); }; - var btnClose = new Button("Close") + var btnClose = new Button() { + Text = "Close", X = Pos.Right(btnSet), Y = Pos.Bottom(this.list), }; - btnClose.Clicked += () => Application.RequestStop(); + btnClose.Accept += (s, e) => Application.RequestStop(); + + this.list.KeyDown += (s, e) => + { + + if (e == Key.Enter && this.list.HasFocus) + { + this.SetProperty(false); + e.Handled = true; + } + }; this.Add(this.list); this.Add(btnSet); this.Add(btnClose); } - /// - public override bool ProcessHotKey(KeyEvent keyEvent) - { - if (keyEvent.Key == Key.Enter && this.list.HasFocus) - { - this.SetProperty(false); - return true; - } - - return base.ProcessHotKey(keyEvent); - } internal static bool SetPropertyToNewValue(Design design, Property p, object? oldValue) { // user wants to give us a new value for this property - if (GetNewValue(design, p, p.GetValue(), out object? newValue)) + if (ValueFactory.GetNewValue(design, p, p.GetValue(), out object? newValue)) { OperationManager.Instance.Do( new SetPropertyOperation(design, p, oldValue, newValue)); @@ -94,309 +99,6 @@ internal static bool SetPropertyToNewValue(Design design, Property p, object? ol return false; } - internal static bool GetNewValue(Design design, Property property, object? oldValue, out object? newValue) - { - if (property.PropertyInfo.PropertyType == typeof(ColorScheme)) - { - return GetNewColorSchemeValue(design, property, out newValue); - } - else - if (property.PropertyInfo.PropertyType == typeof(Attribute) || - property.PropertyInfo.PropertyType == typeof(Attribute?)) - { - // if its an Attribute or nullableAttribute - var picker = new ColorPicker((Attribute?)property.GetValue()); - Application.Run(picker); - - if (!picker.Cancelled) - { - newValue = picker.Result; - return true; - } - else - { - // user cancelled designing the Color - newValue = null; - return false; - } - } - else - if (property.PropertyInfo.PropertyType == typeof(ITextValidateProvider)) - { - string? oldPattern = oldValue is TextRegexProvider r ? (string?)r.Pattern.ToPrimitive() : null; - if (Modals.GetString("New Validation Pattern", "Regex Pattern", oldPattern, out var newPattern)) - { - newValue = string.IsNullOrWhiteSpace(newPattern) ? null : new TextRegexProvider(newPattern); - return true; - } - - // user cancelled entering a pattern - newValue = null; - return false; - } - else - if (property.PropertyInfo.PropertyType == typeof(Pos)) - { - // user is editing a Pos - var designer = new PosEditor(design, property); - - Application.Run(designer); - - if (!designer.Cancelled) - { - newValue = designer.Result; - return true; - } - else - { - // user cancelled designing the Pos - newValue = null; - return false; - } - } - else - if (property.PropertyInfo.PropertyType == typeof(Size)) - { - // user is editing a Size - var oldSize = (Size)(oldValue ?? throw new Exception($"Property {property.PropertyInfo.Name} is of Type Size but it's current value is null")); - var designer = new SizeEditor(oldSize); - - Application.Run(designer); - - if (!designer.Cancelled) - { - newValue = designer.Result; - return true; - } - else - { - // user cancelled designing the Pos - newValue = null; - return false; - } - } - else - if (property.PropertyInfo.PropertyType == typeof(PointF)) - { - // user is editing a PointF - var oldPointF = (PointF)(oldValue ?? throw new Exception($"Property {property.PropertyInfo.Name} is of Type PointF but it's current value is null")); - var designer = new PointEditor(oldPointF.X, oldPointF.Y); - - Application.Run(designer); - - if (!designer.Cancelled) - { - newValue = new PointF(designer.ResultX, designer.ResultY); - return true; - } - else - { - // user cancelled designing the Pos - newValue = null; - return false; - } - } - else - if (property.PropertyInfo.PropertyType == typeof(Dim)) - { - // user is editing a Dim - var designer = new DimEditor(design, property); - Application.Run(designer); - - if (!designer.Cancelled) - { - newValue = designer.Result; - return true; - } - else - { - // user cancelled designing the Dim - newValue = null; - return false; - } - } - else - if (property.PropertyInfo.PropertyType == typeof(bool)) - { - int answer = ChoicesDialog.Query(property.PropertyInfo.Name, $"New value for {property.PropertyInfo.PropertyType}", "Yes", "No"); - - newValue = answer == 0 ? true : false; - return answer != -1; - } - else - if (property.PropertyInfo.PropertyType.IsArray) - { - if (Modals.GetArray( - property.PropertyInfo.Name, - "New Array Value", - property.PropertyInfo.PropertyType.GetElementType() ?? throw new Exception("Property was an Array but GetElementType returned null"), - (Array?)oldValue, - out Array? resultArray)) - { - newValue = resultArray; - return true; - } - } - else - if (property.PropertyInfo.PropertyType == typeof(IListDataSource)) - { - // TODO : Make this work with non strings e.g. - // if user types a bunch of numbers in or dates - var oldValueAsArrayOfStrings = oldValue == null ? - new string[0] : - ((IListDataSource)oldValue).ToList() - .Cast() - .Select(o => o?.ToString()) - .ToArray(); - - if (Modals.GetArray( - property.PropertyInfo.Name, - "New List Value", - typeof(string), - oldValueAsArrayOfStrings ?? new string[0], - out Array? resultArray)) - { - newValue = resultArray; - return true; - } - } - else - if (property.PropertyInfo.PropertyType.IsEnum) - { - if (Modals.GetEnum(property.PropertyInfo.Name, "New Enum Value", property.PropertyInfo.PropertyType, (Enum?)property.GetValue(), out var resultEnum)) - { - newValue = resultEnum; - return true; - } - } - else - if (property.PropertyInfo.PropertyType == typeof(int) - || property.PropertyInfo.PropertyType == typeof(int?) - || property.PropertyInfo.PropertyType == typeof(uint) - || property.PropertyInfo.PropertyType == typeof(uint?)) - { - // deals with null, int and uint - var v = oldValue == null ? null : (int?)Convert.ToInt32(oldValue); - - if (Modals.GetInt(property.PropertyInfo.Name, "New Int Value", v, out var resultInt)) - { - // change back to uint/int/null - newValue = resultInt == null ? null : Convert.ChangeType(resultInt, property.PropertyInfo.PropertyType); - return true; - } - } - else - if (property.PropertyInfo.PropertyType == typeof(float) - || property.PropertyInfo.PropertyType == typeof(float?)) - { - if (Modals.GetFloat(property.PropertyInfo.Name, "New Float Value", (float?)oldValue, out var resultInt)) - { - newValue = resultInt; - return true; - } - } - else - if (property.PropertyInfo.PropertyType == typeof(char?) - || property.PropertyInfo.PropertyType == typeof(char)) - { - if (Modals.GetChar(property.PropertyInfo.Name, "New Single Character", oldValue is null ? null : (char?)oldValue.ToPrimitive() ?? null, out var resultChar)) - { - newValue = resultChar; - return true; - } - } - else - if (property.PropertyInfo.PropertyType == typeof(Rune) - || property.PropertyInfo.PropertyType == typeof(Rune?)) - { - if (Modals.GetChar(property.PropertyInfo.Name, "New Single Character", oldValue is null ? null : (char?)oldValue.ToPrimitive() ?? null, out var resultChar)) - { - newValue = resultChar == null ? null : (Rune)resultChar; - return true; - } - } - else - if (Modals.GetString(property.PropertyInfo.Name, "New String Value", oldValue?.ToString(), out var result, AllowMultiLine(property))) - { - newValue = result; - return true; - } - - newValue = null; - return false; - } - - private static bool GetNewColorSchemeValue(Design design, Property property, out object? newValue) - { - const string custom = "Edit Color Schemes..."; - List offer = new(); - - var defaults = new DefaultColorSchemes(); - var schemes = ColorSchemeManager.Instance.Schemes.ToList(); - - offer.AddRange(schemes); - - foreach (var d in defaults.GetDefaultSchemes()) - { - // user is already explicitly using this default and may even have modified it - if (offer.OfType().Any(s => s.Name.Equals(d.Name))) - { - continue; - } - - offer.Add(d); - } - - // add the option to jump to custom colors - offer.Add(custom); - - if (Modals.Get("Color Scheme", "Ok", offer.ToArray(), design.View.ColorScheme, out var selected)) - { - // if user clicked "Custom..." - if (selected is string s && string.Equals(s, custom)) - { - // show the custom colors dialog - var colorSchemesUI = new ColorSchemesUI(design); - Application.Run(colorSchemesUI); - newValue = null; - return false; - } - - if (selected is NamedColorScheme ns) - { - newValue = ns.Scheme; - - // if it was a default one, tell ColorSchemeManager we are now using it - if (!schemes.Contains(ns)) - { - ColorSchemeManager.Instance.AddOrUpdateScheme(ns.Name, ns.Scheme, design.GetRootDesign()); - } - - return true; - } - - newValue = null; - return false; - } - else - { - // user cancelled selecting scheme - newValue = null; - return false; - } - } - - private static bool AllowMultiLine(Property property) - { - // for the text editor control let them put multiple lines in - if (property.PropertyInfo.Name.Equals("Text") && property.Design.View is TextView tv && tv.Multiline) - { - return true; - } - - return false; - } - private void SetProperty(bool setNull) { if (this.list.SelectedItem != -1) @@ -422,7 +124,7 @@ private void SetProperty(bool setNull) } var oldSelected = this.list.SelectedItem; - this.list.SetSource(this.collection = this.collection.ToList()); + this.list.Source = this.collection.ToListDataSource(); this.list.SelectedItem = oldSelected; this.list.EnsureSelectedItemVisible(); } @@ -433,10 +135,10 @@ private void SetProperty(bool setNull) } } - private void List_KeyPress(KeyEventEventArgs obj) + private void List_KeyPress(object? sender, Key obj) { // TODO: Should really be using the _keyMap here - if (obj.KeyEvent.Key == Key.DeleteChar) + if (obj == Key.DeleteChar) { int rly = ChoicesDialog.Query("Clear", "Clear Property Value?", "Yes", "Cancel"); obj.Handled = true; diff --git a/src/UI/Windows/ExceptionViewer.cs b/src/UI/Windows/ExceptionViewer.cs index 9ff50bf4..075491e0 100644 --- a/src/UI/Windows/ExceptionViewer.cs +++ b/src/UI/Windows/ExceptionViewer.cs @@ -1,5 +1,4 @@ using System.Text.RegularExpressions; -using NStack; using Terminal.Gui; namespace TerminalGuiDesigner.UI.Windows; @@ -32,10 +31,18 @@ public static void ShowException(string errorText, Exception exception) bool toggleStack = true; - var btnOk = new Button("Ok", true); - btnOk.Clicked += () => Application.RequestStop(); - var btnStack = new Button("Stack"); - btnStack.Clicked += () => + var btnOk = new Button() + { + Text = "Ok", + IsDefault = true + }; + + btnOk.Accept += (s, e) => Application.RequestStop(); + var btnStack = new Button() + { + Text = "Stack" + }; + btnStack.Accept += (s, e) => { // flip between stack / no stack textView.Text = GetExceptionText(errorText, exception, toggleStack); @@ -43,19 +50,21 @@ public static void ShowException(string errorText, Exception exception) toggleStack = !toggleStack; }; - var dlg = new Dialog("Error", 10, 10, btnOk, btnStack) + var dlg = new Dialog() { + Title = "Error", X = Pos.Percent(10), Y = Pos.Percent(10), Width = Dim.Percent(80), Height = Dim.Percent(80), + Buttons = new[] { btnOk, btnStack } }; dlg.Add(textView); Application.Run(dlg); } - private static ustring GetExceptionText(string errorText, Exception exception, bool includeStackTrace) + private static string GetExceptionText(string errorText, Exception exception, bool includeStackTrace) { return Wrap(errorText + "\n" + ExceptionHelper.ExceptionToListOfInnerMessages(exception, includeStackTrace), 76); } diff --git a/src/UI/Windows/GetTextDialog.cs b/src/UI/Windows/GetTextDialog.cs index c1fd5d74..667bf811 100644 --- a/src/UI/Windows/GetTextDialog.cs +++ b/src/UI/Windows/GetTextDialog.cs @@ -18,8 +18,9 @@ public GetTextDialog(DialogArgs args, string? initialValue) this.args = args; this.initialValue = initialValue; - this.win = new Window(this.args.WindowTitle) + this.win = new Window() { + Title = this.args.WindowTitle, X = 0, Y = 0, Modal = true, @@ -50,7 +51,7 @@ public GetTextDialog(DialogArgs args, string? initialValue) Text = this.initialValue ?? string.Empty, AllowsTab = false, }; - this.textField.KeyPress += this.TextField_KeyPress; + this.textField.KeyDown += this.TextField_KeyPress; // make it easier for user to replace this text with something else // by directly selecting it all so next keypress replaces text @@ -58,35 +59,38 @@ public GetTextDialog(DialogArgs args, string? initialValue) this.win.Add(this.textField); - var btnOk = new Button("Ok", true) + var btnOk = new Button() { + Text = "Ok", X = 0, Y = Pos.Bottom(this.textField), IsDefault = !this.args.MultiLine, }; - btnOk.Clicked += () => + btnOk.Accept += (s, e) => { this.Accept(); }; - var btnCancel = new Button("Cancel") + var btnCancel = new Button() { + Text = "Cancel", X = Pos.Right(btnOk), Y = Pos.Bottom(this.textField), IsDefault = false, }; - btnCancel.Clicked += () => + btnCancel.Accept += (s, e) => { this.okClicked = false; Application.RequestStop(); }; - var btnClear = new Button("Clear") + var btnClear = new Button() { + Text = "Clear", X = Pos.Right(btnCancel), Y = Pos.Bottom(this.textField), }; - btnClear.Clicked += () => + btnClear.Accept += (s, e) => { this.textField.Text = string.Empty; }; @@ -112,12 +116,12 @@ private void Accept() Application.RequestStop(); } - private void TextField_KeyPress(View.KeyEventEventArgs obj) + private void TextField_KeyPress(object? sender, Key key) { - if (obj.KeyEvent.Key == Key.Enter && !this.args.MultiLine) + if (key == Key.Enter && !this.args.MultiLine) { this.Accept(); - obj.Handled = true; + key.Handled = true; } } } diff --git a/src/UI/Windows/LoadingDialog.Designer.cs b/src/UI/Windows/LoadingDialog.Designer.cs index 395bc200..ca5719cf 100644 --- a/src/UI/Windows/LoadingDialog.Designer.cs +++ b/src/UI/Windows/LoadingDialog.Designer.cs @@ -22,7 +22,7 @@ private void InitializeComponent() { this.Height = 5; this.X = Pos.Center(); this.Y = Pos.Center(); - this.TextAlignment = TextAlignment.Left; + this.TextAlignment = Alignment.Start; this.Title = "Loading..."; this.Title = "Loading..."; this.lblLoading = new Terminal.Gui.Label(); @@ -32,7 +32,7 @@ private void InitializeComponent() { this.lblLoading.Height = 1; this.lblLoading.X = 1; this.lblLoading.Y = 1; - this.lblLoading.TextAlignment = TextAlignment.Left; + this.lblLoading.TextAlignment = Alignment.Start; this.Add(this.lblLoading); } } diff --git a/src/UI/Windows/Modals.cs b/src/UI/Windows/Modals.cs index 4481613a..12205c30 100644 --- a/src/UI/Windows/Modals.cs +++ b/src/UI/Windows/Modals.cs @@ -1,4 +1,5 @@ -using Terminal.Gui; +using System.Diagnostics.CodeAnalysis; +using Terminal.Gui; using YamlDotNet.Core.Tokens; using Key = Terminal.Gui.Key; @@ -38,7 +39,7 @@ public static bool GetInt(string windowTitle, string entryLabel, int? initialVal return false; } - internal static bool GetFloat(string windowTitle, string entryLabel, float? initialValue, out float? result) + internal static bool Getint(string windowTitle, string entryLabel, int? initialValue, out int? result) { if (GetString(windowTitle, entryLabel, initialValue.ToString(), out var newValue)) { @@ -48,7 +49,7 @@ internal static bool GetFloat(string windowTitle, string entryLabel, float? init return true; } - if (float.TryParse(newValue, out var r)) + if (int.TryParse(newValue, out var r)) { result = r; return true; @@ -97,6 +98,44 @@ internal static bool GetArray(string windowTitle, string entryLabel, Type arrayE return false; } + internal static bool TryGetArray(string windowTitle, string entryLabel, Array? initialValue, out Array? result) + { + var dlg = new GetTextDialog( + new () + { + WindowTitle = windowTitle, + EntryLabel = entryLabel, + MultiLine = true, + }, + initialValue == null ? string.Empty : string.Join('\n', initialValue.ToList().Select(v => v?.ToString() ?? string.Empty))); + + if (dlg.ShowDialog()) + { + var resultText = dlg.ResultText; + + if (string.IsNullOrWhiteSpace(resultText)) + { + result = Array.Empty( ); + return true; + } + + resultText = resultText.Replace("\r\n", "\n"); + var newValues = resultText.Split('\n'); + + result = new T[newValues.Length]; + + for (int i = 0; i < newValues.Length; i++) + { + result.SetValue(newValues[i].CastToReflected(typeof(T)), i); + } + + return true; + } + + result = null; + return false; + } + internal static bool GetString(string windowTitle, string entryLabel, string? initialValue, out string? result, bool multiLine = false) { var dlg = new GetTextDialog( @@ -123,10 +162,10 @@ internal static bool Get(string prompt, string okText, T[] collection, T? cur return Get(prompt, okText, true, collection, o => o?.ToString() ?? "Null", false, currentSelection, out selected); } - internal static bool Get(string prompt, string okText, bool addSearch, T[] collection, Func displayMember, bool addNull, T? currentSelection, out T? selected) + internal static bool Get( string prompt, string okText, in bool addSearch, T[] collection, Func displayMember, bool addNull, [NotNullWhen( true )]T? currentSelection, [NotNullWhen( true )] out T? selected ) { - var pick = new BigListBox(prompt, okText, addSearch, collection, displayMember, addNull, currentSelection); - bool toReturn = pick.ShowDialog(); + var pick = new BigListBox( prompt, okText, in addSearch, collection, displayMember, addNull, currentSelection ); + bool toReturn = pick.ShowDialog( ); selected = pick.Selected; return toReturn; } @@ -159,15 +198,29 @@ internal static bool GetChar(string windowTitle, string entryLabel, char? oldVal internal static Terminal.Gui.Key GetShortcut() { - Key key = 0; + Key key = KeyCode.Null; var dlg = new LoadingDialog("Press Shortcut or Del"); - dlg.KeyPress += (s) => + dlg.KeyDown += (s, e) => { - key = s.KeyEvent.Key; - Application.RequestStop(); + if (IsValidShortcut(e)) + { + key = e; + key.Handled = true; + Application.RequestStop(); + } }; Application.Run(dlg); - return key == Key.DeleteChar ? 0 : key; + return key == Key.DeleteChar ? KeyCode.Null : key; + } + + private static bool IsValidShortcut(Key key) + { + if (key.KeyCode == KeyCode.CtrlMask || key.KeyCode == KeyCode.ShiftMask || key.KeyCode == KeyCode.AltMask) + { + return false; + } + + return true; } } diff --git a/src/UI/Windows/PointEditor.Designer.cs b/src/UI/Windows/PointEditor.Designer.cs index b9b84254..4a64ae13 100644 --- a/src/UI/Windows/PointEditor.Designer.cs +++ b/src/UI/Windows/PointEditor.Designer.cs @@ -32,7 +32,7 @@ private void InitializeComponent() { this.Height = 8; this.X = Pos.Center(); this.Y = Pos.Center(); - this.TextAlignment = TextAlignment.Left; + this.TextAlignment = Alignment.Start; this.Title = "Point Designer"; this.lblX = new Terminal.Gui.Label(); this.lblX.Data = "lblX"; @@ -41,7 +41,7 @@ private void InitializeComponent() { this.lblX.Height = 1; this.lblX.X = 2; this.lblX.Y = 1; - this.lblX.TextAlignment = TextAlignment.Left; + this.lblX.TextAlignment = Alignment.Start; this.Add(this.lblX); this.tbX = new Terminal.Gui.TextField(); this.tbX.Data = "tbX"; @@ -50,7 +50,7 @@ private void InitializeComponent() { this.tbX.Height = 1; this.tbX.X = 5; this.tbX.Y = 1; - this.tbX.TextAlignment = TextAlignment.Left; + this.tbX.TextAlignment = Alignment.Start; this.Add(this.tbX); this.lblY = new Terminal.Gui.Label(); this.lblY.Data = "lblY"; @@ -59,7 +59,7 @@ private void InitializeComponent() { this.lblY.Height = 1; this.lblY.X = 2; this.lblY.Y = 3; - this.lblY.TextAlignment = TextAlignment.Left; + this.lblY.TextAlignment = Alignment.Start; this.Add(this.lblY); this.tbY = new Terminal.Gui.TextField(); this.tbY.Data = "tbY"; @@ -68,7 +68,7 @@ private void InitializeComponent() { this.tbY.Height = 1; this.tbY.X = 5; this.tbY.Y = 3; - this.tbY.TextAlignment = TextAlignment.Left; + this.tbY.TextAlignment = Alignment.Start; this.Add(this.tbY); this.btnOk = new Terminal.Gui.Button(); this.btnOk.Data = "btnOk"; @@ -77,7 +77,7 @@ private void InitializeComponent() { this.btnOk.Height = 1; this.btnOk.X = 1; this.btnOk.Y = 5; - this.btnOk.TextAlignment = TextAlignment.Centered; + this.btnOk.TextAlignment = Alignment.Center; this.btnOk.IsDefault = true; this.Add(this.btnOk); this.btnCancel = new Terminal.Gui.Button(); @@ -87,7 +87,7 @@ private void InitializeComponent() { this.btnCancel.Height = 1; this.btnCancel.X = 10; this.btnCancel.Y = 5; - this.btnCancel.TextAlignment = TextAlignment.Centered; + this.btnCancel.TextAlignment = Alignment.Center; this.btnCancel.IsDefault = false; this.Add(this.btnCancel); } diff --git a/src/UI/Windows/PointEditor.cs b/src/UI/Windows/PointEditor.cs index 83e74632..a495bd0c 100644 --- a/src/UI/Windows/PointEditor.cs +++ b/src/UI/Windows/PointEditor.cs @@ -43,21 +43,21 @@ public PointEditor(float x, float y) { tbX.Text = x.ToString(); tbY.Text = y.ToString(); - btnOk.Clicked += Ok; - btnCancel.Clicked += Cancel; + btnOk.Accept += Ok; + btnCancel.Accept += Cancel; } - private void Cancel() + private void Cancel(object sender, EventArgs e) { Cancelled = true; Application.RequestStop(); } - private void Ok() + private void Ok(object sender, EventArgs e) { - if(float.TryParse(tbX.Text.ToString(), out var x)) + if(int.TryParse(tbX.Text.ToString(), out var x)) { - if(float.TryParse(tbY.Text.ToString(), out var y)) + if(int.TryParse(tbY.Text.ToString(), out var y)) { ResultX = x; ResultY = y; diff --git a/src/UI/Windows/PosEditor.Designer.cs b/src/UI/Windows/PosEditor.Designer.cs index 0f26ec68..416e5092 100644 --- a/src/UI/Windows/PosEditor.Designer.cs +++ b/src/UI/Windows/PosEditor.Designer.cs @@ -58,10 +58,8 @@ private void InitializeComponent() { this.Y = Pos.Center(); this.Modal = true; this.Text = ""; - this.Border.BorderStyle = Terminal.Gui.BorderStyle.Single; - this.Border.Effect3D = true; - this.Border.DrawMarginFrame = true; - this.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.Border.BorderStyle = Terminal.Gui.LineStyle.Single; + this.TextAlignment = Terminal.Gui.Alignment.Start; this.Title = ""; this.rgPosType.Width = 12; this.rgPosType.Height = 5; @@ -69,8 +67,8 @@ private void InitializeComponent() { this.rgPosType.Y = 1; this.rgPosType.Data = "rgPosType"; this.rgPosType.Text = ""; - this.rgPosType.TextAlignment = Terminal.Gui.TextAlignment.Left; - this.rgPosType.RadioLabels = new NStack.ustring[] { + this.rgPosType.TextAlignment = Terminal.Gui.Alignment.Start; + this.rgPosType.RadioLabels = new string[] { "Absolute", "Percent", "Relative", @@ -83,9 +81,9 @@ private void InitializeComponent() { this.lineview1.Y = 1; this.lineview1.Data = "lineview1"; this.lineview1.Text = ""; - this.lineview1.TextAlignment = Terminal.Gui.TextAlignment.Left; - this.lineview1.LineRune = '│'; - this.lineview1.Orientation = Terminal.Gui.Graphs.Orientation.Vertical; + this.lineview1.TextAlignment = Terminal.Gui.Alignment.Start; + this.lineview1.LineRune = new System.Text.Rune('│'); + this.lineview1.Orientation = Orientation.Vertical; this.Add(this.lineview1); this.lblValue.Width = 6; this.lblValue.Height = 1; @@ -93,7 +91,7 @@ private void InitializeComponent() { this.lblValue.Y = 1; this.lblValue.Data = "lblValue"; this.lblValue.Text = "Value:"; - this.lblValue.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblValue.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblValue); this.tbValue.Width = 15; this.tbValue.Height = 1; @@ -102,7 +100,7 @@ private void InitializeComponent() { this.tbValue.Secret = false; this.tbValue.Data = "tbValue"; this.tbValue.Text = ""; - this.tbValue.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.tbValue.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.tbValue); this.lblRelativeTo.Width = 12; this.lblRelativeTo.Height = 1; @@ -110,7 +108,7 @@ private void InitializeComponent() { this.lblRelativeTo.Y = 3; this.lblRelativeTo.Data = "lblRelativeTo"; this.lblRelativeTo.Text = "Relative To:"; - this.lblRelativeTo.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblRelativeTo.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblRelativeTo); this.ddRelativeTo.Width = 15; this.ddRelativeTo.Height = 5; @@ -118,7 +116,7 @@ private void InitializeComponent() { this.ddRelativeTo.Y = 3; this.ddRelativeTo.Data = "ddRelativeTo"; this.ddRelativeTo.Text = ""; - this.ddRelativeTo.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.ddRelativeTo.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.ddRelativeTo); this.lblSide.Width = 5; this.lblSide.Height = 1; @@ -126,7 +124,7 @@ private void InitializeComponent() { this.lblSide.Y = 5; this.lblSide.Data = "lblSide"; this.lblSide.Text = "Side:"; - this.lblSide.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblSide.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblSide); this.ddSide.Width = 15; this.ddSide.Height = 5; @@ -134,7 +132,7 @@ private void InitializeComponent() { this.ddSide.Y = Pos.Top(lblSide); this.ddSide.Data = "ddSide"; this.ddSide.Text = ""; - this.ddSide.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.ddSide.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.ddSide); this.lblOffset.Width = 7; this.lblOffset.Height = 1; @@ -142,7 +140,7 @@ private void InitializeComponent() { this.lblOffset.Y = 7; this.lblOffset.Data = "lblOffset"; this.lblOffset.Text = "Offset:"; - this.lblOffset.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.lblOffset.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.lblOffset); this.tbOffset.Width = 15; this.tbOffset.Height = 1; @@ -151,14 +149,14 @@ private void InitializeComponent() { this.tbOffset.Secret = false; this.tbOffset.Data = "tbOffset"; this.tbOffset.Text = ""; - this.tbOffset.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.tbOffset.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.tbOffset); this.btnOk.Width = 8; this.btnOk.X = 11; this.btnOk.Y = 9; this.btnOk.Data = "btnOk"; this.btnOk.Text = "Ok"; - this.btnOk.TextAlignment = Terminal.Gui.TextAlignment.Centered; + this.btnOk.TextAlignment = Terminal.Gui.Alignment.Center; this.btnOk.IsDefault = true; this.Add(this.btnOk); this.btnCancel.Width = 10; @@ -166,7 +164,7 @@ private void InitializeComponent() { this.btnCancel.Y = 9; this.btnCancel.Data = "btnCancel"; this.btnCancel.Text = "Cancel"; - this.btnCancel.TextAlignment = Terminal.Gui.TextAlignment.Centered; + this.btnCancel.TextAlignment = Terminal.Gui.Alignment.Center; this.btnCancel.IsDefault = false; this.Add(this.btnCancel); } diff --git a/src/UI/Windows/PosEditor.cs b/src/UI/Windows/PosEditor.cs index 78780930..a1ea77ec 100644 --- a/src/UI/Windows/PosEditor.cs +++ b/src/UI/Windows/PosEditor.cs @@ -21,7 +21,6 @@ namespace TerminalGuiDesigner.UI.Windows; public partial class PosEditor : Dialog { private Design design; - private Property property; /// /// Users configured (assembled from radio button @@ -36,36 +35,31 @@ public partial class PosEditor : Dialog { /// /// Prompt user to create a new value to populate - /// on with. + /// on with. /// /// What to set the value on. - /// The property to set (must be of type ). - public PosEditor(Design design, Property property) { + /// The current value for the property. + public PosEditor(Design design, Pos oldValue) { InitializeComponent(); this.design = design; - this.property = property; - Title = "Pos Designer"; - Border.BorderStyle = BorderStyle.Double; + Border.BorderStyle = LineStyle.Double; - rgPosType.KeyPress += RgPosType_KeyPress; + rgPosType.KeyDown += RgPosType_KeyPress; - btnOk.Clicked += BtnOk_Clicked; - btnCancel.Clicked += BtnCancel_Clicked; + btnOk.Accept += BtnOk_Clicked; + btnCancel.Accept += BtnCancel_Clicked; Cancelled = true; Modal = true; - var siblings = design.GetSiblings().ToList(); + var siblings = design.GetSiblings().ToListObs(); ddRelativeTo.SetSource(siblings); - ddRelativeTo.AddKeyBinding(Key.CursorDown, Command.Expand); - - ddSide.SetSource(Enum.GetValues(typeof(Side)).Cast().ToList()); - ddSide.AddKeyBinding(Key.CursorDown, Command.Expand); + ddSide.SetSource(Enum.GetValues(typeof(Side)).Cast().ToListObs()); - var val = (Pos)property.GetValue(); + var val = oldValue; if(val.GetPosType(siblings,out var type,out var value,out var relativeTo,out var side, out var offset)) { switch(type) @@ -100,18 +94,18 @@ public PosEditor(Design design, Property property) { } - private void RgPosType_KeyPress(KeyEventEventArgs obj) + private void RgPosType_KeyPress(object sender, Key key) { - var c = (char)obj.KeyEvent.KeyValue; + var c = (char)key; // if user types in some text change the focus to the text box to enable entering digits - if ((obj.KeyEvent.Key == Key.Backspace || char.IsDigit(c)) && tbValue.Visible) + if ((key == Key.Backspace || char.IsDigit(c)) && tbValue.Visible) { - tbValue?.FocusFirst(); + tbValue?.FocusFirst(TabBehavior.TabStop); } } - private void DdType_SelectedItemChanged(SelectedItemChangedArgs obj) + private void DdType_SelectedItemChanged(object sender, SelectedItemChangedArgs obj) { SetupForCurrentPosType(); } @@ -198,13 +192,13 @@ private void SetupForCurrentPosType() } } - private void BtnCancel_Clicked() + private void BtnCancel_Clicked(object sender, EventArgs e) { Cancelled = true; Application.RequestStop(); } - private void BtnOk_Clicked() + private void BtnOk_Clicked(object sender, EventArgs e) { if(GetPosType() == PosType.AnchorEnd && GetValue(out var value) && value <=0) { @@ -299,7 +293,7 @@ private bool BuildPosAbsolute(out Pos result) { if (GetValue(out int newPos)) { - result = Pos.At(newPos); + result = Pos.Absolute(newPos); return true; } diff --git a/src/UI/Windows/SizeEditor.Designer.cs b/src/UI/Windows/SizeEditor.Designer.cs index d5252d7c..ff9578d0 100644 --- a/src/UI/Windows/SizeEditor.Designer.cs +++ b/src/UI/Windows/SizeEditor.Designer.cs @@ -40,10 +40,8 @@ private void InitializeComponent() { this.Y = Pos.Center(); this.Modal = true; this.Text = ""; - this.Border.BorderStyle = Terminal.Gui.BorderStyle.Single; - this.Border.Effect3D = true; - this.Border.DrawMarginFrame = true; - this.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.Border.BorderStyle = Terminal.Gui.LineStyle.Single; + this.TextAlignment = Terminal.Gui.Alignment.Start; this.Title = "Size"; this.label1.Width = 4; this.label1.Height = 1; @@ -51,7 +49,7 @@ private void InitializeComponent() { this.label1.Y = 0; this.label1.Data = "label1"; this.label1.Text = "Width:"; - this.label1.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.label1.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.label1); this.tfWidth.Width = Dim.Fill(1); this.tfWidth.Height = 1; @@ -60,7 +58,7 @@ private void InitializeComponent() { this.tfWidth.Secret = false; this.tfWidth.Data = "tfWidth"; this.tfWidth.Text = ""; - this.tfWidth.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.tfWidth.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.tfWidth); this.label12.Width = 4; this.label12.Height = 1; @@ -68,7 +66,7 @@ private void InitializeComponent() { this.label12.Y = 2; this.label12.Data = "label12"; this.label12.Text = "Height:"; - this.label12.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.label12.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.label12); this.tfHeight.Width = Dim.Fill(1); this.tfHeight.Height = 1; @@ -77,14 +75,14 @@ private void InitializeComponent() { this.tfHeight.Secret = false; this.tfHeight.Data = "tfHeight"; this.tfHeight.Text = ""; - this.tfHeight.TextAlignment = Terminal.Gui.TextAlignment.Left; + this.tfHeight.TextAlignment = Terminal.Gui.Alignment.Start; this.Add(this.tfHeight); this.btnOk.Width = 6; this.btnOk.X = 0; this.btnOk.Y = 4; this.btnOk.Data = "btnOk"; this.btnOk.Text = "Ok"; - this.btnOk.TextAlignment = Terminal.Gui.TextAlignment.Centered; + this.btnOk.TextAlignment = Terminal.Gui.Alignment.Center; this.btnOk.IsDefault = false; this.Add(this.btnOk); this.btnCancel.Width = 10; @@ -92,7 +90,7 @@ private void InitializeComponent() { this.btnCancel.Y = 4; this.btnCancel.Data = "btnCancel"; this.btnCancel.Text = "Cancel"; - this.btnCancel.TextAlignment = Terminal.Gui.TextAlignment.Centered; + this.btnCancel.TextAlignment = Terminal.Gui.Alignment.Center; this.btnCancel.IsDefault = false; this.Add(this.btnCancel); } diff --git a/src/UI/Windows/SizeEditor.cs b/src/UI/Windows/SizeEditor.cs index 0355be63..5b35961c 100644 --- a/src/UI/Windows/SizeEditor.cs +++ b/src/UI/Windows/SizeEditor.cs @@ -39,7 +39,7 @@ public SizeEditor(Size s) tfWidth.Text = s.Width.ToString(); tfHeight.Text = s.Height.ToString(); - btnOk.Clicked += () => + btnOk.Accept += (s, e) => { try { @@ -55,7 +55,7 @@ public SizeEditor(Size s) RequestStop(); }; - btnCancel.Clicked += () => + btnCancel.Accept += (s, e) => { Cancelled = true; RequestStop(); diff --git a/src/UI/Windows/SliderOptionEditor.Designer.cs b/src/UI/Windows/SliderOptionEditor.Designer.cs new file mode 100644 index 00000000..38d01acb --- /dev/null +++ b/src/UI/Windows/SliderOptionEditor.Designer.cs @@ -0,0 +1,175 @@ + +//------------------------------------------------------------------------------ + +// +// This code was generated by: +// TerminalGuiDesigner v1.1.0.0 +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ----------------------------------------------------------------------------- +namespace TerminalGuiDesigner.UI.Windows { + using System; + using Terminal.Gui; + using System.Collections; + using System.Collections.Generic; + + + public partial class SliderOptionEditor : Terminal.Gui.Dialog { + + private Terminal.Gui.ColorScheme redOnBlack; + + private Terminal.Gui.ColorScheme tgDefault; + + private Terminal.Gui.Label label; + + private Terminal.Gui.TextField tfLegend; + + private Terminal.Gui.Label label2; + + private Terminal.Gui.TextField tfLegendAbbr; + + private Terminal.Gui.Label lblOneChar; + + private Terminal.Gui.Label label3; + + private Terminal.Gui.TextField tfData; + + private Terminal.Gui.Label lblType; + + private Terminal.Gui.Button btnOk; + + private Terminal.Gui.Button btnCancel; + + private void InitializeComponent() { + this.btnCancel = new Terminal.Gui.Button(); + this.btnOk = new Terminal.Gui.Button(); + this.lblType = new Terminal.Gui.Label(); + this.tfData = new Terminal.Gui.TextField(); + this.label3 = new Terminal.Gui.Label(); + this.lblOneChar = new Terminal.Gui.Label(); + this.tfLegendAbbr = new Terminal.Gui.TextField(); + this.label2 = new Terminal.Gui.Label(); + this.tfLegend = new Terminal.Gui.TextField(); + this.label = new Terminal.Gui.Label(); + this.redOnBlack = new Terminal.Gui.ColorScheme( + new Terminal.Gui.Attribute(Terminal.Gui.Color.Red, Terminal.Gui.Color.Black), + new Terminal.Gui.Attribute(Terminal.Gui.Color.Red, Terminal.Gui.Color.Yellow), + new Terminal.Gui.Attribute(Terminal.Gui.Color.BrightRed, Terminal.Gui.Color.Black), + new Terminal.Gui.Attribute(Terminal.Gui.Color.Gray, Terminal.Gui.Color.Black), + new Terminal.Gui.Attribute(Terminal.Gui.Color.BrightRed, Terminal.Gui.Color.Yellow) + ); + + this.tgDefault = new Terminal.Gui.ColorScheme( + new Terminal.Gui.Attribute(Terminal.Gui.Color.White, Terminal.Gui.Color.Blue), + new Terminal.Gui.Attribute(Terminal.Gui.Color.Black, Terminal.Gui.Color.Gray), + new Terminal.Gui.Attribute(Terminal.Gui.Color.BrightCyan, Terminal.Gui.Color.Blue), + new Terminal.Gui.Attribute(Terminal.Gui.Color.Yellow, Terminal.Gui.Color.Blue), + new Terminal.Gui.Attribute(Terminal.Gui.Color.BrightBlue, Terminal.Gui.Color.Gray) + ); + this.Width = 50; + this.Height = 8; + this.X = Pos.Center(); + this.Y = Pos.Center(); + this.Visible = true; + this.Modal = true; + this.TextAlignment = Terminal.Gui.Alignment.Start; + this.Title = "OptionEditor"; + this.label.Width = 4; + this.label.Height = 1; + this.label.X = 6; + this.label.Y = 0; + this.label.Visible = true; + this.label.Data = "label"; + this.label.Text = "Legend:"; + this.label.TextAlignment = Terminal.Gui.Alignment.Start; + this.Add(this.label); + this.tfLegend.Width = Dim.Fill(0); + this.tfLegend.Height = 1; + this.tfLegend.X = 14; + this.tfLegend.Y = 0; + this.tfLegend.Visible = true; + this.tfLegend.Secret = false; + this.tfLegend.Data = "tfLegend"; + this.tfLegend.Text = ""; + this.tfLegend.TextAlignment = Terminal.Gui.Alignment.Start; + this.Add(this.tfLegend); + this.label2.Width = 4; + this.label2.Height = 1; + this.label2.X = 0; + this.label2.Y = 1; + this.label2.Visible = true; + this.label2.Data = "label2"; + this.label2.Text = "Abbreviation:"; + this.label2.TextAlignment = Terminal.Gui.Alignment.Start; + this.Add(this.label2); + this.tfLegendAbbr.Width = 1; + this.tfLegendAbbr.Height = 1; + this.tfLegendAbbr.X = 14; + this.tfLegendAbbr.Y = 1; + this.tfLegendAbbr.Visible = true; + this.tfLegendAbbr.Secret = false; + this.tfLegendAbbr.Data = "tfLegendAbbr"; + this.tfLegendAbbr.Text = ""; + this.tfLegendAbbr.TextAlignment = Terminal.Gui.Alignment.Start; + this.Add(this.tfLegendAbbr); + this.lblOneChar.Width = 10; + this.lblOneChar.Height = 1; + this.lblOneChar.X = 0; + this.lblOneChar.Y = 2; + this.lblOneChar.Visible = true; + this.lblOneChar.Data = "lblOneChar"; + this.lblOneChar.Text = "(Single Char) "; + this.lblOneChar.TextAlignment = Terminal.Gui.Alignment.Center; + this.Add(this.lblOneChar); + this.label3.Width = 4; + this.label3.Height = 1; + this.label3.X = 7; + this.label3.Y = 3; + this.label3.Visible = true; + this.label3.Data = "label3"; + this.label3.Text = "Data:"; + this.label3.TextAlignment = Terminal.Gui.Alignment.Start; + this.Add(this.label3); + this.tfData.Width = Dim.Fill(0); + this.tfData.Height = 1; + this.tfData.X = 14; + this.tfData.Y = 3; + this.tfData.Visible = true; + this.tfData.Secret = false; + this.tfData.Data = "tfData"; + this.tfData.Text = ""; + this.tfData.TextAlignment = Terminal.Gui.Alignment.Start; + this.Add(this.tfData); + this.lblType.Width = Dim.Fill(0); + this.lblType.Height = 1; + this.lblType.X = 0; + this.lblType.Y = 4; + this.lblType.Visible = true; + this.lblType.Data = "lblType"; + this.lblType.Text = "( Type ) "; + this.lblType.TextAlignment = Terminal.Gui.Alignment.Center; + this.Add(this.lblType); + this.btnOk.Width = 8; + this.btnOk.Height = 1; + this.btnOk.X = 11; + this.btnOk.Y = 5; + this.btnOk.Visible = true; + this.btnOk.Data = "btnOk"; + this.btnOk.Text = "Ok"; + this.btnOk.TextAlignment = Terminal.Gui.Alignment.Center; + this.btnOk.IsDefault = true; + this.Add(this.btnOk); + this.btnCancel.Width = 8; + this.btnCancel.Height = 1; + this.btnCancel.X = 23; + this.btnCancel.Y = 5; + this.btnCancel.Visible = true; + this.btnCancel.Data = "btnCancel"; + this.btnCancel.Text = "Cancel"; + this.btnCancel.TextAlignment = Terminal.Gui.Alignment.Center; + this.btnCancel.IsDefault = false; + this.Add(this.btnCancel); + } + } +} diff --git a/src/UI/Windows/SliderOptionEditor.cs b/src/UI/Windows/SliderOptionEditor.cs new file mode 100644 index 00000000..fcaab054 --- /dev/null +++ b/src/UI/Windows/SliderOptionEditor.cs @@ -0,0 +1,113 @@ + +//------------------------------------------------------------------------------ + +// +// This code was generated by: +// TerminalGuiDesigner v1.1.0.0 +// You can make changes to this file and they will not be overwritten when saving. +// +// ----------------------------------------------------------------------------- +namespace TerminalGuiDesigner.UI.Windows { + using System.Reflection; + using System.Text; + using Terminal.Gui; + + + public partial class SliderOptionEditor { + private readonly Type genericTypeArgument; + private readonly Type sliderOptionType; + + /// + /// True if the dialog was canceled before completing + /// + public bool Cancelled { get; internal set; } = true; + + /// + /// The resulting value as configured by the user + /// + public object Result { get; internal set; } + + /// + /// Creates a new instance of the Designer to create an instance of + /// where T is of . + /// + /// The T Type of the you want to design + /// Previous value (if editing an existing instance). + public SliderOptionEditor(Type genericTypeArgument, object? oldValue) { + InitializeComponent(); + + this.genericTypeArgument = genericTypeArgument; + this.sliderOptionType = typeof(SliderOption<>).MakeGenericType(this.genericTypeArgument); + + btnOk.Accept += BtnOk_Clicked; + btnCancel.Accept += BtnCancel_Clicked; + + lblType.Text = $"({genericTypeArgument.Name})"; + + if(oldValue != null) + { + var p = sliderOptionType.GetProperty("Legend"); + tfLegend.Text = (string)p.GetValue(oldValue); + + p = sliderOptionType.GetProperty("LegendAbbr"); + tfLegendAbbr.Text = ((Rune)p.GetValue(oldValue)).ToString(); + + p = sliderOptionType.GetProperty("Data"); + + if (this.genericTypeArgument == typeof(string)) + { + tfData.Text = (string)p.GetValue(oldValue); + } + else + { + tfData.Text = p.GetValue(oldValue)?.ToString() ?? ""; + } + } + } + + private void BtnCancel_Clicked(object sender, EventArgs e) + { + this.Cancelled = true; + Application.RequestStop(); + } + + private void BtnOk_Clicked(object sender, EventArgs e) + { + try + { + this.BuildResult(); + } + catch(Exception ex) + { + ExceptionViewer.ShowException("Could not build result", ex); + return; + } + + this.Cancelled = false; + Application.RequestStop(); + } + + private void BuildResult() + { + Result = Activator.CreateInstance(sliderOptionType); + + var p = sliderOptionType.GetProperty("Legend"); + p.SetValue(Result, tfLegend.Text); + + p = sliderOptionType.GetProperty("LegendAbbr"); + p.SetValue(Result, new Rune(tfLegendAbbr.Text[0])); + + p = sliderOptionType.GetProperty("Data"); + + if(this.genericTypeArgument == typeof(string)) + { + p.SetValue(Result, tfData.Text); + } + else + { + p.SetValue(Result, Convert.ChangeType(tfData.Text, this.genericTypeArgument)); + } + + } + } +} diff --git a/src/ViewExtensions.cs b/src/ViewExtensions.cs index 2259b6d0..7253b95e 100644 --- a/src/ViewExtensions.cs +++ b/src/ViewExtensions.cs @@ -21,24 +21,15 @@ public static class ViewExtensions /// any Terminal.Gui artifacts (e.g. ContentView). public static IList GetActualSubviews(this View v) { - if (v is Window w) - { - return w.Subviews[0].Subviews; - } - - if (v is FrameView f) + if (v is TabView t) { - return f.Subviews[0].Subviews; + return t.Tabs.Select(tab => tab.View).Where(v => v != null).ToList(); } - if (v is ScrollView scroll) + // ScrollView has a content view so to reach its children you have to dive down an extra layer + if (v is ScrollView sc) { - return scroll.Subviews[0].Subviews; - } - - if (v is TabView t) - { - return t.Tabs.Select(tab => tab.View).Where(v => v != null).ToList(); + return sc.Subviews[0].Subviews; } return v.Subviews; @@ -189,46 +180,6 @@ public static string GetActualText(this View view) return GetNearestContainerDesign(d.View.SuperView); } - /// - /// - /// Converts a view-relative (col,row) position to a screen-relative position (col,row). The values are optionally clamped to the screen dimensions. - /// - /// This method differs from the private method in Terminal.Gui because it will unwrap private views e.g. such that the real - /// client coordinates of children are returned (e.g. see ). - /// - /// The view that you want to translate client coordinates for. - /// View-relative column. - /// View-relative row. - /// Absolute column; screen-relative. - /// Absolute row; screen-relative. - /// Whether to clip the result of the ViewToScreen method, if set to true, the rcol, rrow values are clamped to the screen (terminal) dimensions (0..TerminalDim-1). - public static void ViewToScreenActual(this View v, int col, int row, out int rcol, out int rrow, bool clipped = true) - { - if (v is Window || v is FrameView) - { - v.Subviews[0].ViewToScreenActual(col, row, out rcol, out rrow, clipped); - return; - } - - // Computes the real row, col relative to the screen. - rrow = row + v.Frame.Y; - rcol = col + v.Frame.X; - var ccontainer = v.SuperView; - while (ccontainer != null) - { - rrow += ccontainer.Frame.Y; - rcol += ccontainer.Frame.X; - ccontainer = ccontainer.SuperView; - } - - // The following ensures that the cursor is always in the screen boundaries. - if (clipped) - { - rrow = Math.Min(rrow, Application.Driver.Rows - 1); - rcol = Math.Min(rcol, Application.Driver.Cols - 1); - } - } - /// /// Returns true if is a Type that is designed to store other /// sub-views in it (, etc). @@ -281,6 +232,11 @@ public static bool IsBorderlessContainerView(this View v) return true; } + if(v.GetType().IsGenericType(typeof(TreeView<>))) + { + return TreeViewExtensions.IsEmpty(v); + } + return false; } @@ -302,8 +258,11 @@ public static bool IsBorderlessContainerView(this View v) v.Visible = false; } - var point = w.ScreenToView(m.X, m.Y); - var hit = ApplicationExtensions.FindDeepestView(w, m.X, m.Y); + var point = w.ScreenToContent(m.Position); + + var hit = View.FindDeepestView(w, m.Position); + + hit = UnpackHitView(hit); int resizeBoxArea = 2; @@ -311,14 +270,21 @@ public static bool IsBorderlessContainerView(this View v) { var screenFrame = hit.FrameToScreen(); - isLowerRight = Math.Abs(screenFrame.X + screenFrame.Width - point.X) <= resizeBoxArea + if (point != new Point(screenFrame.X, screenFrame.Y)) + { + isLowerRight = Math.Abs(screenFrame.X + screenFrame.Width - point.X) <= resizeBoxArea && Math.Abs(screenFrame.Y + screenFrame.Height - point.Y) <= resizeBoxArea; + } + else + { + isLowerRight = false; + } isBorder = - m.X == screenFrame.X + screenFrame.Width - 1 || - m.X == screenFrame.X || - m.Y == screenFrame.Y + screenFrame.Height - 1 || - m.Y == screenFrame.Y; + m.Position.X == screenFrame.X + screenFrame.Width - 1 || + m.Position.X == screenFrame.X || + m.Position.Y == screenFrame.Y + screenFrame.Height - 1 || + m.Position.Y == screenFrame.Y; } else { @@ -332,6 +298,44 @@ public static bool IsBorderlessContainerView(this View v) v.Visible = true; } + return hit is Adornment a ? a.Parent : hit; + } + + /// + /// + /// Sometimes returns what the library considers + /// the clicked View rather than what the user would expect. For example clicking in + /// the area of a . + /// + /// This works out what the real view is. + /// + /// + /// + public static View? UnpackHitView(this View? hit) + { + if (hit != null && hit.GetType().Name.Equals("TabRowView")) + { + hit = hit.SuperView; + } + + // Translate clicks in the border as the real View being clicked + if (hit is Border b) + { + hit = b.Parent; + + } + + // TabView nesting of 'fake' views goes: + // TabView + // - TabViewRow + // - View (pane) + // - Border (note you need Parent not SuperView to find Border parent) + + if (hit?.SuperView is TabView tv) + { + hit = tv; + } + return hit; } @@ -340,16 +344,16 @@ public static bool IsBorderlessContainerView(this View v) /// with the rectangle. /// /// whose bounds will be intersected with . - /// to intersect with . + /// to intersect with . /// True if the client area intersects. - public static bool IntersectsScreenRect(this View v, Rect screenRect) + public static bool IntersectsScreenRect(this View v, Rectangle screenRect) { // TODO: maybe this should use Frame instead? Currently this will not let you drag box // selection over the border of a container to select it (e.g. FrameView). - v.ViewToScreenActual(0, 0, out var x0, out var y0); - v.ViewToScreenActual(v.Bounds.Width, v.Bounds.Height, out var x1, out var y1); + var p0 = v.ContentToScreen(new Point(0, 0)); + var p1 = v.ContentToScreen(new Point(v.GetContentSize().Width, v.GetContentSize().Height)); - return Rect.FromLTRB(x0, y0, x1, y1).IntersectsWith(screenRect); + return Rectangle.FromLTRB(p0.X, p0.Y, p1.X, p1.Y).IntersectsWith(screenRect); } /// @@ -368,7 +372,7 @@ public static bool IntersectsScreenRect(this View v, Rect screenRect) /// if never called (i.e. getter is returning inherited parent value). public static ColorScheme? GetExplicitColorScheme(this View v) { - var explicitColorSchemeField = typeof(View).GetField("colorScheme", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) + var explicitColorSchemeField = typeof(View).GetField("_colorScheme", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) ?? throw new Exception("ColorScheme private backing field no longer exists"); return (ColorScheme?)explicitColorSchemeField.GetValue(v); @@ -390,38 +394,9 @@ public static IEnumerable OrderViewsByScreenPosition(IEnumerable vie .ThenBy(v => v.Frame.X); } - /// - /// Returns in screen coordinates. For tests ensure you - /// have run and that has - /// a route to (e.g. is showing or ). - /// - /// The view you want to translate coordinates for. - /// Screen coordinates of 's . - public static Rect FrameToScreen(this View view) - { - int x = 0; - int y = 0; - - var current = view; - - while (current != null) - { - x += current.Frame.X; - y += current.Frame.Y; - current = current.SuperView; - } - - return new Rect(x, y, view.Frame.Width, view.Frame.Height); - } - private static bool HasNoBorderProperty(this View v) { - if (v.Border == null) - { - return true; - } - - if (v.Border.BorderStyle == BorderStyle.None) + if (v.Border == null || v.BorderStyle == LineStyle.None) { return true; } diff --git a/src/ViewFactory.cs b/src/ViewFactory.cs index 425036bf..a8a7e7a0 100644 --- a/src/ViewFactory.cs +++ b/src/ViewFactory.cs @@ -1,250 +1,343 @@ -using System.Data; -using NStack; -using Terminal.Gui; -using Terminal.Gui.TextValidateProviders; -using TerminalGuiDesigner.Operations.MenuOperations; -using TerminalGuiDesigner.Operations.TableViewOperations; -using static Terminal.Gui.Border; -using Attribute = Terminal.Gui.Attribute; - -namespace TerminalGuiDesigner; - -/// -/// Creates new instances configured to have -/// sensible dimensions and content for dragging/configuring in -/// the designer. -/// -public class ViewFactory -{ - /// - /// Initializes a new instance of the class. - /// - public ViewFactory() - { - } - - /// - /// Returns all Types that are supported by . - /// - /// All supported types. - public static IEnumerable GetSupportedViews() - { - Type[] exclude = new Type[] - { - typeof(Toplevel), - typeof(ToplevelContainer), - typeof(Dialog), - typeof(FileDialog), - typeof(SaveDialog), - typeof(OpenDialog), - typeof(ScrollBarView), - typeof(TreeView<>), - typeof(PanelView), - }; // The generic version of TreeView - - return typeof(View).Assembly.DefinedTypes.Where(t => - typeof(View).IsAssignableFrom(t) && - !t.IsInterface && !t.IsAbstract && t.IsPublic) - .Except(exclude) - .OrderBy(t => t.Name).ToArray(); - } - - /// - /// Creates a new instance of of Type with - /// size/placeholder values that make it easy to see and design in the editor. - /// - /// A Type of . See for the - /// full list of allowed Types. - /// A new instance of Type . - /// Thrown if Type is not a subclass of . - public View Create(Type t) - { - if (typeof(TableView).IsAssignableFrom(t)) - { - return this.CreateTableView(); - } - - if (typeof(TabView).IsAssignableFrom(t)) - { - return this.CreateTabView(); - } - - if (typeof(RadioGroup).IsAssignableFrom(t)) - { - return this.CreateRadioGroup(); - } - - if (typeof(MenuBar).IsAssignableFrom(t)) - { - return this.CreateMenuBar(); - } - - if (typeof(StatusBar).IsAssignableFrom(t)) - { - return new StatusBar(new[] { new StatusItem(Key.F1, "F1 - Edit Me", null) }); - } - - if (t == typeof(TextValidateField)) - { - return new TextValidateField - { - Provider = new TextRegexProvider(".*"), - Text = "Heya", - Width = 5, - Height = 1, - }; - } - - if (t == typeof(ProgressBar)) - { - return new ProgressBar - { - Width = 10, - Height = 1, - Fraction = 1f, - }; - } - - if (t == typeof(View)) - { - return new View - { - Width = 10, - Height = 5, - }; - } - - if (t == typeof(Window)) - { - return new Window - { - Width = 10, - Height = 5, - }; - } - - if (t == typeof(TextField)) - { - return new TextField - { - Width = 10, - Height = 1, - }; - } - - if (typeof(GraphView).IsAssignableFrom(t)) - { - return new GraphView - { - Width = 20, - Height = 5, - GraphColor = Attribute.Make(Color.White, Color.Black), - }; - } - - if (typeof(ListView).IsAssignableFrom(t)) - { - var lv = new ListView(new List { "Item1", "Item2", "Item3" }) - { - Width = 20, - Height = 3, - }; - - return lv; - } - - if (t == typeof(LineView)) - { - return new LineView() - { - Width = 8, - Height = 1, - }; - } - - if (t == typeof(TreeView)) - { - return new TreeView() - { - Width = 16, - Height = 5, - }; - } - - if (t == typeof(ScrollView)) - { - return new ScrollView() - { - Width = 10, - Height = 5, - ContentSize = new Size(20, 10), - }; - } - - var instance = Activator.CreateInstance(t) as View ?? throw new Exception($"CreateInstance returned null for Type '{t}'"); - instance.SetActualText("Heya"); - - instance.Width = Math.Max(instance.Bounds.Width, 4); - instance.Height = Math.Max(instance.Bounds.Height, 1); - - if (instance is FrameView || instance is HexView) - { - instance.Height = 5; - instance.Width = 10; - } - - return instance; - } - - private MenuBar CreateMenuBar() - { - return new MenuBar(new MenuBarItem[] - { - new MenuBarItem( - "_File (F9)", - new MenuItem[] { new MenuItem(AddMenuOperation.DefaultMenuItemText, string.Empty, () => { }) }), - }); - } - - private View CreateRadioGroup() - { - var group = new RadioGroup - { - Width = 10, - Height = 2, - }; - group.RadioLabels = new ustring[] { "Option 1", "Option 2" }; - - return group; - } - - private TableView CreateTableView() - { - var dt = new DataTable(); - dt.Columns.Add("Column 0"); - dt.Columns.Add("Column 1"); - dt.Columns.Add("Column 2"); - dt.Columns.Add("Column 3"); - - return new TableView - { - Width = 50, - Height = 5, - Table = dt, - }; - } - - private TabView CreateTabView() - { - var tabView = new TabView - { - Width = 50, - Height = 5, - }; - - tabView.AddEmptyTab("Tab1"); - tabView.AddEmptyTab("Tab2"); - - return tabView; - } -} +using System.Collections.ObjectModel; +using System.Data; +using Terminal.Gui; +using Terminal.Gui.TextValidateProviders; +using TerminalGuiDesigner.Operations.MenuOperations; +using Attribute = Terminal.Gui.Attribute; + +namespace TerminalGuiDesigner; + +/// +/// Creates new instances configured to have sensible dimensions +/// and content for dragging/configuring in the designer. +/// +public static class ViewFactory +{ + /// + /// A constant defining the default text for a new menu item added via the + /// + /// + /// adds a new top level menu (e.g. File, Edit etc.).
+ /// In the designer, all menus must have at least 1 under them, so it will be + /// created with a single in it, already.
+ /// That item will bear this text.

+ /// This string should be used by any other areas of code that want to create new under + /// a top/sub menu (e.g. ). + ///
+ /// The string "Edit Me" + /// + internal const string DefaultMenuItemText = "Edit Me"; + + internal static readonly Type[] KnownUnsupportedTypes = + [ + typeof( Toplevel ), + typeof( Dialog ), + typeof( FileDialog ), + typeof( SaveDialog ), + typeof( OpenDialog ), + typeof( ScrollBarView ), + + // BUG These seem to cause stack overflows in CreateSubControlDesigns (see TestAddView_RoundTrip) + typeof( Wizard ), + typeof( WizardStep ), + + // TODO: Requires tests and comprehensive testing, also its generic so that's more complicated + typeof(NumericUpDown), + typeof(NumericUpDown<>), + + // Ignore menu bar v2 for now + typeof(MenuBarv2), + + // This is unstable when added directly as a view see https://github.com/gui-cs/Terminal.Gui/issues/3664 + typeof(Shortcut), + + ]; + + /// + /// Gets a new instance of a default [], to include as the default initial + /// + /// collection of a new + /// + /// + /// A new single-element array of , with default text, an empty + /// , and empty string. + /// + internal static MenuBarItem[] DefaultMenuBarItems + { + get + { + return + [ + new( "_File (F9)", + [ new MenuItem( DefaultMenuItemText, string.Empty, static ( ) => { } ) ] ) + ]; + } + } + + /// + /// Gets all Types that are supported by . + /// + /// An of s supported by . + public static IEnumerable SupportedViewTypes { get; } = + typeof(View).Assembly.DefinedTypes + .Where(unfilteredType => unfilteredType is + { + IsInterface: false, + IsAbstract: false, + IsPublic: true, + IsValueType: false + }) + .Where(filteredType => filteredType.IsSubclassOf(typeof(View)) && filteredType != typeof(Adornment) + && !filteredType.IsSubclassOf(typeof(Adornment))) + .Where(viewDescendantType => !KnownUnsupportedTypes.Any(viewDescendantType.IsAssignableTo) + || viewDescendantType == typeof(Window)) + // Slider is an alias of Slider so don't offer that + .Where(vt => vt != typeof(Slider)); + + private static bool IsSupportedType( this Type t ) + { + return t == typeof( Window ) || ( !KnownUnsupportedTypes.Any( t.IsSubclassOf ) & !KnownUnsupportedTypes.Contains( t ) ); + } + + /// + /// Creates a new instance of a of Type with + /// size/placeholder values that make it easy to see and design in the editor. + /// + /// + /// A concrete descendant type of that does not exist in the + /// collection and which has a public constructor. + /// + /// + /// An optional width of the requested view. Default values are dependent on the requested + /// type, if not supplied. + /// + /// + /// An optional height of the requested view. Default values are dependent on the requested + /// type, if not supplied. + /// + /// Initial text for the new view. Only used if it is relevant. + /// If an unsupported type is requested + /// + /// A new instance of with the specified dimensions or defaults, if not provided. + /// + /// + /// must inherit from , must have a public constructor, and must + /// not exist in the collection, at run-time. + /// + public static T Create(int? width = null, int? height = null, string? text = null ) + where T : View, new( ) + { + if ( !IsSupportedType( typeof( T ) ) ) + { + throw new NotSupportedException( $"Requested type {typeof( T ).Name} is not supported" ); + } + + T newView = new( ); + + switch ( newView ) + { + case Button: + case CheckBox: + case ComboBox: + case Label: + newView.SetActualText( text ?? "Heya" ); + SetDefaultDimensions( newView, width ?? 4, height ?? 1 ); + newView.Width = Dim.Auto(); + break; + case ColorPicker: + case TextView: + SetDefaultDimensions(newView, width ?? 10, height ?? 4); + break; + case Line: + case Slider: + case TileView: + SetDefaultDimensions( newView, width ?? 4, height ?? 1 ); + break; + case TableView tv: + var dt = new DataTable( ); + dt.Columns.Add( "Column 0" ); + dt.Columns.Add( "Column 1" ); + dt.Columns.Add( "Column 2" ); + dt.Columns.Add( "Column 3" ); + SetDefaultDimensions( newView, width ?? 50, height ?? 5 ); + tv.Table = new DataTableSource( dt ); + break; + case TabView tv: + tv.AddEmptyTab( "Tab1" ); + tv.AddEmptyTab( "Tab2" ); + SetDefaultDimensions( newView, width ?? 50, height ?? 5 ); + break; + case TextValidateField tvf: + tvf.Provider = new TextRegexProvider( ".*" ); + tvf.Text = text ?? "Heya"; + SetDefaultDimensions( newView, width ?? 5, height ?? 1 ); + break; + case DateField df: + df.Date = DateTime.Today; + SetDefaultDimensions( newView, width ?? 20, height ?? 1 ); + break; + case TextField tf: + tf.Text = text ?? "Heya"; + SetDefaultDimensions( newView, width ?? 5, height ?? 1 ); + break; + case ProgressBar pb: + pb.Fraction = 1f; + SetDefaultDimensions( newView, width ?? 10, height ?? 1 ); + break; + case MenuBar mb: + mb.Menus = DefaultMenuBarItems; + break; + case StatusBar sb: + sb.SetShortcuts(new[] { new Shortcut( Key.F1, "F1 - Edit Me", null ) }); + break; + case RadioGroup rg: + rg.RadioLabels = new string[] { "Option 1", "Option 2" }; + SetDefaultDimensions( newView, width ?? 10, height ?? 2 ); + break; + case GraphView gv: + gv.GraphColor = new Attribute( Color.White, Color.Black ); + SetDefaultDimensions( newView, width ?? 20, height ?? 5 ); + break; + case ListView lv: + lv.SetSource( new ObservableCollection { "Item1", "Item2", "Item3" } ); + SetDefaultDimensions( newView, width ?? 20, height ?? 3 ); + break; + case FrameView: + case HexView: + newView.SetActualText( text ?? "Heya" ); + SetDefaultDimensions( newView, width ?? 10, height ?? 5 ); + break; + case Window: + SetDefaultDimensions( newView, width ?? 10, height ?? 5 ); + break; + case LineView: + SetDefaultDimensions( newView, width ?? 8, height ?? 1 ); + break; + case TreeView: + SetDefaultDimensions( newView, width ?? 16, height ?? 5 ); + break; + case Bar: + SetDefaultDimensions(newView,width?? 4, height?? 1); + newView.SetActualText(text ?? "Heya"); + break; + case TreeView fstv: + fstv.TreeBuilder = new DelegateTreeBuilder((p) => + { + try + { + return p is DirectoryInfo d ? d.GetFileSystemInfos() : Enumerable.Empty(); + } + catch (Exception) + { + return Enumerable.Empty(); + } + }); + + SetDefaultDimensions(newView, width ?? 16, height ?? 5); + break; + case ScrollView sv: + sv.SetContentSize(new Size( 20, 10 )); + SetDefaultDimensions(newView, width ?? 10, height ?? 5 ); + break; + case SpinnerView sv: + sv.AutoSpin = true; + if ( width is not null ) + { + sv.Width = width; + } + + if ( height is not null ) + { + sv.Height = height; + } + + break; + case not null when newView.GetType( ).IsSubclassOf( typeof(View) ): + // Case for view inheritors + SetDefaultDimensions( newView, width ?? 5, height ?? 1 ); + break; + case { }: + newView.SetActualText( text ?? "Heya" ); + SetDefaultDimensions(newView, 10, 5); + break; + case null: + throw new InvalidOperationException( $"Unexpected null result from type {typeof( T ).Name} construtor." ); + } + + return newView; + + static void SetDefaultDimensions( T v, int width = 5, int height = 1 ) + { + v.Width = v.Width is DimFill ? width : Math.Max(v.GetContentSize().Width, width); + + v.Height = v.Height is DimFill ? height : Math.Max( v.GetContentSize().Height, height ); + } + } + + /// + /// Creates a new instance of of with + /// size/placeholder values that make it easy to see and design in the editor. + /// + /// + /// A of .
+ /// See for the full list of allowed Types. + /// + /// A new instance of . + /// Thrown if is not a subclass of . + /// Delegates to , for types supported by that method. + [Obsolete( "Migrate to using generic Create method" )] + public static View Create( Type requestedType ) + { + if (requestedType.IsGenericType) + { + var method = typeof(ViewFactory).GetMethods().Single(m=>m.Name=="Create" && m.IsGenericMethodDefinition); + method = method.MakeGenericMethod(requestedType) ?? throw new Exception("Could not find Create method on ViewFactory"); + + return (View)(method.Invoke(null, new object?[] { null, null, null }) ?? throw new Exception("ViewFactory.Create resulted in null")); + } + + return requestedType switch + { + null => throw new ArgumentNullException( nameof( requestedType ) ), + { } t when t == typeof( DateField ) => Create( ), + { } t when t == typeof( Button ) => Create