diff --git a/README.md b/README.md index 25b5bfb1..8e8d5c26 100644 --- a/README.md +++ b/README.md @@ -112,13 +112,14 @@ You can create a menu separator by typing `---` ### Features ------------------------------- -The following feature list shows the current capabilities and the roadmap +The following feature list shows the current capabilities and the roadmap. Features in +italics are experimental and require passing the `-e` flag when starting application. - [x] Design classes - [x] Window - [x] Dialog - - [ ] View - - [ ] Top level (with statusbar and or menu) + - [x] _View_ + - [x] _Top level_ - [x] Configure root properties (e.g. Window.Width, Title etc) - [x] Configure subview properties - [x] (Name) @@ -203,7 +204,7 @@ The following feature list shows the current capabilities and the roadmap - [x] TextView - [x] TimeField - [x] TreeView - - [ ] View + - [x] View ### Class Diagram ------------------------------- diff --git a/src/Design.cs b/src/Design.cs index ae5f83a7..ad306daf 100644 --- a/src/Design.cs +++ b/src/Design.cs @@ -27,11 +27,14 @@ public class Design private readonly List _designableProperties; + + public DesignState State { get; } + private readonly Logger logger = LogManager.GetCurrentClassLogger(); public Property? GetDesignableProperty(string propertyName) { - return GetDesignableProperties().SingleOrDefault(p=>p.PropertyInfo.Name.Equals(propertyName)); + return GetDesignableProperties().SingleOrDefault(p => p.PropertyInfo.Name.Equals(propertyName)); } @@ -40,12 +43,9 @@ public class Design /// this instance. Instead use so that /// new child controls are preserved for design time changes /// - public View View {get;} - public bool IsContainerView { get - { - return View.IsContainerView(); - } - } + public View View { get; } + public bool IsContainerView => View.IsContainerView(); + public bool IsBorderlessContainerView => View.IsBorderlessContainerView(); public Design(SourceCodeFile sourceCode, string fieldName, View view) { @@ -54,10 +54,40 @@ public Design(SourceCodeFile sourceCode, string fieldName, View view) FieldName = fieldName; _designableProperties = new List(LoadDesignableProperties()); + State = new DesignState(this); } public void CreateSubControlDesigns() { + + // Unlike Window/Dialog the View/TopLevel classes do not have an explicit + // colors schemes. When creating a new View or TopLevel we need to use + // the Colors.Base and fiddle a bit with coloring/clearing to ensure things render correctly + + var baseType = View.GetType().BaseType; + + if (baseType == typeof(View) || baseType == typeof(Toplevel)) + { + if(View.ColorScheme == null || View.ColorScheme == Colors.TopLevel) + { + State.OriginalScheme = View.ColorScheme = Colors.Base; + } + + // TODO: Remove this when https://github.com/gui-cs/Terminal.Gui/issues/2094 is fixed + // HACK + + // View and TopLevel doe not clear their states regularly during drawing + // we have to do that ourselves + View.DrawContent += (r) => + { + // manually erase stale content + Application.Driver.SetAttribute( + State.OriginalScheme?.Normal ?? + View.ColorScheme.Normal); + View.Clear(); + }; + } + CreateSubControlDesigns(View); } @@ -67,11 +97,11 @@ private void CreateSubControlDesigns(View view) { logger.Info($"Found subView of Type '{subView.GetType()}'"); - if(subView.Data is string name) + if (subView.Data is string name) { subView.Data = CreateSubControlDesign(SourceCode, name, subView); } - + CreateSubControlDesigns(subView); } } @@ -81,7 +111,7 @@ public Design CreateSubControlDesign(SourceCodeFile sourceCode, string name, Vie // 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) + if (super != null) { super.Remove(subView); } @@ -89,7 +119,7 @@ public Design CreateSubControlDesign(SourceCodeFile sourceCode, string name, Vie // 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) { // add example rows so that it is easier to design the view @@ -106,7 +136,7 @@ public Design CreateSubControlDesign(SourceCodeFile sourceCode, string name, Vie // To make the graph render correctly we need some content // either a Series or an Annotation - if(subView is GraphView gv && gv.Series.Count == 0 && gv.Annotations.Count == 0) + if (subView is GraphView gv && gv.Series.Count == 0 && gv.Annotations.Count == 0) { // We don't have either so add one gv.Annotations.Add(new TextAnnotation @@ -115,34 +145,42 @@ public Design CreateSubControlDesign(SourceCodeFile sourceCode, string name, Vie Text = "" }); } - if(subView is MenuBar mb) - { + if (subView is MenuBar mb) + { MenuTracker.Instance.Register(mb); } - if(subView is CheckBox cb) + if (subView is CheckBox cb) { RegisterCheckboxDesignTimeChanges(cb); } - if(subView is TextView txt) + if (subView is TextView txt) + { + // prevent control from responding to events + txt.MouseClick += SuppressNativeClickEvents; + txt.KeyDown += (s) => s.Handled = true; + } + + if (subView is TextField tf) { // prevent control from responding to events - txt.MouseClick += (s)=>s.Handled = true; - txt.KeyDown += (s)=>s.Handled = true; + tf.MouseClick += SuppressNativeClickEvents; + tf.KeyDown += (s) => s.Handled = true; } - - if(subView is TreeView tree) + if (subView is TreeView tree) { - tree.AddObject(new TreeNode("Example Branch 1"){ - Children = new []{new TreeNode("Child 1")} + tree.AddObject(new TreeNode("Example Branch 1") + { + Children = new[] { new TreeNode("Child 1") } }); - tree.AddObject(new TreeNode("Example Branch 2"){ - Children = new []{ + tree.AddObject(new TreeNode("Example Branch 2") + { + Children = new[]{ new TreeNode("Child 1"), new TreeNode("Child 2")} }); - + for (int l = 0; l < 20; l++) tree.AddObject(new TreeNode($"Example Leaf {l}")); } @@ -152,11 +190,16 @@ public Design CreateSubControlDesign(SourceCodeFile sourceCode, string name, Vie super.Add(subView); } - var d = new Design(sourceCode,name, subView); - subView.Enter += (s=>SelectionManager.Instance.SetSelection(d)); + var d = new Design(sourceCode, name, subView); return d; } + private void SuppressNativeClickEvents(View.MouseEventArgs obj) + { + // Suppress everything except single click (selection) + obj.Handled = obj.MouseEvent.Flags != MouseFlags.Button1Clicked; + } + private void RegisterCheckboxDesignTimeChanges(CheckBox cb) { // prevent space toggling the checkbox @@ -183,19 +226,19 @@ private void RegisterCheckboxDesignTimeChanges(CheckBox cb) /// public bool HasKnownColorScheme() { - var userDefinedColorScheme = SelectionManager.Instance.GetOriginalExplicitColorScheme(this) ?? View.GetExplicitColorScheme(); + var userDefinedColorScheme = State.OriginalScheme ?? View.GetExplicitColorScheme(); - if(userDefinedColorScheme == null) + if (userDefinedColorScheme == null) return false; // theres a color scheme defined but we aren't tracking it // so report it as inherited since it must have got it from // the API somehow - if(Colors.ColorSchemes.Values.Contains(userDefinedColorScheme)) + if (Colors.ColorSchemes.Values.Contains(userDefinedColorScheme)) return false; // it has a ColorScheme but not one we are tracking - if(ColorSchemeManager.Instance.GetNameForColorScheme(userDefinedColorScheme) == null) + if (ColorSchemeManager.Instance.GetNameForColorScheme(userDefinedColorScheme) == null) return false; return true; @@ -224,7 +267,7 @@ protected virtual IEnumerable LoadDesignableProperties() { yield return CreateProperty(nameof(View.Width)); - if(AllowChangingHeight()) + if (AllowChangingHeight()) yield return CreateProperty(nameof(View.Height)); yield return CreateProperty(nameof(View.X)); @@ -235,7 +278,7 @@ protected virtual IEnumerable LoadDesignableProperties() // its important that this comes before Text because // changing the validator clears the text - if(View is TextValidateField) + if (View is TextValidateField) { yield return CreateProperty(nameof(TextValidateField.Provider)); } @@ -254,38 +297,38 @@ protected virtual IEnumerable LoadDesignableProperties() yield return CreateProperty(nameof(TextView.WordWrap)); } - if(View is Toplevel) + if (View is Toplevel) { yield return CreateProperty(nameof(Toplevel.Modal)); } // Allow changing the FieldName on anything but root where // such an action would break things badly - if(!this.IsRoot) + if (!this.IsRoot) yield return new NameProperty(this); - yield return new Property(this,View.GetActualTextProperty()); + yield return new Property(this, View.GetActualTextProperty()); // Border properties - Most views dont have a border so Border is - if(View.Border != null) + if (View.Border != null) { - yield return CreateSubProperty(nameof(Border.BorderStyle),nameof(View.Border),View.Border); - yield return CreateSubProperty(nameof(Border.Effect3D),nameof(View.Border),View.Border); + yield return CreateSubProperty(nameof(Border.BorderStyle), nameof(View.Border), View.Border); + yield return CreateSubProperty(nameof(Border.Effect3D), nameof(View.Border), View.Border); yield return CreateSubProperty(nameof(Border.DrawMarginFrame), nameof(View.Border), View.Border); } - + yield return CreateProperty(nameof(View.TextAlignment)); if (View is Button) { yield return CreateProperty(nameof(Button.IsDefault)); } - if(View is LineView) + if (View is LineView) { yield return CreateProperty(nameof(LineView.LineRune)); yield return CreateProperty(nameof(LineView.Orientation)); } - if(View is ProgressBar) + if (View is ProgressBar) { yield return CreateProperty(nameof(ProgressBar.Fraction)); yield return CreateProperty(nameof(ProgressBar.BidirectionalMarquee)); @@ -302,7 +345,7 @@ protected virtual IEnumerable LoadDesignableProperties() { yield return CreateProperty(nameof(ListView.Source)); } - if(View is GraphView gv) + if (View is GraphView gv) { yield return CreateProperty(nameof(GraphView.GraphColor)); yield return CreateProperty(nameof(GraphView.ScrollOffset)); @@ -310,16 +353,16 @@ protected virtual IEnumerable LoadDesignableProperties() yield return CreateProperty(nameof(GraphView.MarginBottom)); yield return CreateProperty(nameof(GraphView.CellSize)); - yield return CreateSubProperty(nameof(HorizontalAxis.Visible),nameof(GraphView.AxisX),gv.AxisX); - yield return CreateSubProperty(nameof(HorizontalAxis.Increment),nameof(GraphView.AxisX),gv.AxisX); - yield return CreateSubProperty(nameof(HorizontalAxis.ShowLabelsEvery),nameof(GraphView.AxisX),gv.AxisX); - yield return CreateSubProperty(nameof(HorizontalAxis.Minimum),nameof(GraphView.AxisX),gv.AxisX); - yield return CreateSubProperty(nameof(HorizontalAxis.Text),nameof(GraphView.AxisX),gv.AxisX); - yield return CreateSubProperty(nameof(VerticalAxis.Visible),nameof(GraphView.AxisY),gv.AxisY); - yield return CreateSubProperty(nameof(VerticalAxis.Increment),nameof(GraphView.AxisY),gv.AxisY); - yield return CreateSubProperty(nameof(VerticalAxis.ShowLabelsEvery),nameof(GraphView.AxisY),gv.AxisY); - yield return CreateSubProperty(nameof(VerticalAxis.Minimum),nameof(GraphView.AxisY),gv.AxisY); - yield return CreateSubProperty(nameof(VerticalAxis.Text),nameof(GraphView.AxisY),gv.AxisY); + yield return CreateSubProperty(nameof(HorizontalAxis.Visible), nameof(GraphView.AxisX), gv.AxisX); + yield return CreateSubProperty(nameof(HorizontalAxis.Increment), nameof(GraphView.AxisX), gv.AxisX); + yield return CreateSubProperty(nameof(HorizontalAxis.ShowLabelsEvery), nameof(GraphView.AxisX), gv.AxisX); + yield return CreateSubProperty(nameof(HorizontalAxis.Minimum), nameof(GraphView.AxisX), gv.AxisX); + yield return CreateSubProperty(nameof(HorizontalAxis.Text), nameof(GraphView.AxisX), gv.AxisX); + yield return CreateSubProperty(nameof(VerticalAxis.Visible), nameof(GraphView.AxisY), gv.AxisY); + yield return CreateSubProperty(nameof(VerticalAxis.Increment), nameof(GraphView.AxisY), gv.AxisY); + yield return CreateSubProperty(nameof(VerticalAxis.ShowLabelsEvery), nameof(GraphView.AxisY), gv.AxisY); + yield return CreateSubProperty(nameof(VerticalAxis.Minimum), nameof(GraphView.AxisY), gv.AxisY); + yield return CreateSubProperty(nameof(VerticalAxis.Text), nameof(GraphView.AxisY), gv.AxisY); } if (View is Window) @@ -333,20 +376,20 @@ protected virtual IEnumerable LoadDesignableProperties() if (View is TreeView tree) { - yield return CreateSubProperty(nameof(TreeStyle.CollapseableSymbol),nameof(TreeView.Style),tree.Style); - yield return CreateSubProperty(nameof(TreeStyle.ColorExpandSymbol),nameof(TreeView.Style),tree.Style); - yield return CreateSubProperty(nameof(TreeStyle.ExpandableSymbol),nameof(TreeView.Style),tree.Style); - yield return CreateSubProperty(nameof(TreeStyle.InvertExpandSymbolColors),nameof(TreeView.Style),tree.Style); - yield return CreateSubProperty(nameof(TreeStyle.LeaveLastRow),nameof(TreeView.Style),tree.Style); - yield return CreateSubProperty(nameof(TreeStyle.ShowBranchLines),nameof(TreeView.Style),tree.Style); + yield return CreateSubProperty(nameof(TreeStyle.CollapseableSymbol), nameof(TreeView.Style), tree.Style); + yield return CreateSubProperty(nameof(TreeStyle.ColorExpandSymbol), nameof(TreeView.Style), tree.Style); + yield return CreateSubProperty(nameof(TreeStyle.ExpandableSymbol), nameof(TreeView.Style), tree.Style); + yield return CreateSubProperty(nameof(TreeStyle.InvertExpandSymbolColors), nameof(TreeView.Style), tree.Style); + yield return CreateSubProperty(nameof(TreeStyle.LeaveLastRow), nameof(TreeView.Style), tree.Style); + yield return CreateSubProperty(nameof(TreeStyle.ShowBranchLines), nameof(TreeView.Style), tree.Style); } - + if (View is TableView tv) { yield return CreateProperty(nameof(TableView.FullRowSelect)); - yield return CreateSubProperty(nameof(TableStyle.AlwaysShowHeaders),nameof(TableView.Style),tv.Style); + yield return CreateSubProperty(nameof(TableStyle.AlwaysShowHeaders), nameof(TableView.Style), tv.Style); yield return CreateSubProperty(nameof(TableStyle.ExpandLastColumn), nameof(TableView.Style), tv.Style); yield return CreateSubProperty(nameof(TableStyle.InvertSelectedCellFirstCharacter), nameof(TableView.Style), tv.Style); yield return CreateSubProperty(nameof(TableStyle.ShowHorizontalHeaderOverline), nameof(TableView.Style), tv.Style); @@ -355,7 +398,7 @@ protected virtual IEnumerable LoadDesignableProperties() yield return CreateSubProperty(nameof(TableStyle.ShowVerticalHeaderLines), nameof(TableView.Style), tv.Style); } - + if (View is TabView tabView) { yield return CreateProperty(nameof(TabView.MaxTabTextWidth)); @@ -365,7 +408,7 @@ protected virtual IEnumerable LoadDesignableProperties() yield return CreateSubProperty(nameof(TabStyle.TabsOnBottom), nameof(TabView.Style), tabView.Style); } - if(View is RadioGroup) + if (View is RadioGroup) { yield return CreateProperty(nameof(RadioGroup.RadioLabels)); } @@ -374,22 +417,22 @@ protected virtual IEnumerable LoadDesignableProperties() private bool AllowChangingHeight() { // don't support multi line buttons - if(View is Button) + if (View is Button) return false; - return true; + return true; } private Property CreateSubProperty(string name, string subObjectName, object subObject) { - return new Property(this,subObject.GetType().GetProperty(name) + return new Property(this, subObject.GetType().GetProperty(name) ?? throw new Exception($"Could not find expected Property '{name}' on Sub Object of Type '{subObject.GetType()}'") - ,subObjectName,subObject); + , subObjectName, subObject); } private Property CreateProperty(string name) { - return new Property(this,View.GetType().GetProperty(name) + return new Property(this, View.GetType().GetProperty(name) ?? throw new Exception($"Could not find expected Property '{name}' on View of Type '{View.GetType()}'")); } @@ -416,7 +459,7 @@ internal IEnumerable GetExtraOperations(Point pos) if (View is TableView tv) { DataColumn? col = null; - if(!pos.IsEmpty) + if (!pos.IsEmpty) { // TODO: Currently you have to right click in the row (body) of the table // and cannot right click the headers themselves @@ -430,16 +473,16 @@ internal IEnumerable GetExtraOperations(Point pos) yield return new RenameColumnOperation(this, col); } - if(IsContainerView || IsRoot) + if (IsContainerView || IsRoot) { - yield return new AddViewOperation(SourceCode,this); + yield return new AddViewOperation(SourceCode, this); yield return new PasteOperation(this); } else { var nearestContainer = this.View.GetNearestContainerDesign(); - if(nearestContainer != null) - yield return new AddViewOperation(SourceCode,nearestContainer); + if (nearestContainer != null) + yield return new AddViewOperation(SourceCode, nearestContainer); } yield return new DeleteViewOperation(this.View); @@ -454,9 +497,9 @@ internal IEnumerable GetExtraOperations(Point pos) yield return new MoveTabOperation(this, 1); } - if(View is MenuBar) + if (View is MenuBar) { - yield return new AddMenuOperation(this,null); + yield return new AddMenuOperation(this, null); yield return new RemoveMenuOperation(this); } } @@ -470,19 +513,19 @@ public IEnumerable GetSiblings() { // If there is no parent then we are likely the top rot Design // or an orphan. Either way we have no siblings - if(View.SuperView == null) + if (View.SuperView == null) { yield break; } - foreach(var v in View.SuperView.Subviews) + foreach (var v in View.SuperView.Subviews) { - if(v == View) + if (v == View) { continue; } - if(v.Data is Design d) + if (v.Data is Design d) { yield return d; } @@ -496,12 +539,12 @@ public IEnumerable GetSiblings() public IEnumerable GetAllDesigns() { var root = GetRootDesign(); - + // Return the root design yield return root; - + // And all child designs - foreach(var d in GetAllChildDesigns(root.View)) + foreach (var d in GetAllChildDesigns(root.View)) { yield return d; } @@ -516,13 +559,13 @@ public Design GetRootDesign() var toReturn = this; var v = View; - while(v.SuperView != null) + while (v.SuperView != null) { v = v.SuperView; - if(v.Data is Design d) + if (v.Data is Design d) { - if(d.IsRoot) + if (d.IsRoot) return d; toReturn = d; @@ -593,10 +636,10 @@ public string GetUniqueFieldName(string candidate) // what field names are already taken by other objects? var usedFieldNames = allDesigns.Select(d => d.FieldName).ToList(); - usedFieldNames.AddRange(ColorSchemeManager.Instance.Schemes.Select(k=>k.Name)); + usedFieldNames.AddRange(ColorSchemeManager.Instance.Schemes.Select(k => k.Name)); // if name is already unique thats great - if(!usedFieldNames.Contains(candidate)) + if (!usedFieldNames.Contains(candidate)) { return candidate; } @@ -619,7 +662,7 @@ public string GetUniqueFieldName(string candidate) public IEnumerable GetDependantDesigns() { var everyone = GetAllDesigns().ToArray(); - return everyone.Where(o=>DependsOnUs(o,everyone)); + return everyone.Where(o => DependsOnUs(o, everyone)); } private bool DependsOnUs(Design other, Design[] everyone) @@ -629,7 +672,7 @@ private bool DependsOnUs(Design other, Design[] everyone) return false; // if their X depends on us - if(other.View.X.GetPosType(everyone,out _,out _, out var relativeTo,out _,out _)) + if (other.View.X.GetPosType(everyone, out _, out _, out var relativeTo, out _, out _)) { if (relativeTo == this) return true; diff --git a/src/DesignState.cs b/src/DesignState.cs new file mode 100644 index 00000000..978ff670 --- /dev/null +++ b/src/DesignState.cs @@ -0,0 +1,58 @@ +using Terminal.Gui; +using TerminalGuiDesigner.UI; + +namespace TerminalGuiDesigner; + +/// +/// Describes state based changes and custom callbacks on a +/// e.g. +/// +public class DesignState +{ + public ColorScheme? OriginalScheme { get; set; } + public Design Design{ get; } + + public DesignState(Design design) + { + Design = design; + OriginalScheme = Design.View.GetExplicitColorScheme(); + Design.View.DrawContent += DrawContent; + Design.View.Enter += Enter; + } + + private void Enter(View.FocusEventArgs obj) + { + SelectionManager.Instance.SetSelection(Design); + } + + private void DrawContent(Rect r) + { + if(Design.View.IsBorderlessContainerView() && Editor.ShowBorders) + { + DrawBorderlessViewFrame(r); + } + } + + private void DrawBorderlessViewFrame(Rect r) + { + bool isSelected = SelectionManager.Instance.Selected.Contains(Design); + + var color = isSelected ? + SelectionManager.Instance.SelectedScheme.Normal : + Design.View.ColorScheme.Normal; + + Application.Driver.SetAttribute(color); + + var v = Design.View; + + for (int x = 0; x < r.Width ; x++) + for (int y = 0; y < r.Height; y++) + { + 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); + } + } + } +} \ No newline at end of file diff --git a/src/Keys.yaml b/src/Keys.yaml index 1ab7bdd8..899802a2 100644 --- a/src/Keys.yaml +++ b/src/Keys.yaml @@ -12,6 +12,7 @@ Delete: DeleteChar ToggleDragging: F3 AddView: F2 ToggleShowFocused: L, CtrlMask +ToggleShowBorders: B, CtrlMask RightClick: Button3Clicked Copy: C, CtrlMask Paste: V, CtrlMask diff --git a/src/Operations/DragOperation.cs b/src/Operations/DragOperation.cs index 9087fc28..a34369ca 100644 --- a/src/Operations/DragOperation.cs +++ b/src/Operations/DragOperation.cs @@ -16,14 +16,14 @@ private class DragMemento public Pos OriginalX {get;} public Pos OriginalY {get;} - public View OriginalSuperView {get;} + public View? OriginalSuperView {get;} public DragMemento(Design design) { Design = design; OriginalX = design.View.X; OriginalY = design.View.Y; - OriginalSuperView = design.View.SuperView; + OriginalSuperView = design.View.SuperView?.GetNearestDesign()?.View; } } @@ -108,7 +108,7 @@ private bool Do(DragMemento mem) // if changing to a new container if (DropInto != mem.OriginalSuperView && mem.OriginalSuperView != null) { - mem.OriginalSuperView.Remove(mem.Design.View); + mem.Design.View.SuperView.Remove(mem.Design.View); DropInto.Add(mem.Design.View); } } diff --git a/src/Options.cs b/src/Options.cs index b0901b0b..08c51a55 100644 --- a/src/Options.cs +++ b/src/Options.cs @@ -21,7 +21,10 @@ public class Options [Option( HelpText = "Enables UseSystemConsole, an alternative console display driver")] public bool Usc { get;set; } - + + [Option( 'e', HelpText = "Enables experimental features")] + public bool Experimental { get;set; } + #nullable enable warnings [Usage(ApplicationAlias = "TerminalGuiDesigner")] diff --git a/src/Program.cs b/src/Program.cs index 94e15a1b..e54b1db5 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -14,6 +14,7 @@ public static void Main(string[] args) .WithParsed(o => { Application.UseSystemConsole = o.Usc; + Editor.Experimental = o.Experimental; Application.Init(); var editor = new Editor(); diff --git a/src/SelectionManager.cs b/src/SelectionManager.cs index d09084b9..fb187206 100644 --- a/src/SelectionManager.cs +++ b/src/SelectionManager.cs @@ -17,8 +17,6 @@ public class SelectionManager /// public IReadOnlyCollection Selected => selection.AsReadOnly(); - Dictionary oldSchemes = new(); - /// /// Set to true to prevent changes to the current /// collection (e.g. if running a modal dialog / context menu). @@ -57,16 +55,8 @@ public ColorScheme SelectedScheme public static SelectionManager Instance = new(); - private ColorScheme selectedScheme; - - - public ColorScheme? GetOriginalExplicitColorScheme(Design design) - { - if (oldSchemes.ContainsKey(design)) - return oldSchemes[design]; + private ColorScheme? selectedScheme; - return null; - } /// /// Changes the selection without respecting @@ -99,40 +89,25 @@ private void SetSelection(bool respectLock, Design[] designs) foreach (var d in selection) { - // record the old color scheme so we can get reset it - // later when it is no longer selected - oldSchemes.Add(d, d.View.GetExplicitColorScheme()); - // since the view is selected mark it so d.View.ColorScheme = SelectedScheme; } } - /// - /// Updates the cached known ColorScheme (prior to selection). Use this method - /// if you are making changes to the ColorScheme of an actively selected object - /// - public void UpdateKnownScheme(Design design, ColorScheme? colorScheme) - { - if(oldSchemes.ContainsKey(design)) - { - oldSchemes[design] = colorScheme; - } - } - public void Clear(bool respectLock = true) { if (LockSelection && respectLock) return; + var selected = selection.ToArray(); + selection.Clear(); // reset old color schemes so views don't still look selected - foreach (var kvp in oldSchemes) + foreach (var d in selected) { - kvp.Key.View.ColorScheme = kvp.Value; + d.View.ColorScheme = d.State.OriginalScheme; } - oldSchemes.Clear(); } /// diff --git a/src/ToCode/ColorSchemeProperty.cs b/src/ToCode/ColorSchemeProperty.cs index d45b379f..66a8eada 100644 --- a/src/ToCode/ColorSchemeProperty.cs +++ b/src/ToCode/ColorSchemeProperty.cs @@ -38,10 +38,7 @@ public override CodeExpression GetRhs() } public override object? GetValue() { - if (SelectionManager.Instance.Selected.Contains(Design)) - return SelectionManager.Instance.GetOriginalExplicitColorScheme(Design); - - return Design.View.GetExplicitColorScheme(); + return Design.State.OriginalScheme ?? Design.View.GetExplicitColorScheme(); } protected override string GetHumanReadableValue() { @@ -62,6 +59,6 @@ public override void SetValue(object? value) { base.SetValue(value); - SelectionManager.Instance.UpdateKnownScheme(Design,value as ColorScheme); + Design.State.OriginalScheme = value as ColorScheme; } } diff --git a/src/ToCode/ViewToCode.cs b/src/ToCode/ViewToCode.cs index 5ab08a75..370053c9 100644 --- a/src/ToCode/ViewToCode.cs +++ b/src/ToCode/ViewToCode.cs @@ -31,22 +31,25 @@ public Design GenerateNewView(FileInfo csFilePath, string namespaceName,Type vie var csharpCode = GetGenerateNewViewCode(className, namespaceName); File.WriteAllText(sourceFile.CsFile.FullName, csharpCode); - var view = (View)(Activator.CreateInstance(viewType) ?? throw new Exception($"Could not create instance of Type '{viewType}' ('Activator.CreateInstance' returned null)")); + 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)) { - view.Width = Dim.Fill(); - view.Height = Dim.Fill(); + prototype.Width = Dim.Fill(); + prototype.Height = Dim.Fill(); } - var design = new Design(sourceFile, Design.RootDesignName, view); + // use the prototype to create a designer cs file + var design = new Design(sourceFile, Design.RootDesignName, prototype); design.CreateSubControlDesigns(); GenerateDesignerCs(design, sourceFile,viewType); + // Reload the designer cs file to create a new instance (which is returned). + // NOTE: prototype is not the same instance that is returned; var decompiler = new CodeToView(sourceFile); return decompiler.CreateInstance(); diff --git a/src/UI/Editor.cs b/src/UI/Editor.cs index 5da27472..d9cff379 100644 --- a/src/UI/Editor.cs +++ b/src/UI/Editor.cs @@ -14,6 +14,7 @@ public class Editor : Toplevel private SourceCodeFile? _currentDesignerFile; private bool enableDrag = true; private bool enableShowFocused = true; + public static bool ShowBorders = true; bool _editting = false; @@ -37,6 +38,11 @@ public class Editor : Toplevel /// private Guid? _lastSavedOperation; + /// + /// True to enable experimental features + /// + public static bool Experimental { get; internal set; } + private string GetHelpWithEmptyFormLoaded() { return @$"{_keyMap.AddView} to Add a View"; @@ -54,6 +60,7 @@ private string GetHelp() {_keyMap.ShowColorSchemes} - Color Schemes {_keyMap.ToggleDragging} - Toggle mouse dragging on/off {_keyMap.ToggleShowFocused} - Toggle show focused view field name +{_keyMap.ToggleShowBorders} - Toggle dotted borders for frameless views {_keyMap.EditProperties} - Edit View Properties {_keyMap.ViewSpecificOperations} - View Specific Operations {_keyMap.EditRootProperties} - Edit Root Properties @@ -71,14 +78,14 @@ public Editor() CanFocus = true; // If there are custom keybindings read those - if(File.Exists("Keys.yaml")) + if (File.Exists("Keys.yaml")) { var d = new Deserializer(); try { _keyMap = d.Deserialize(File.ReadAllText("Keys.yaml")); - if(_keyMap.SelectionColor != null) + if (_keyMap.SelectionColor != null) { SelectionManager.Instance.SelectedScheme = _keyMap.SelectionColor.Scheme; } @@ -86,7 +93,7 @@ public Editor() catch (Exception ex) { // if there is bad yaml use the defaults - ExceptionViewer.ShowException("Failed to read keybindings",ex); + ExceptionViewer.ShowException("Failed to read keybindings", ex); _keyMap = new KeyMap(); } } @@ -119,7 +126,7 @@ private void BuildRootMenu() // center all the commands int maxWidth = rootCommands.Max(v => v.Length); - for(int i=0;i v.Name.Equals(options.ViewType))??toCreate; + toCreate = GetSupportedRootViews().FirstOrDefault(v => v.Name.Equals(options.ViewType)) ?? toCreate; } New(toLoadOrCreate, toCreate, options.Namespace); @@ -234,41 +242,44 @@ public void Run(Options options) } }; + Application.RootMouseEvent += (m) => { - // if another window is showing don't respond to mouse if (!IsCurrentTop) return; - + if (_editting || !enableDrag || _viewBeingEdited == null) return; try { - _mouseManager.HandleMouse(m,_viewBeingEdited); + _mouseManager.HandleMouse(m, _viewBeingEdited); + - //right click - if(m.Flags.HasFlag(_keyMap.RightClick)) + if (m.Flags.HasFlag(_keyMap.RightClick)) { var hit = _viewBeingEdited.View.HitTest(m, out _, out _); - - if(hit != null) + + if (hit != null) { var d = hit.GetNearestDesign() ?? _viewBeingEdited; - if(d != null) - CreateAndShowContextMenu(m,d); + if (d != null) + { + CreateAndShowContextMenu(m, d); + } + } } } catch (System.Exception ex) { - ExceptionViewer.ShowException("Error processing mouse",ex); - } + ExceptionViewer.ShowException("Error processing mouse", ex); + } }; - Application.Run(this,ErrorHandler); + Application.Run(this, ErrorHandler); Application.Shutdown(); } @@ -280,12 +291,15 @@ private void CreateAndShowContextMenu(MouseEvent? m, Design? rightClicked) var selected = SelectionManager.Instance.Selected.ToArray(); var factory = new OperationFactory( - (p, v) => { return EditDialog.GetNewValue(p.Design, p, v, out var newValue) ? newValue - : throw new OperationCanceledException(); }); + (p, v) => + { + return EditDialog.GetNewValue(p.Design, p, v, out var newValue) ? newValue + : throw new OperationCanceledException(); + }); var operations = factory .CreateOperations(selected, m, rightClicked, out string name) - .Where(o=>!o.IsImpossible) + .Where(o => !o.IsImpossible) .ToArray(); var setProps = operations.OfType(); @@ -295,15 +309,16 @@ private void CreateAndShowContextMenu(MouseEvent? m, Design? rightClicked) var othersItems = others.Select(ToMenuItem); var all = new List(); - + // only add the set properties category if there are some - if(setPropsItems.Any()) + if (setPropsItems.Any()) all.Add(new MenuBarItem(name, setPropsItems.ToArray()) { - Action = () => { + Action = () => + { if (selected.Length == 1 || rightClicked != null) ShowEditProperties(rightClicked ?? selected[0]); - } + } }); all.AddRange(othersItems.ToArray()); @@ -317,13 +332,13 @@ private void CreateAndShowContextMenu(MouseEvent? m, Design? rightClicked) if (m.HasValue) { - menu.Position = new Point(m.Value.X,m.Value.Y); + menu.Position = new Point(m.Value.X, m.Value.Y); } else { var d = SelectionManager.Instance.Selected.FirstOrDefault() ?? _viewBeingEdited; - d.View.ViewToScreen(0,0,out var x, out var y); - menu.Position = new Point(x,y); + d.View.ViewToScreen(0, 0, out var x, out var y); + menu.Position = new Point(x, y); } _menuOpen = true; @@ -350,7 +365,7 @@ private void Try(Action action) } catch (Exception ex) { - ExceptionViewer.ShowException("Operation failed",ex); + ExceptionViewer.ShowException("Operation failed", ex); } finally { @@ -362,9 +377,9 @@ public override void Redraw(Rect bounds) { base.Redraw(bounds); - + // if we are editing a view - if(_viewBeingEdited != null) + if (_viewBeingEdited != null) { if (enableShowFocused) { @@ -373,30 +388,29 @@ public override void Redraw(Rect bounds) string? toDisplay = GetLowerRightTextIfAny(); // and have a designable view focused - if(toDisplay != null) + if (toDisplay != null) { // write its name in the lower right - int y = Bounds.Height -1; - int right = bounds.Width -1; + int y = Bounds.Height - 1; + int right = bounds.Width - 1; var len = toDisplay.Length; - - for(int i=0;i!d.IsRoot) + .Where(d => !d.IsRoot) .ToArray(); SelectionManager.Instance.ForceSetSelection(everyone); @@ -625,7 +646,7 @@ private void Paste() { var paste = new PasteOperation(d); - if(paste.IsImpossible) + if (paste.IsImpossible) return; OperationManager.Instance.Do(paste); @@ -644,9 +665,9 @@ private void ShowViewSpecificOperations() if (d != null) { - var options = d.GetExtraOperations().Where(o=>!o.IsImpossible).ToArray(); + var options = d.GetExtraOperations().Where(o => !o.IsImpossible).ToArray(); - if(options.Any() && Modals.Get("Operations","Ok",options, out var selected) && selected != null) + if (options.Any() && Modals.Get("Operations", "Ok", options, out var selected) && selected != null) { OperationManager.Instance.Do(selected); } @@ -660,25 +681,25 @@ private void ShowHelp() private void MoveControl(int deltaX, int deltaY) { - DoForSelectedViews((d)=>new MoveViewOperation(d,deltaX,deltaY)); + DoForSelectedViews((d) => new MoveViewOperation(d, deltaX, deltaY)); } private void Delete() { - if(_viewBeingEdited == null) + if (_viewBeingEdited == null) return; - - if(SelectionManager.Instance.Selected.Any()) + + if (SelectionManager.Instance.Selected.Any()) { - var cmd = new DeleteViewOperation(SelectionManager.Instance.Selected.Select(d=>d.View).ToArray()); + var cmd = new DeleteViewOperation(SelectionManager.Instance.Selected.Select(d => d.View).ToArray()); OperationManager.Instance.Do(cmd); } - + } - private void DoForSelectedViews(Func operationFuc, bool allowOnRoot=false) + private void DoForSelectedViews(Func operationFuc, bool allowOnRoot = false) { - if(_viewBeingEdited == null) + if (_viewBeingEdited == null) return; var selected = SelectionManager.Instance.Selected.ToArray(); @@ -691,14 +712,14 @@ private void DoForSelectedViews(Func operationFuc, bool allow OperationManager.Instance.Do(op); } - else if(selected.Length == 1) + else if (selected.Length == 1) { 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( @@ -712,8 +733,8 @@ private void Open() { var ofd = new OpenDialog("Open", $"Select {SourceCodeFile.ExpectedExtension} file", new List(new[] { SourceCodeFile.ExpectedExtension })); - - Application.Run(ofd,ErrorHandler); + + Application.Run(ofd, ErrorHandler); if (!ofd.Canceled) { @@ -735,7 +756,7 @@ private void Open() private bool ErrorHandler(Exception arg) { - ExceptionViewer.ShowException("Global Exception",arg); + ExceptionViewer.ShowException("Global Exception", arg); return true; } @@ -749,16 +770,18 @@ private void Open(FileInfo toOpen) Design? instance = null; SelectionManager.Instance.Clear(); - Task.Run(()=>{ - + Task.Run(() => + { + var decompiler = new CodeToView(new SourceCodeFile(toOpen)); _currentDesignerFile = decompiler.SourceFile; instance = decompiler.CreateInstance(); - }).ContinueWith((t,o)=>{ + }).ContinueWith((t, o) => + { // no longer loading - Application.MainLoop.Invoke(()=>Application.RequestStop()); + Application.MainLoop.Invoke(() => Application.RequestStop()); if (t.Exception != null) { @@ -771,15 +794,15 @@ private void Open(FileInfo toOpen) if (instance != null) ReplaceViewBeingEdited(instance); - },TaskScheduler.FromCurrentSynchronizationContext()); + }, TaskScheduler.FromCurrentSynchronizationContext()); - Application.Run(open,ErrorHandler); + Application.Run(open, ErrorHandler); } private void New() { - if(!Modals.Get("Create New View","Ok",GetSupportedRootViews(),out var selected)) + if (!Modals.Get("Create New View", "Ok", GetSupportedRootViews(), out var selected)) { return; } @@ -805,31 +828,31 @@ private void New() return; var file = new FileInfo(path); - + // Check if we are about to overwrite some files // and if so warn the user var files = new SourceCodeFile(file); var sb = new StringBuilder(); - - if(files.CsFile.Exists) + + if (files.CsFile.Exists) { sb.AppendLine(files.CsFile.Name); } - if(files.DesignerFile.Exists) + if (files.DesignerFile.Exists) { sb.AppendLine(files.DesignerFile.Name); } - if(sb.Length > 0) + if (sb.Length > 0) { - var chosen = MessageBox.Query("Overwrite Files?",$"The following files will be overwritten:{Environment.NewLine}{sb.ToString().TrimEnd()}","Ok","Cancel"); - + var chosen = MessageBox.Query("Overwrite Files?", $"The following files will be overwritten:{Environment.NewLine}{sb.ToString().TrimEnd()}", "Ok", "Cancel"); + // user cancelled - if(chosen != 0) + if (chosen != 0) return; } - New(file,selected,null); + New(file, selected, null); } catch (Exception ex) { @@ -841,7 +864,13 @@ private void New() private Type[] GetSupportedRootViews() { - return new Type[] { typeof(Window), typeof(Dialog) }; + // TODO: When more robust, remove these from experimental status + if(Editor.Experimental) + { + return new Type[] { typeof(Window), typeof(Dialog), typeof(View) , typeof(Toplevel)}; + } + + return new Type[] { typeof(Window), typeof(Dialog)}; } private void New(FileInfo toOpen, Type typeToCreate, string? explicitNamespace) @@ -859,11 +888,11 @@ private void New(FileInfo toOpen, Type typeToCreate, string? explicitNamespace) return; } } - + //Validate the namespace 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"); + MessageBox.ErrorQuery("Invalid Namespace", "Namespace must not contain spaces, be empty or begin with a number", "Ok"); return; } @@ -875,12 +904,14 @@ private void New(FileInfo toOpen, Type typeToCreate, string? explicitNamespace) var open = new LoadingDialog(toOpen); - Task.Run(() => { + Task.Run(() => + { // Create the view files and compile instance = viewToCode.GenerateNewView(toOpen, ns ?? "YourNamespace", typeToCreate, out _currentDesignerFile); - }).ContinueWith((t, o) => { + }).ContinueWith((t, o) => + { // no longer loading Application.MainLoop.Invoke(() => Application.RequestStop()); @@ -899,7 +930,7 @@ private void New(FileInfo toOpen, Type typeToCreate, string? explicitNamespace) }, TaskScheduler.FromCurrentSynchronizationContext()); - Application.Run(open,ErrorHandler); + Application.Run(open, ErrorHandler); } private void ReplaceViewBeingEdited(Design design) @@ -944,7 +975,7 @@ private void Save() _lastSavedOperation = OperationManager.Instance.GetLastAppliedOperation()?.UniqueIdentifier; } - + public bool HasUnsavedChanges() { var savedOp = _lastSavedOperation; @@ -972,7 +1003,7 @@ private void ShowAddViewWindow() var toAddTo = SelectionManager.Instance.GetMostSelectedContainerOrNull() ?? _viewBeingEdited; OperationManager.Instance.Do( - new AddViewOperation(_currentDesignerFile,toAddTo) + new AddViewOperation(_currentDesignerFile, toAddTo) ); } @@ -989,15 +1020,15 @@ private void ShowEditProperties() private void ShowEditProperties(Design d) { var edit = new EditDialog(d); - Application.Run(edit,ErrorHandler); + Application.Run(edit, ErrorHandler); } private void ShowColorSchemes() { - if(_viewBeingEdited == null) + if (_viewBeingEdited == null) return; - + var schemes = new ColorSchemesUI(_viewBeingEdited); - Application.Run(schemes); + Application.Run(schemes); } } diff --git a/src/UI/KeyMap.cs b/src/UI/KeyMap.cs index 970f652a..a23d708f 100644 --- a/src/UI/KeyMap.cs +++ b/src/UI/KeyMap.cs @@ -19,6 +19,7 @@ public class KeyMap public Key ToggleDragging { get; set; } = Key.F3; public Key AddView { get; set; } = Key.F2; public Key ToggleShowFocused { get; set; } = Key.CtrlMask | Key.L; + public Key ToggleShowBorders { get; set; } = Key.CtrlMask | Key.B; public MouseFlags RightClick { get; set; } = MouseFlags.Button3Clicked; public Key Copy { get; set; } = Key.CtrlMask | Key.C; public Key Paste { get; set; } = Key.CtrlMask | Key.V; diff --git a/src/UI/MouseManager.cs b/src/UI/MouseManager.cs index c8b906fd..8b2bd6dc 100644 --- a/src/UI/MouseManager.cs +++ b/src/UI/MouseManager.cs @@ -114,10 +114,10 @@ public void HandleMouse(MouseEvent m, Design viewBeingEdited) if (SelectionStart != null && SelectionBox != null && SelectionContainer != null) { SelectionManager.Instance.SetSelection( - SelectionContainer.Subviews + SelectionContainer.GetActualSubviews() .Where(v => v.IntersectsScreenRect(SelectionBox.Value)) .Select(v=>v.GetNearestDesign()) - .Where(d=>d !=null) + .Where(d=>d !=null && !d.IsRoot) .Cast() .ToArray() ); diff --git a/src/ViewExtensions.cs b/src/ViewExtensions.cs index de419b14..d1eff0e4 100644 --- a/src/ViewExtensions.cs +++ b/src/ViewExtensions.cs @@ -17,7 +17,7 @@ public static class ViewExtensions /// public static IList GetActualSubviews(this View v) { - if(v is Window w) + if (v is Window w) { return w.Subviews[0].Subviews; } @@ -27,7 +27,7 @@ public static IList GetActualSubviews(this View v) } if (v is TabView t) { - return t.Tabs.Select(tab=>tab.View).Where(v=>v!=null).ToList(); + return t.Tabs.Select(tab => tab.View).Where(v => v != null).ToList(); } return v.Subviews; @@ -118,10 +118,10 @@ public static string GetActualText(this View view) /// public static Design? GetNearestDesign(this View view) { - if(view is null) + if (view is null) return null; - if(view.Data is Design d) + if (view.Data is Design d) { return d; } @@ -150,7 +150,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. /// @@ -183,13 +182,52 @@ public static void ViewToScreen(this View v, int col, int row, out int rcol, out public static bool IsContainerView(this View v) { var type = v.GetType(); + + if(v.Data is Design d) + { + // The root class user is designing (e.g. MyView) could be inheriting from + // TopLevel or View in which case we must allow dropping into it + if (d.IsRoot) + { + return true; + } + } + // TODO: are there any others? return v is ScrollView || v is TabView || v is FrameView || v is Window || type == typeof(View) || type.Name.Equals("ContentView"); } - public static View? HitTest(this View w, MouseEvent m, out bool isBorder,out bool isLowerRight, params View[] ignoring) + public static bool IsBorderlessContainerView(this View v) + { + var type = v.GetType(); + + // TODO: are there any others? + if (type == typeof(View) && v.IsBorderless()) + return true; + + if (v is TabView tabView) + { + return !tabView.Style.ShowBorder || tabView.Style.TabsOnBottom; + } + + return false; + } + + public static bool IsBorderless(this View v) + { + if (v.Border == null) + return true; + + if (v.Border.BorderStyle == BorderStyle.None) + return true; + + return false; + } + + + public static View? HitTest(this View w, MouseEvent m, out bool isBorder, out bool isLowerRight, params View[] ignoring) { // hide the views while we perform the hit test - foreach(View v in ignoring) + foreach (View v in ignoring) { v.Visible = false; } @@ -198,18 +236,21 @@ public static bool IsContainerView(this View v) var hit = ApplicationExtensions.FindDeepestView(w, m.X, m.Y); int resizeBoxArea = 2; - + + // get nearest Design to the clicked thing (avoids issues with ContentView and other wierd subviews etc) + hit = hit?.GetNearestDesign()?.View; + if (hit != null) { - hit.ViewToScreen(hit.Bounds.Right, hit.Bounds.Bottom,out int lowerRightX, out int lowerRightY,true); - hit.ViewToScreen(0, 0,out int upperLeftX, out int upperLeftY,true); + hit.ViewToScreen(hit.Bounds.Right, hit.Bounds.Bottom, out int lowerRightX, out int lowerRightY, true); + hit.ViewToScreen(0, 0, out int upperLeftX, out int upperLeftY, true); isLowerRight = Math.Abs(lowerRightX - point.X) <= resizeBoxArea && Math.Abs(lowerRightY - point.Y) <= resizeBoxArea; - isBorder = - m.X == lowerRightX-1 || + isBorder = + m.X == lowerRightX - 1 || m.X == upperLeftX || - m.Y == lowerRightY-1 || + m.Y == lowerRightY - 1 || m.Y == upperLeftY; } else @@ -249,7 +290,7 @@ public static bool IntersectsScreenRect(this View v, Rect screenRect) v.ViewToScreen(0, 0, out var x0, out var y0); v.ViewToScreen(v.Bounds.Width, v.Bounds.Height, out var x1, out var y1); - return Rect.FromLTRB(x0,y0,x1,y1).IntersectsWith(screenRect); + return Rect.FromLTRB(x0, y0, x1, y1).IntersectsWith(screenRect); } @@ -260,7 +301,7 @@ public static bool IntersectsScreenRect(this View v, Rect screenRect) /// 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); @@ -273,6 +314,6 @@ public static bool IntersectsScreenRect(this View v, Rect screenRect) /// public static IEnumerable OrderViewsByScreenPosition(IEnumerable views) { - return views.OrderBy(v=>v.Frame.Y).ThenBy(v=>v.Frame.X); + return views.OrderBy(v => v.Frame.Y).ThenBy(v => v.Frame.X); } } diff --git a/src/ViewFactory.cs b/src/ViewFactory.cs index 25268acb..571846ee 100644 --- a/src/ViewFactory.cs +++ b/src/ViewFactory.cs @@ -40,9 +40,10 @@ public View Create(Type t) return CreateMenuBar(); } - if(t == typeof(TextValidateField)) + if (t == typeof(TextValidateField)) { - return new TextValidateField{ + return new TextValidateField + { Provider = new TextRegexProvider(".*"), Text = "Heya", @@ -51,27 +52,46 @@ public View Create(Type t) }; } - if(t == typeof(ProgressBar)) + if (t == typeof(ProgressBar)) { - return new ProgressBar{ + return new ProgressBar + { Width = 10, Height = 1, Fraction = 1f }; } - if(typeof(GraphView).IsAssignableFrom(t)) + if (t == typeof(View)) + { + return new View + { + Width = 10, + Height = 5, + }; + } + if (t == typeof(TextField)) + { + return new TextField + { + Width = 10, + Height = 1, + }; + } + if (typeof(GraphView).IsAssignableFrom(t)) { - return new GraphView{ + return new GraphView + { Width = 20, Height = 5, - GraphColor = Attribute.Make(Color.White,Color.Black) + GraphColor = Attribute.Make(Color.White, Color.Black) }; } - if(typeof(ListView).IsAssignableFrom(t)) + if (typeof(ListView).IsAssignableFrom(t)) { - var lv = new ListView(new List{"Item1","Item2","Item3"}){ + var lv = new ListView(new List { "Item1", "Item2", "Item3" }) + { Width = 20, Height = 3, }; @@ -79,16 +99,18 @@ public View Create(Type t) return lv; } - if(t == typeof(LineView)) + if (t == typeof(LineView)) { - return new LineView(){ + return new LineView() + { Width = 8, Height = 1 }; } - if(t == typeof(TreeView)) + if (t == typeof(TreeView)) { - return new TreeView(){ + return new TreeView() + { Width = 16, Height = 5 }; @@ -136,7 +158,7 @@ private View CreateRadioGroup() Width = 10, Height = 5, }; - group.RadioLabels = new ustring[]{"Option 1","Option 2"}; + group.RadioLabels = new ustring[] { "Option 1", "Option 2" }; return group; } @@ -159,14 +181,14 @@ private TableView CreateTableView() private TabView CreateTabView() { - var tabView = new TabView + var tabView = new TabView { Width = 50, Height = 5, }; - tabView.AddTab(new TabView.Tab("Tab1", new View{Width = Dim.Fill(),Height=Dim.Fill()}),false); - tabView.AddTab(new TabView.Tab("Tab2", new View{Width = Dim.Fill(),Height=Dim.Fill()}),false); + tabView.AddTab(new TabView.Tab("Tab1", new View { Width = Dim.Fill(), Height = Dim.Fill() }), false); + tabView.AddTab(new TabView.Tab("Tab2", new View { Width = Dim.Fill(), Height = Dim.Fill() }), false); return tabView; } @@ -184,10 +206,10 @@ internal IEnumerable GetSupportedViews() typeof(ScrollBarView), typeof(TreeView<>)}; // The generic version of TreeView - return typeof(View).Assembly.DefinedTypes.Where(t => - typeof(View).IsAssignableFrom(t) && + return typeof(View).Assembly.DefinedTypes.Where(t => + typeof(View).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract && t.IsPublic ).Except(exclude) - .OrderBy(t=>t.Name).ToArray(); + .OrderBy(t => t.Name).ToArray(); } } diff --git a/tests/ColorSchemeTests.cs b/tests/ColorSchemeTests.cs index 86e11764..982cc5cb 100644 --- a/tests/ColorSchemeTests.cs +++ b/tests/ColorSchemeTests.cs @@ -28,12 +28,16 @@ public void TestHasColorScheme(bool whenMultiSelected) Assert.IsNotNull(d.View.ColorScheme); Assert.IsFalse(d.HasKnownColorScheme()); - d.View.ColorScheme = new ColorScheme(); - + var scheme = new ColorScheme(); + var prop = new SetPropertyOperation(d , d.GetDesignableProperty(nameof(View.ColorScheme)) + ?? throw new Exception("Expected Property did not exist or was not designable"),null,scheme); + + prop.Do(); + // we still don't know about this scheme yet Assert.IsFalse(d.HasKnownColorScheme()); - ColorSchemeManager.Instance.AddOrUpdateScheme("fff",d.View.ColorScheme); + ColorSchemeManager.Instance.AddOrUpdateScheme("fff", scheme); if (whenMultiSelected) { @@ -185,7 +189,7 @@ public void TestColorScheme_RoundTrip(bool multiSelectBeforeSaving) { //unselect it so it is rendered with correct scheme SelectionManager.Instance.Clear(); - l.ColorScheme = mgr.Schemes.Single().Scheme; + l.ColorScheme = d.State.OriginalScheme = mgr.Schemes.Single().Scheme; if (multiSelectBeforeSaving) { diff --git a/tests/DragOperationTests.cs b/tests/DragOperationTests.cs index 1c79dac6..83879400 100644 --- a/tests/DragOperationTests.cs +++ b/tests/DragOperationTests.cs @@ -94,12 +94,15 @@ public void TestSimpleDrag_IntoAnotherView() Width = 5, Height = 4 }; + container1.Data = new Design(d.SourceCode, "v1", container1); + var container2 = new View{ X=5, Y=6, Width = 5, Height = 4 }; + container2.Data = new Design(d.SourceCode, "v2", container2); d.View.Add(container1); d.View.Add(container2); diff --git a/tests/TabViewTests.cs b/tests/TabViewTests.cs index 874765ee..6179fade 100644 --- a/tests/TabViewTests.cs +++ b/tests/TabViewTests.cs @@ -208,4 +208,30 @@ public void TestGetAllDesigns_TabView() Assert.AreEqual(3,tvDesign.GetAllDesigns().Count(),$"Expected only 3 Designs but they were {string.Join(",",tvDesign.GetAllDesigns())}"); } + + [Test] + public void TabView_IsBorderless_DependsOnShowBorder() + { + var inst = new TabView(); + + Assert.IsTrue(inst.Style.ShowBorder); + Assert.False(inst.IsBorderlessContainerView()); + + inst.Style.ShowBorder = false; + + Assert.True(inst.IsBorderlessContainerView()); + } + + [Test] + public void TabView_IsBorderless_DependsOnTabsOnBottom() + { + var inst = new TabView(); + + Assert.IsFalse(inst.Style.TabsOnBottom); + Assert.False(inst.IsBorderlessContainerView()); + + inst.Style.TabsOnBottom = true; + + Assert.True(inst.IsBorderlessContainerView()); + } } \ No newline at end of file diff --git a/tests/ViewExtensionsTests.cs b/tests/ViewExtensionsTests.cs index 2cc5bbe1..a4e8184c 100644 --- a/tests/ViewExtensionsTests.cs +++ b/tests/ViewExtensionsTests.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using System; using Terminal.Gui; using TerminalGuiDesigner; @@ -22,12 +23,12 @@ public class ViewExtensionsTests : Tests [TestCase(3,4,true,false,false)] public void TestHitTest(int x, int y, bool hit, bool border, bool lowerRight) { - var v = new View{ - X=2, - Y=3, - Width = 5, - Height = 3 - }; + var v = Get10By10View().View; + + v.X = 2; + v.Y = 3; + v.Width = 5; + v.Height = 3; Application.Top.Add(v); bool isLowerRight; @@ -46,4 +47,30 @@ public void TestHitTest(int x, int y, bool hit, bool border, bool lowerRight) Assert.AreEqual(lowerRight, isLowerRight); Assert.AreEqual(border,isBorder); } + + [TestCase(typeof(Label), false)] + [TestCase(typeof(TableView), false)] + [TestCase(typeof(TabView), true)] + [TestCase(typeof(View), true)] + [TestCase(typeof(Window), true)] + public void TestIsContainerView(Type viewType, bool expectIsContainerView) + { + var inst = (View?)Activator.CreateInstance(viewType) + ?? throw new Exception("CreateInstance returned null!"); + + Assert.AreEqual(expectIsContainerView,inst.IsContainerView()); + } + + [TestCase(typeof(Label), false)] + [TestCase(typeof(TableView), false)] + [TestCase(typeof(TabView), false)] + [TestCase(typeof(Window), false)] + [TestCase(typeof(View), true)] + public void TestOutOfBox_IsBorderlessContainerView(Type viewType, bool expectResult) + { + var inst = (View?)Activator.CreateInstance(viewType) + ?? throw new Exception("CreateInstance returned null!"); + + Assert.AreEqual(expectResult, inst.IsBorderlessContainerView()); + } }