Skip to content

Commit ec01d50

Browse files
authored
Merge pull request #317 from gui-cs/better-response-when-ref
When trying to delete a referenced view, give feedback about the error
2 parents 5ae21fd + 492a956 commit ec01d50

8 files changed

Lines changed: 420 additions & 4 deletions

File tree

src/Operations/DeleteViewOperation.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ public class DeleteViewOperation : Operation
1313
private readonly View[] from;
1414
private readonly Design[] originalSelection;
1515

16+
/// <summary>
17+
/// Views which reference the <see cref="Design"/> being operated on and so would
18+
/// crash or break if the view were deleted
19+
/// </summary>
20+
public Design[] PreventDeleting { get; set; }
21+
1622
/// <summary>
1723
/// Initializes a new instance of the <see cref="DeleteViewOperation"/> class.
1824
/// </summary>
@@ -34,7 +40,9 @@ public DeleteViewOperation(params Design[] delete)
3440

3541
// there are view(s) that depend on us (e.g. for positioning)
3642
// that are not also being deleted themselves
37-
if (design.GetDependantDesigns().Any(dep => !delete.Contains(dep)))
43+
PreventDeleting = design.GetDependantDesigns().Where(dep => !delete.Contains(dep)).ToArray();
44+
45+
if (PreventDeleting.Length > 0)
3846
{
3947
// Prevent deleting - so there are no orphan references from existing Views e.g. Pos.Right(thingIJustDeleted);
4048
this.IsImpossible = true;

src/Operations/DragOperation.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ public partial class DragOperation : Operation
1414

1515
private View? dropInto;
1616

17+
18+
/// <summary>
19+
/// Views which reference the <see cref="Design"/> being operated on and so would
20+
/// crash or break if the view were moved to another container (see <see cref="DropInto"/>)
21+
/// </summary>
22+
public Design[] PreventDrag { get; set; }
23+
1724
/// <summary>
1825
/// Initializes a new instance of the <see cref="DragOperation"/> class.
1926
/// Begins a drag operation in which <paramref name="beingDragged"/> is moved.
@@ -136,9 +143,17 @@ public View? DropInto
136143
}
137144

138145
this.dropInto = value;
146+
147+
148+
IsImpossible = dropInto != null && IsChangingContainer() && this.mementos.Any(HasDependantsThatAreNotAlsoPartOfDrag);
139149
}
140150
}
141151

152+
private bool IsChangingContainer()
153+
{
154+
return mementos.Any(m => m.OriginalSuperView != DropInto);
155+
}
156+
142157
/// <summary>
143158
/// Moves all dragged views back to original positions.
144159
/// </summary>
@@ -173,6 +188,18 @@ public void ContinueDrag(Point dest)
173188
}
174189
}
175190

191+
private bool HasDependantsThatAreNotAlsoPartOfDrag(DragMemento arg)
192+
{
193+
// there are view(s) that depend on us (e.g. for positioning)
194+
// that are not also being deleted themselves
195+
196+
var alsoBeingDragged = new HashSet<Design>(mementos.Select(m => m.Design));
197+
PreventDrag = arg.Design.GetDependantDesigns().Where(dep => !alsoBeingDragged.Contains(dep)).ToArray();
198+
199+
return PreventDrag.Any();
200+
}
201+
202+
176203
/// <inheritdoc/>
177204
protected override bool DoImpl()
178205
{
@@ -260,4 +287,13 @@ private void ContinueDrag(DragMemento mem, Point dest)
260287

261288
this.DestinationY = dest.Y;
262289
}
290+
291+
/// <summary>
292+
/// Restores all mementos to original locations
293+
/// </summary>
294+
/// <exception cref="NotImplementedException"></exception>
295+
public void Abandon()
296+
{
297+
UndoImpl();
298+
}
263299
}

src/UI/Editor.cs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ namespace TerminalGuiDesigner.UI;
2626
/// application. Hooks key and mouse events and mounts as a sub-view whatever file
2727
/// the user opens.
2828
/// </summary>
29-
public class Editor : Toplevel
29+
public class Editor : Toplevel, IErrorReporter
3030
{
3131
private KeyMap keyMap;
3232
private readonly KeyboardManager keyboardManager;
@@ -61,6 +61,11 @@ public class Editor : Toplevel
6161
/// </summary>
6262
public static bool Quiet = false;
6363

64+
/// <summary>
65+
/// Start your message with this if you want it to be visually highlighted as bad.
66+
/// </summary>
67+
public const string Error = "Error";
68+
6469
/// <summary>
6570
/// Initializes a new instance of the <see cref="Editor"/> class.
6671
/// </summary>
@@ -77,7 +82,10 @@ public Editor()
7782
LoadKeyMap();
7883

7984
this.keyboardManager = new KeyboardManager(this.keyMap);
80-
this.mouseManager = new MouseManager();
85+
this.mouseManager = new MouseManager()
86+
{
87+
ErrorReporter = this
88+
};
8189
this.Closing += this.Editor_Closing;
8290

8391
this.BuildRootMenu();
@@ -293,6 +301,13 @@ protected override void OnDrawComplete(DrawContext? context)
293301
// and have a designable view focused
294302
if (toDisplay != null)
295303
{
304+
var before = GetCurrentAttribute();
305+
306+
if (toDisplay.StartsWith(Error))
307+
{
308+
SetAttribute(new Attribute(Color.Red, Color.Black));
309+
}
310+
296311
// write its name in the lower right
297312
int y = this.GetContentSize().Height - 1;
298313
int right = bounds.Width - 1;
@@ -303,6 +318,8 @@ protected override void OnDrawComplete(DrawContext? context)
303318
{
304319
this.AddRune(right - len + i, y, runes[i]);
305320
}
321+
322+
SetAttribute(before);
306323
}
307324
}
308325

@@ -1151,10 +1168,31 @@ private void Delete()
11511168
if (SelectionManager.Instance.Selected.Any())
11521169
{
11531170
var cmd = new DeleteViewOperation(SelectionManager.Instance.Selected.ToArray());
1171+
1172+
1173+
if (cmd.IsImpossible && cmd.PreventDeleting.Any())
1174+
{
1175+
ShowErrorThatViewIsUsedByOthers(cmd.PreventDeleting);
1176+
return;
1177+
}
1178+
11541179
OperationManager.Instance.Do(cmd);
11551180
}
11561181
}
11571182

1183+
public void ShowErrorThatViewIsUsedByOthers(Design[] usedBy)
1184+
{
1185+
if (usedBy.Length == 1)
1186+
{
1187+
flashMessage = $"{Error}, view referenced by " + usedBy[0].FieldName;
1188+
}
1189+
else
1190+
{
1191+
flashMessage = $"{Error}, view referenced by {usedBy.Length} views";
1192+
}
1193+
this.SetNeedsDraw();
1194+
}
1195+
11581196
private void DoForSelectedViews(Func<Design, Operation> operationFunc, bool allowOnRoot = false)
11591197
{
11601198
if (this.viewBeingEdited == null)
@@ -1469,4 +1507,4 @@ private void ShowEditProperties(Design d)
14691507
var edit = new EditDialog(d);
14701508
Application.Run(edit, this.ErrorHandler);
14711509
}
1472-
}
1510+
}

src/UI/IErrorReporter.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace TerminalGuiDesigner.UI;
2+
3+
public interface IErrorReporter
4+
{
5+
public void ShowErrorThatViewIsUsedByOthers(Design[] usedBy);
6+
}

src/UI/MouseManager.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ public class MouseManager
3333
/// </summary>
3434
public Rectangle? SelectionBox => RectExtensions.FromBetweenPoints(this.selectionStart, this.selectionEnd);
3535

36+
/// <summary>
37+
/// Set to report impossible commands as they are executed i.e. to give info
38+
/// to user about why operation failed etc.
39+
/// </summary>
40+
public IErrorReporter? ErrorReporter;
41+
3642
/// <summary>
3743
/// Responds to <see cref="Application.MouseEvent"/>(by changing a 'drag a box' selection area
3844
/// or starting a resize etc).
@@ -198,6 +204,13 @@ public void HandleMouse(MouseEventArgs m, Design viewBeingEdited)
198204
// we are dragging into a new container
199205
this.dragOperation.DropInto = into.View;
200206

207+
208+
if (this.dragOperation.IsImpossible && this.dragOperation.PreventDrag.Any())
209+
{
210+
ErrorReporter?.ShowErrorThatViewIsUsedByOthers(this.dragOperation.PreventDrag);
211+
this.dragOperation.Abandon();
212+
}
213+
201214
// end drag
202215
OperationManager.Instance.Do(this.dragOperation);
203216
this.dragOperation = null;

tests/Operations/DeleteViewOperationTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,49 @@ public void TestDeleting_ClearsSelection(bool lockSelection)
103103
ClassicAssert.AreEqual(2, designOut.GetAllDesigns().Count());
104104
ClassicAssert.Contains(lbl1Design, SelectionManager.Instance.Selected.ToArray(), "Undoing a delete operation should restore the previous selection");
105105
}
106+
107+
[Test]
108+
public void TestPreventDeleting_PopulatedWhenDependenciesExist()
109+
{
110+
var viewToCode = new ViewToCode();
111+
var file = new FileInfo("TestPreventDeleting_PopulatedWhenDependenciesExist.cs");
112+
var designOut = viewToCode.GenerateNewView(file, "YourNamespace", typeof(View));
113+
114+
var lbl1 = ViewFactory.Create<Label>();
115+
var lbl2 = ViewFactory.Create<Label>();
116+
117+
// add 2 labels
118+
new AddViewOperation(lbl1, designOut, "lbl1").Do();
119+
new AddViewOperation(lbl2, designOut, "lbl2").Do();
120+
121+
// Add dependency: lbl2 depends on lbl1
122+
lbl2.X = Pos.Right(lbl1) + 5;
123+
124+
var cmd = new DeleteViewOperation((Design)lbl1.Data);
125+
126+
ClassicAssert.IsTrue(cmd.IsImpossible, "Deleting lbl1 should be impossible because lbl2 depends on it");
127+
ClassicAssert.AreEqual(1, cmd.PreventDeleting.Length, "PreventDeleting should contain exactly one dependent design");
128+
ClassicAssert.AreSame(lbl2.Data, cmd.PreventDeleting[0], "PreventDeleting should contain lbl2 because it depends on lbl1");
129+
}
130+
131+
[Test]
132+
public void TestPreventDeleting_EmptyWhenNoDependencies()
133+
{
134+
var viewToCode = new ViewToCode();
135+
var file = new FileInfo("TestPreventDeleting_EmptyWhenNoDependencies.cs");
136+
var designOut = viewToCode.GenerateNewView(file, "YourNamespace", typeof(View));
137+
138+
var lbl1 = ViewFactory.Create<Label>();
139+
var lbl2 = ViewFactory.Create<Label>();
140+
141+
// add 2 labels (no dependencies between them)
142+
new AddViewOperation(lbl1, designOut, "lbl1").Do();
143+
new AddViewOperation(lbl2, designOut, "lbl2").Do();
144+
145+
var cmd = new DeleteViewOperation((Design)lbl1.Data);
146+
147+
ClassicAssert.IsFalse(cmd.IsImpossible, "Deleting lbl1 should be possible because nothing depends on it");
148+
ClassicAssert.IsEmpty(cmd.PreventDeleting, "PreventDeleting should be empty when no dependents exist");
149+
}
150+
106151
}

0 commit comments

Comments
 (0)