Bug 578835 - Improve Sleak usability

- Only one click is needed for Snap & Diff now
- Next click automatically creates new snapshot & shows a diff
- Using SahForm to allow resize left/right panes
- Moved "save" buttons to same line with "Snap & Diff"
- Removed space at the label end
- Improved checkbox labels
- Moved "Stack" to the same row with combo box

Change-Id: I7c69e47bbf6ba0a1f9ec9038f581eff6566b0318
Reviewed-on: https://git.eclipse.org/r/c/platform/eclipse.platform.swt/+/190964
Reviewed-by: Alexander Kurtakov <akurtako@redhat.com>
Reviewed-by: Andrey Loskutov <loskutov@gmx.de>
Tested-by: Andrey Loskutov <loskutov@gmx.de>
diff --git a/bundles/org.eclipse.swt.tools.spies/src/org/eclipse/swt/tools/internal/Sleak.java b/bundles/org.eclipse.swt.tools.spies/src/org/eclipse/swt/tools/internal/Sleak.java
index 5358aab..f64394f 100644
--- a/bundles/org.eclipse.swt.tools.spies/src/org/eclipse/swt/tools/internal/Sleak.java
+++ b/bundles/org.eclipse.swt.tools.spies/src/org/eclipse/swt/tools/internal/Sleak.java
@@ -15,9 +15,11 @@
 
 import java.io.*;
 import java.util.*;
+import java.util.function.*;
 import java.util.stream.*;
 
 import org.eclipse.swt.*;
+import org.eclipse.swt.custom.*;
 import org.eclipse.swt.graphics.*;
 import org.eclipse.swt.layout.*;
 import org.eclipse.swt.widgets.*;
@@ -33,17 +35,17 @@
 public class Sleak {
 	List list;
 	Canvas canvas;
-	Button enableTracking, snapshot, diff, stackTrace, saveAs, save;
+	Button enableTracking, diff, stackTrace, saveAs, save;
 	Combo diffType;
 	Text text;
 	Label label;
 
 	String filterPath = "";
 	String fileName = "sleakout";
-	String selectedName = null;
+	String selectedName;
 	boolean incrementFileNames = true;
 	boolean saveImages = true;
-	int fileCount = 0;
+	int fileCount;
 
 	java.util.List<ObjectWithError> oldObjects = new ArrayList<> ();
 	java.util.List<ObjectWithError> objects = new ArrayList<> ();
@@ -90,49 +92,84 @@
 }
 
 public void create (Composite parent) {
-	enableTracking = new Button (parent, SWT.CHECK);
-	enableTracking.setText ("Enable");
+
+	SashForm sash = new SashForm(parent, SWT.SMOOTH);
+	sash.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+	sash.setOrientation(SWT.HORIZONTAL);
+	sash.setVisible(true);
+
+	Composite left = new Composite(sash, 0);
+	left.setLayout(new GridLayout());
+	left.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+	Composite right = new Composite(sash, 0);
+	right.setLayout(new GridLayout());
+
+	sash.setWeights(new int[] {40,60});
+
+	// Right side
+	canvas = new Canvas (right, SWT.BORDER);
+	canvas.addListener (SWT.Paint, event -> paintCanvas (event));
+	canvas.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 10));
+	text = new Text (right, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
+	text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 10));
+	setVisible(text,  false);
+
+	// Left side
+	enableTracking = new Button (left, SWT.CHECK);
+	enableTracking.setText ("Enable resource tracking");
 	enableTracking.setToolTipText("Enable Device resource tracking. Only resources allocated once enabled will be tracked. To track devices created before view is created, turn on tracing options, see https://www.eclipse.org/swt/tools.php");
 	enableTracking.addListener (SWT.Selection, e -> toggleEnableTracking ());
 	enableTracking.setSelection(enableTracking.getDisplay().isTracking());
-	canvas = new Canvas (parent, SWT.BORDER);
-	canvas.addListener (SWT.Paint, event -> paintCanvas (event));
-	canvas.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 10));
-	text = new Text (parent, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
-	text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 10));
-	setVisible(text,  false);
-	snapshot = new Button (parent, SWT.PUSH);
-	snapshot.setText ("Snap");
-	snapshot.addListener (SWT.Selection, event -> refreshAll ());
-	snapshot.setLayoutData(new GridData(SWT.FILL, SWT.NONE, false, false));
-	diff = new Button (parent, SWT.PUSH);
-	diff.setText ("Diff");
+	enableTracking.setLayoutData(new GridData(SWT.NONE, SWT.NONE, false, false));
+
+	Composite buttons = new Composite(left, 0);
+	buttons.setLayout(new GridLayout(4, false));
+	buttons.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false));
+
+	diff = new Button (buttons, SWT.PUSH);
+	diff.setText ("Snap && Diff");
 	diff.addListener (SWT.Selection, event -> refreshDifference ());
-	diff.setLayoutData(new GridData(SWT.FILL, SWT.NONE, false, false));
-	diffType = new Combo (parent, SWT.CHECK);
-	diffType.add ("Object identity");
-	diffType.add ("Creator class and line");
-	diffType.add ("Creator class");
-	diffType.select(0);
-	diffType.setLayoutData(new GridData(SWT.FILL, SWT.NONE, false, false));
-	stackTrace = new Button (parent, SWT.CHECK);
-	stackTrace.setText ("Stack");
-	stackTrace.addListener (SWT.Selection, e -> toggleStackTrace ());
-	list = new List (parent, SWT.BORDER | SWT.V_SCROLL);
-	list.addListener (SWT.Selection, event -> refreshObject ());
-	list.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, true));
-	label = new Label (parent, SWT.NONE);
-	label.setText ("0 object(s)");
-	saveAs = new Button (parent, SWT.PUSH);
-	saveAs.setText ("Save As...");
-	saveAs.setToolTipText("Saves the contents of the list to a file, optionally with the stack traces if selected.");
-	saveAs.addListener (SWT.Selection, event -> saveToFile (true));
-	saveAs.setLayoutData(new GridData(SWT.FILL, SWT.NONE, false, false));
-	save = new Button (parent, SWT.PUSH);
+	GridData diffData = new GridData(SWT.FILL, SWT.NONE, true, false);
+	diffData.horizontalSpan = 2;
+	diff.setLayoutData(diffData);
+
+	save = new Button (buttons, SWT.PUSH);
 	save.setText ("Save");
 	save.setToolTipText("Saves to the previously selected file.");
 	save.addListener (SWT.Selection, event -> saveToFile (false));
 	save.setLayoutData(new GridData(SWT.FILL, SWT.NONE, false, false));
+
+	saveAs = new Button (buttons, SWT.PUSH);
+	saveAs.setText ("Save As...");
+	saveAs.setToolTipText("Saves the contents of the list to a file, optionally with the stack traces if selected.");
+	saveAs.addListener (SWT.Selection, event -> saveToFile (true));
+	saveAs.setLayoutData(new GridData(SWT.FILL, SWT.NONE, false, false));
+
+	Composite checkboxAndCombo = new Composite(left, 0);
+	checkboxAndCombo.setLayout(new GridLayout(2, false));
+	checkboxAndCombo.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false));
+
+	diffType = new Combo (checkboxAndCombo, SWT.CHECK);
+	diffType.add ("Object identity");
+	diffType.add ("Creator class and line");
+	diffType.add ("Creator class");
+	diffType.select(0);
+	diffType.setLayoutData(new GridData(SWT.NONE, SWT.NONE, false, false));
+
+	stackTrace = new Button (checkboxAndCombo, SWT.CHECK);
+	stackTrace.setText ("Show Stack");
+	stackTrace.addListener (SWT.Selection, e -> toggleStackTrace ());
+	stackTrace.setLayoutData(new GridData(SWT.RIGHT, SWT.NONE, true, false));
+
+	list = new List (left, SWT.BORDER | SWT.V_SCROLL);
+	list.addListener (SWT.Selection, event -> refreshObject ());
+	list.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, true));
+
+	label = new Label (left, SWT.WRAP);
+	label.setText ("0 object(s)");
+	label.setLayoutData(new GridData(SWT.FILL, SWT.NONE, false, false));
+
 	stackTrace.setSelection (false);
 }
 
@@ -142,19 +179,25 @@
 	display.setTracking(!tracking);
 }
 
-void refreshLabel () {
-	Map<String, Long> deleted = oldObjects.stream().collect(
-			Collectors.groupingBy(o -> o.object.getClass().getSimpleName(), Collectors.counting()));
-	Map<String, Long> created = objects.stream().collect(
-			Collectors.groupingBy(o -> o.object.getClass().getSimpleName(), Collectors.counting()));
+void refreshLabel (java.util.List<ObjectWithError> createdObjects, java.util.List<ObjectWithError> deletedObjects) {
+	Function<? super ObjectWithError, ? extends String> classifier = o -> o.object.getClass().getSimpleName();
+
+	Map<String, Long> deleted = deletedObjects.stream().collect(
+			Collectors.groupingBy(classifier, Collectors.counting()));
+
+	Map<String, Long> created = createdObjects.stream().collect(
+			Collectors.groupingBy(classifier, Collectors.counting()));
+
 	StringBuilder sb = new StringBuilder();
-	Stream.concat(deleted.keySet().stream(), created.keySet().stream()).distinct().sorted().
-		forEach(type -> addCounts(sb, type, deleted.get(type), created.get(type)));
-	label.setText (sb.length() > 0 ? sb.toString() : "0 object(s)");
-	canvas.getParent().layout();
+	Stream<String> deletedAndCreated = Stream.concat(deleted.keySet().stream(), created.keySet().stream());
+	deletedAndCreated.distinct().sorted().forEach(type -> addCounts(sb, type, deleted.get(type), created.get(type)));
+
+	String description = sb.length() > 0 ? sb.toString() :  "0 object(s)";
+	label.setText (description.strip());
+	label.getParent().layout();
 }
 
-void addCounts (StringBuilder string, String type, Long deleted, Long created) {
+static void addCounts (StringBuilder string, String type, Long deleted, Long created) {
 	if (deleted != null || created != null) {
 		if (deleted != null) {
 			string.append("-" + deleted);
@@ -171,6 +214,86 @@
 
 void refreshDifference () {
 	Display display = canvas.getDisplay();
+	DeviceData info = getDeviceData(display);
+
+	boolean hasOldData = !oldObjects.isEmpty();
+
+	java.util.List<ObjectWithError> old = new ArrayList<>(oldObjects);
+	java.util.List<ObjectWithError> disposed = new ArrayList<>();
+	java.util.List<ObjectWithError> created = new ArrayList<>();
+	java.util.List<ObjectWithError> same = collectNewObjects(info, old, disposed, created);
+
+
+	if (diffType.getSelectionIndex() > 0) {
+		Iterator<ObjectWithError> object = created.iterator ();
+		while (object.hasNext ()) {
+			StackTraceElement stack = object.next ().getCreator ();
+			Iterator<ObjectWithError> equal = same.iterator ();
+			while (equal.hasNext ()) {
+				if (creatorEquals(stack, equal.next().getCreator ())) {
+					equal.remove ();
+					object.remove ();
+					break;
+				}
+			}
+		}
+	}
+
+	objects.clear();
+	objects.addAll(created);
+
+	oldObjects.clear();
+	oldObjects.addAll(same);
+	oldObjects.addAll(created);
+
+
+	list.removeAll ();
+	text.setText ("");
+	canvas.redraw ();
+	if(hasOldData) {
+		for (ObjectWithError object : created) {
+			list.add (object.object.toString());
+		}
+	}
+	if(hasOldData) {
+		refreshLabel (created, disposed);
+	} else {
+		refreshLabel (Collections.emptyList(), Collections.emptyList());
+	}
+}
+
+private static java.util.List<ObjectWithError> collectNewObjects(DeviceData info,
+		java.util.List<ObjectWithError> oldObjects,
+		java.util.List<ObjectWithError> disposedObjects,
+		java.util.List<ObjectWithError> createdObjects
+		) {
+	disposedObjects.addAll(oldObjects);
+	ArrayList<ObjectWithError> sameObjects = new ArrayList<>();
+
+	for (int i=0; i<info.objects.length; i++) {
+		boolean found = false;
+		Iterator<ObjectWithError> oldObject = oldObjects.iterator ();
+		Object infoObject = info.objects [i];
+		if (!(infoObject instanceof Color)) {
+			// Bug 563018: Colors don't require disposal, so exclude them from the list of allocated objects.
+			while (oldObject.hasNext () && !found) {
+				ObjectWithError next = oldObject.next ();
+				if (infoObject == next.object) {
+					sameObjects.add(next);
+					found = true;
+				}
+			}
+			if (!found) {
+				createdObjects.add(new ObjectWithError (infoObject, info.errors[i]));
+			}
+		}
+	}
+	// objects that were not found in new system state are disposed
+	disposedObjects.removeAll(sameObjects);
+	return sameObjects;
+}
+
+private DeviceData getDeviceData(Display display) {
 	DeviceData info = display.getDeviceData ();
 	if (!info.tracking) {
 		Shell shell = canvas.getShell();
@@ -182,46 +305,7 @@
 			toggleEnableTracking();
 		}
 	}
-
-	oldObjects = new ArrayList<> (objects);
-	objects.clear ();
-	for (int i=0; i<info.objects.length; i++) {
-		boolean found = false;
-		Iterator<ObjectWithError> oldObject = oldObjects.iterator ();
-		if (!(info.objects [i] instanceof Color)) {
-			// Bug 563018: Colors don't require disposal, so exclude them from the list of allocated objects.
-			while (oldObject.hasNext () && !found) {
-				if (info.objects [i] == oldObject.next ().object) {
-					oldObject.remove ();
-					found = true;
-				}
-			}
-			if (!found) {
-				objects.add (new ObjectWithError (info.objects [i], info.errors[i]));
-			}
-		}
-	}
-	if (diffType.getSelectionIndex() > 0) {
-		Iterator<ObjectWithError> object = objects.iterator ();
-		while (object.hasNext ()) {
-			StackTraceElement stack = object.next ().getCreator ();
-			Iterator<ObjectWithError> old = oldObjects.iterator ();
-			while (old.hasNext ()) {
-				if (creatorEquals(stack, old.next().getCreator ())) {
-					old.remove ();
-					object.remove ();
-					break;
-				}
-			}
-		}
-	}
-	list.removeAll ();
-	text.setText ("");
-	canvas.redraw ();
-	for (ObjectWithError object : objects) {
-		list.add (object.object.toString());
-	}
-	refreshLabel ();
+	return info;
 }
 
 boolean creatorEquals (StackTraceElement first, StackTraceElement second) {
@@ -261,7 +345,7 @@
 		fileName = String.format("%s_%03d", fileName, fileCount++);
 	}
 	try (PrintWriter file = new PrintWriter(new FileOutputStream(fileName))) {
-		
+
 		int i = 0;
 		for (ObjectWithError o : objects) {
 			Object object = o.object;
@@ -386,7 +470,7 @@
 		text.setText (objects.get(index).getStack());
 		setVisible(text, true);
 		setVisible(canvas, false);
-		canvas.getParent().layout();
+		text.getParent().layout();
 	} else {
 		setVisible(canvas, true);
 		setVisible(text, false);
@@ -399,12 +483,6 @@
 	((GridData)control.getLayoutData()).exclude = !visible;
 }
 
-void refreshAll () {
-	objects.clear();
-	refreshDifference ();
-	oldObjects = new ArrayList<>(objects);
-}
-
 private static final class ObjectWithError {
 	final Object object;
 	final Error error;
diff --git a/bundles/org.eclipse.swt.tools.spies/src/org/eclipse/swt/tools/views/SleakView.java b/bundles/org.eclipse.swt.tools.spies/src/org/eclipse/swt/tools/views/SleakView.java
index 7525e4f..e398586 100644
--- a/bundles/org.eclipse.swt.tools.spies/src/org/eclipse/swt/tools/views/SleakView.java
+++ b/bundles/org.eclipse.swt.tools.spies/src/org/eclipse/swt/tools/views/SleakView.java
@@ -17,8 +17,6 @@
 import javax.annotation.*;
 
 import org.eclipse.e4.ui.di.*;
-import org.eclipse.swt.*;
-import org.eclipse.swt.layout.*;
 import org.eclipse.swt.tools.internal.*;
 import org.eclipse.swt.widgets.*;
 
@@ -32,11 +30,9 @@
 
 	@PostConstruct
 	public void createPartControl(Composite parent) {
-		composite = new Composite(parent, SWT.NONE);
-		GridLayout gridLayout = new GridLayout(2, false);
-		composite.setLayout(gridLayout);
 		Sleak sleak = new Sleak ();
-		sleak.create(composite);
+		sleak.create(parent);
+		composite = parent;
 	}
 
 	@Focus