/*
* ARX: Powerful Data Anonymization
* Copyright 2012 - 2017 Fabian Prasser, Florian Kohlmayer and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.deidentifier.arx.gui.view.impl.explore;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.deidentifier.arx.ARXLattice;
import org.deidentifier.arx.ARXLattice.ARXNode;
import org.deidentifier.arx.ARXResult;
import org.deidentifier.arx.gui.Controller;
import org.deidentifier.arx.gui.model.ModelNodeFilter;
import org.deidentifier.arx.gui.resources.Resources;
import org.deidentifier.arx.gui.view.SWTUtil;
import org.eclipse.nebula.widgets.nattable.util.GUIHelper;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Path;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.Transform;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
/**
* This class implements a view of a lattice.
*
* @author Fabian Prasser
*/
public class ViewLattice extends ViewSolutionSpace {
/**
* This class is here for backwards compatibility only.
*/
@SuppressWarnings("unused")
private static class Bounds implements Serializable {
/** SVUID */
private static final long serialVersionUID = -7472570696920782588L;
}
/**
* The current drag type.
*/
private static enum DragType {
/** MOVE */
MOVE,
/** ZOOM */
ZOOM,
/** NONE */
NONE
}
/**
* This class is here for serializability, only.
*/
private static class SerializablePath implements Serializable {
/** SVUID */
private static final long serialVersionUID = -4572722688452678425L;
/** Path */
private final transient Path path;
/** Constructor
* @param path
*/
public SerializablePath(Path path){
this.path = path;
}
/**
* Dispose
*/
public void dispose(){
if (!this.path.isDisposed()) {
this.path.dispose();
}
}
/**
* Returns the path
* @return
*/
public Path getPath(){
return this.path;
}
}
/** Color. */
private static final Color COLOR_BLACK = GUIHelper.getColor(0, 0, 0);
/** Color. */
private static final Color COLOR_LIGHT_GRAY = GUIHelper.getColor(211, 211, 211);
/** Color. */
private static final Color COLOR_LINE = GUIHelper.getColor(200, 200, 200);
/** Attribute constant. */
private static final int ATTRIBUTE_CENTER = 4;
/** Attribute constant. */
private static final int ATTRIBUTE_LABEL = 5;
/** Attribute constant. */
private static final int ATTRIBUTE_VISIBLE = 6;
/** Attribute constant. */
private static final int ATTRIBUTE_PATH = 7;
/** Attribute constant. */
private static final int ATTRIBUTE_EXTENT = 8;
/** Time to wait for a tool tip to show. */
private static final int TOOLTIP_WAIT = 200;
/** Global settings. */
private static final double NODE_INITIAL_SIZE = 200d;
/** Global settings. */
private static final double NODE_FRAME_RATIO = 0.7d;
/** Global settings. */
private static final double NODE_SIZE_RATIO = 0.3d;
/** Global settings. */
private static final double ZOOM_SPEED = 10d;
/** Global settings. */
private static final int MSG_WIDTH = 300;
/** Global settings. */
private static final int MSG_HEIGHT = 100;
/** Global settings. */
private static final int MIN_WIDTH = 2;
/** Global settings. */
private static final int MIN_HEIGHT = 1;
/** For the current view. */
private static final int STROKE_WIDTH_NODE = 1;
/** For the current view. */
private static final int STROKE_WIDTH_CONNECTION = 1;
/** The font. */
private Font font = null;
/** For the current view. */
private double nodeWidth = 0f;
/** For the current view. */
private double nodeHeight = 0f;
/** The lattice to display. */
private final List<List<ARXNode>> lattice = new ArrayList<List<ARXNode>>();
/** The according ARX lattice. */
private ARXLattice arxLattice = null;
/** The lattice to display. */
private int latticeWidth = 0;
/** The screen size. */
private Point screen = null;
/** The number of nodes. */
private int numNodes = 0;
/** Drag parameters. */
private int dragX = 0;
/** Drag parameters. */
private int dragY = 0;
/** Drag parameters. */
private int dragStartX = 0;
/** Drag parameters. */
private int dragStartY = 0;
/** Drag parameters. */
private DragType dragType = DragType.NONE;
/** The tool tip. */
private int tooltipX = -1;
/** The tool tip. */
private int tooltipY = -1;
/** The tool tip. */
private int oldTooltipX = -1;
/** The tool tip. */
private int oldTooltipY = -1;
/** The canvas. */
private final Canvas canvas;
/**
* Creates a new instance.
*
* @param parent
* @param controller
*/
public ViewLattice(final Composite parent, final Controller controller) {
super(parent, controller);
// Compute font
FontData[] fd = parent.getFont().getFontData();
fd[0].setHeight(8);
this.font = new Font(parent.getDisplay(), fd[0]);
// Build canvas
this.canvas = new Canvas(getPrimaryComposite(), SWT.DOUBLE_BUFFERED);
this.canvas.setLayoutData(SWTUtil.createFillGridData());
this.canvas.addPaintListener(new PaintListener() {
@Override
public void paintControl(PaintEvent e) {
screen = canvas.getSize();
e.gc.setAdvanced(true);
e.gc.setAntialias(SWT.ON);
draw(e.gc);
}
});
this.canvas.addDisposeListener(new DisposeListener(){
public void widgetDisposed(DisposeEvent arg0) {
clearLatticeAndDisposePaths(); // Free resources
}
});
// Initialize
this.initializeToolTipTimer();
this.initializeListeners();
}
@Override
public void dispose() {
super.dispose();
font.dispose();
}
/**
* Resets the view.
*/
@Override
public void reset() {
super.reset();
this.numNodes = 0;
this.arxLattice = null;
this.clearLatticeAndDisposePaths();
this.latticeWidth = 0;
this.screen = null;
this.canvas.redraw();
}
/**
* Called when button 1 is clicked on a node.
*
* @param node
*/
private void actionButtonClicked1(ARXNode node) {
actionSelectNode(node);
canvas.redraw();
}
/**
* Called when button 3 is clicked on a node.
*
* @param node
* @param x
* @param y
*/
private void actionButtonClicked3(ARXNode node, final int x, final int y) {
actionSelectNode(node);
canvas.redraw();
actionShowMenu(x, y);
dragType = DragType.NONE;
}
/**
* Clears the lattice.
*/
private void clearLatticeAndDisposePaths() {
for (List<ARXNode> level : lattice) {
for (ARXNode node : level) {
SerializablePath path = (SerializablePath)node.getAttributes().get(ATTRIBUTE_PATH);
if (path!=null && path.getPath()!=null) {
node.getAttributes().put(ATTRIBUTE_PATH, null);
path.dispose();
}
}
}
this.lattice.clear();
}
/**
* Draws the lattice.
*
* @param g
*/
private void draw(final GC g) {
// Fill background
Point size = canvas.getSize();
g.setBackground(g.getDevice().getSystemColor(SWT.COLOR_LIST_BACKGROUND));
g.fillRectangle(0, 0, size.x, size.y);
// Return, if nothing to show
if (getModel() == null) {
// Draw border
g.setForeground(g.getDevice().getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW));
g.drawRectangle(0, 0, screen.x-1, screen.y-1);
return;
}
// If too many nodes
if (numNodes > getModel().getMaxNodesInViewer()) {
int x = (size.x / 2) - (MSG_WIDTH / 2);
int y = (size.y / 2) - (MSG_HEIGHT / 2);
if ((x < 0) || (y < 0)) { return; }
g.setBackground(COLOR_LIGHT_GRAY);
g.fillRectangle(x, y, MSG_WIDTH, MSG_HEIGHT);
g.setForeground(COLOR_BLACK);
g.drawRectangle(x, y, MSG_WIDTH, MSG_HEIGHT);
drawText(g, Resources.getMessage("LatticeView.7"), x, y, MSG_WIDTH, MSG_HEIGHT); //$NON-NLS-1$
return;
}
// Return, if nothing to show
if (lattice.isEmpty() || (screen == null)) { return; }
// Draw connections
drawConnections(g);
// Draw nodes
drawNodes(g);
// Draw border
g.setForeground(g.getDevice().getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW));
g.drawRectangle(0, 0, screen.x-1, screen.y-1);
}
/**
* Draws the connections.
*
* @param g
*/
private void drawConnections(GC g) {
// Prepare
Set<ARXNode> done = new HashSet<ARXNode>();
int[] clip = new int[4];
// Set style
g.setLineWidth(STROKE_WIDTH_CONNECTION);
g.setForeground(COLOR_LINE);
// For each node
for (List<ARXNode> level : lattice) {
for (ARXNode node1 : level) {
// Obtain coordinates
double[] center1 = (double[]) node1.getAttributes().get(ATTRIBUTE_CENTER);
// Draw
for (final ARXNode node2 : node1.getSuccessors()) {
boolean visible = (Boolean)node2.getAttributes().get(ATTRIBUTE_VISIBLE);
if (visible && !done.contains(node2)) {
// Obtain coordinates
double[] center2 = (double[]) node2.getAttributes().get(ATTRIBUTE_CENTER);
// Perform clipping
if (liangBarsky(0, screen.x, 0, screen.y,
center1[0], center1[1],
center2[0], center2[1],
clip)) {
// Draw
g.drawLine(clip[0], clip[1], clip[2], clip[3]);
}
}
}
// Add to set of already processed nodes
done.add(node1);
}
}
}
/**
* Draws a node.
*
* @param g
*/
private void drawNodes(final GC g) {
// Prepare
Rectangle bounds = new Rectangle(0, 0, (int)nodeWidth, (int)nodeHeight);
Transform transform = new Transform(g.getDevice());
// Set style
g.setLineWidth(STROKE_WIDTH_NODE);
g.setFont(font);
// Draw nodes
for (List<ARXNode> level : lattice) {
for (ARXNode node : level) {
// Obtain coordinates
double[] center = (double[]) node.getAttributes().get(ATTRIBUTE_CENTER);
bounds.x = (int)(center[0] - nodeWidth / 2d);
bounds.y = (int)(center[1] - nodeHeight / 2d);
// Clipping
if (bounds.intersects(new Rectangle(0, 0, screen.x, screen.y))) {
// Retrieve/compute text rendering data
SerializablePath path = (SerializablePath) node.getAttributes().get(ATTRIBUTE_PATH);
Point extent = (Point) node.getAttributes().get(ATTRIBUTE_EXTENT);
if (path == null || path.getPath() == null) {
String text = (String) node.getAttributes().get(ATTRIBUTE_LABEL);
path = new SerializablePath(new Path(canvas.getDisplay()));
path.getPath().addString(text, 0, 0, font);
node.getAttributes().put(ATTRIBUTE_PATH, path);
extent = g.textExtent(text);
node.getAttributes().put(ATTRIBUTE_EXTENT, extent);
}
// Degrade if too far away
if (bounds.width <= 4) {
g.setBackground(getInnerColor(node));
g.setAntialias(SWT.OFF);
g.fillRectangle(bounds.x, bounds.y, bounds.width, bounds.height);
// Draw real node
} else {
// Fill background
g.setBackground(getInnerColor(node));
g.setAntialias(SWT.OFF);
if (node != getSelectedNode()) {
g.fillOval(bounds.x, bounds.y, bounds.width, bounds.height);
} else {
g.fillRectangle(bounds.x, bounds.y, bounds.width, bounds.height);
}
// Draw line
g.setLineWidth(getOuterStrokeWidth(node, bounds.width));
g.setForeground(getOuterColor(node));
g.setAntialias(SWT.ON);
if (node != getSelectedNode()) {
g.drawOval(bounds.x, bounds.y, bounds.width, bounds.height);
} else {
g.drawRectangle(bounds.x, bounds.y, bounds.width, bounds.height);
}
// Draw text
if (bounds.width >= 20) {
// Enable anti-aliasing
g.setTextAntialias(SWT.ON);
// Compute position and factor
float factor1 = (bounds.width * 0.7f) / (float)extent.x;
float factor2 = (bounds.height * 0.7f) / (float)extent.y;
float factor = Math.min(factor1, factor2);
int positionX = bounds.x + (int)(((float)bounds.width - (float)extent.x * factor) / 2f);
int positionY = bounds.y + (int)(((float)bounds.height - (float)extent.y * factor) / 2f);
// Initialize transformation
transform.identity();
transform.translate(positionX, positionY);
transform.scale(factor, factor);
g.setTransform(transform);
// Draw and reset
g.setBackground(COLOR_BLACK);
g.fillPath(path.getPath());
g.setTransform(null);
g.setTextAntialias(SWT.OFF);
}
}
}
}
}
// Clean up
transform.dispose();
}
/**
* Utility method which centers a text in a rectangle.
*
* @param gc
* @param text
* @param x
* @param y
* @param width
* @param height
*/
private void drawText(final GC gc, final String text, final int x, final int y, final int width, final int height) {
Point size = canvas.getSize();
Point extent = gc.textExtent(text);
gc.setClipping(x, y, width, height);
int xx = x + (width - extent.x) / 2;
int yy = y + height / 2 - extent.y / 2;
gc.drawText(text, xx, yy, true);
gc.setClipping(0, 0, size.x, size.y);
}
/**
* Returns the node at the given location.
*
* @param x
* @param y
* @return
*/
private ARXNode getNode(final int x, final int y) {
for (List<ARXNode> level : lattice) {
for (ARXNode node : level) {
double[] bounds = (double[]) node.getAttributes().get(ATTRIBUTE_CENTER);
if (bounds == null) { return null; }
if ((x >= bounds[0] - nodeHeight/2) &&
(y >= bounds[1] - nodeHeight/2) &&
(x <= (bounds[0] + nodeWidth/2)) &&
(y <= (bounds[1] + nodeHeight/2))) {
return node;
}
}
}
return null;
}
/**
* Initializes the data structures for displaying a new lattice.
*
* @param result
* @param filter
*/
private void initialize(final ARXResult result, final ModelNodeFilter filter) {
// Return if nothing to do
if ((result == null) || (result.getLattice() == null) || (filter == null)) {
reset();
return;
}
// Clear the lattice
if (!result.getLattice().equals(this.arxLattice)) {
this.clearLatticeAndDisposePaths();
this.arxLattice = result.getLattice();
} else {
this.lattice.clear();
}
// Build the visible sub-lattice
ARXLattice originalLattice = result.getLattice();
this.latticeWidth = 0;
this.numNodes = 0;
for (ARXNode[] originalLevel : originalLattice.getLevels()) {
List<ARXNode> level = new ArrayList<ARXNode>();
for (ARXNode node : originalLevel) {
boolean visible = filter.isAllowed(result.getLattice(), node);
node.getAttributes().put(ATTRIBUTE_VISIBLE, visible);
if (visible) {
level.add(node);
numNodes++;
}
}
if (!level.isEmpty()) {
this.lattice.add(level);
}
this.latticeWidth = Math.max(this.latticeWidth, level.size());
}
// Check
if (numNodes > getModel().getMaxNodesInViewer()) { return; }
// Now initialize the text attribute
for (List<ARXNode> level : this.lattice) {
for (ARXNode node : level) {
if (!node.getAttributes().containsKey(ATTRIBUTE_LABEL)) {
String text = Arrays.toString(node.getTransformation());
text = text.substring(1, text.length() - 1);
text = super.trimLabel(text);
node.getAttributes().put(ATTRIBUTE_LABEL, text);
}
}
}
// Reset the parameters
initializeCanvas();
}
/**
* Recomputes the initial positions of all nodes.
*/
private void initializeCanvas() {
// Obtain screen size
screen = canvas.getSize();
// Obtain optimal width and height per node
double width = NODE_INITIAL_SIZE;
double height = width * NODE_SIZE_RATIO;
if ((height * lattice.size()) > screen.y) {
double factor = screen.y / (height * lattice.size());
height *= factor; width *= factor;
}
if ((width * latticeWidth) > screen.x) {
double factor = screen.x / (width * latticeWidth);
height *= factor; width *= factor;
}
nodeWidth = width * NODE_FRAME_RATIO;
nodeHeight = height * NODE_FRAME_RATIO;
// Compute deltas to center the lattice
final double deltaY = (screen.y - (height * lattice.size())) / 2d;
final double deltaX = (screen.x - (width * latticeWidth)) / 2d;
// For each level
double positionY = lattice.size() -1;
for (List<ARXNode> level : lattice) {
// For each node on this level
double centerY = deltaY + (positionY * height) + (height / 2d);
double positionX = 0;
for (ARXNode node : level) {
// Create and store node properties
double offset = (latticeWidth * width) - (level.size() * width);
double centerX = deltaX + (positionX * width) + (width / 2d) + (offset / 2d);
node.getAttributes().put(ATTRIBUTE_CENTER, new double[]{centerX, centerY});
// Next node
positionX++;
}
// Next level
positionY--;
}
}
/**
* Creates all required listeners.
*/
private void initializeListeners() {
canvas.addMouseListener(new MouseAdapter(){
@Override
public void mouseDown(MouseEvent arg0) {
dragX = arg0.x;
dragY = arg0.y;
dragStartX = arg0.x;
dragStartY = arg0.y;
if (dragType == DragType.NONE) {
if (arg0.button == 1) {
dragType = DragType.MOVE;
} else if (arg0.button == 3) {
dragType = DragType.ZOOM;
}
}
}
@Override
public void mouseUp(MouseEvent arg0) {
dragType = DragType.NONE;
}
});
canvas.addMouseListener(new MouseAdapter(){
/** Drag parameters */
private int clickX = 0;
/** Drag parameters */
private int clickY = 0;
@Override
public void mouseDown(MouseEvent arg0) {
clickX = arg0.x;
clickY = arg0.y;
if (arg0.button == 1) {
final ARXNode node = getNode(arg0.x, arg0.y);
if (node != null) {
actionButtonClicked1(node);
}
}
}
@Override
public void mouseUp(MouseEvent arg0) {
if (arg0.button == 3 && arg0.x == clickX && arg0.y == clickY) {
final ARXNode node = getNode(arg0.x, arg0.y);
if (node != null) {
Point display = canvas.toDisplay(arg0.x, arg0.y);
actionButtonClicked3(node, display.x, display.y);
}
}
clickX = arg0.x;
clickY = arg0.y;
}
});
canvas.addMouseMoveListener(new MouseMoveListener(){
@Override
public void mouseMove(MouseEvent arg0) {
if (dragType != DragType.NONE) {
final int deltaX = arg0.x - dragX;
final int deltaY = arg0.y - dragY;
if (dragType == DragType.MOVE) {
// Just move the nodes around
for (List<ARXNode> level : lattice) {
for (ARXNode node : level) {
double[] center = (double[]) node.getAttributes().get(ATTRIBUTE_CENTER);
center[0] += deltaX;
center[1] += deltaY;
}
}
} else if (dragType == DragType.ZOOM) {
// Compute zoom
double zoom = -((double) deltaY / (double) screen.y) * ZOOM_SPEED;
double newWidth = nodeWidth + (zoom * nodeWidth);
double newHeight = nodeHeight + (zoom * nodeHeight);
// Adjust
zoom = newWidth > screen.x ? (screen.x - nodeWidth) / nodeWidth : zoom;
zoom = newWidth < MIN_WIDTH ? (MIN_WIDTH - nodeWidth) / nodeWidth : zoom;
zoom = newHeight > screen.y ? (screen.y - nodeHeight) / nodeHeight : zoom;
zoom = newHeight < MIN_HEIGHT ? (MIN_HEIGHT - nodeHeight) / nodeHeight : zoom;
// Zoom the node size
nodeWidth += zoom * nodeWidth;
nodeHeight += zoom * nodeHeight;
// Zoom the node positions
for (List<ARXNode> level : lattice) {
for (ARXNode node : level) {
double[] center = (double[]) node.getAttributes().get(ATTRIBUTE_CENTER);
center[0] -= dragStartX;
center[0] += zoom * center[0];
center[0] += dragStartX;
center[1] -= dragStartY;
center[1] += zoom * center[1];
center[1] += dragStartY;
}
}
}
// Store mouse data & redraw
dragX += deltaX;
dragY += deltaY;
canvas.redraw();
}
}
});
canvas.addMouseMoveListener(new MouseMoveListener(){
public void mouseMove(MouseEvent arg0) {
tooltipX = arg0.x;
tooltipY = arg0.y;
}
});
canvas.addListener(SWT.MouseExit, new Listener() {
public void handleEvent(Event e) {
tooltipX = -1;
tooltipY = -1;
}
});
canvas.addControlListener(new ControlAdapter() {
@Override
public void controlResized(ControlEvent arg0) {
screen = canvas.getSize();
initializeCanvas();
canvas.redraw();
}
});
}
/**
* For performance reasons, we check for tool tips only at certain times.
*/
private void initializeToolTipTimer() {
canvas.getDisplay().timerExec(TOOLTIP_WAIT, new Runnable(){
public void run(){
if (tooltipX != oldTooltipX || tooltipY != oldTooltipY) {
String text = null;
if (tooltipX != -1 && tooltipY != -1) {
ARXNode node = getNode(tooltipX, tooltipY);
text = node == null ? null : getTooltipDecorator().decorate(node);
}
canvas.setToolTipText(text);
}
oldTooltipX = tooltipX;
oldTooltipY = tooltipY;
if (!canvas.isDisposed()) {
canvas.getDisplay().timerExec(TOOLTIP_WAIT, this);
}
}
});
}
/**
* Liang-Barsky line clipping function. Adapted from Daniel White
*
* @param edgeLeft
* @param edgeRight
* @param edgeTop
* @param edgeBottom
* @param x0src
* @param y0src
* @param x1src
* @param y1src
* @param clip
* @return
* @see http://www.skytopia.com/project/articles/compsci/clipping.html
*/
private boolean liangBarsky (double edgeLeft, double edgeRight, double edgeTop, double edgeBottom,
double x0src, double y0src, double x1src, double y1src,
int[] clip) {
// Init
double t0 = 0.0;
double t1 = 1.0;
double xdelta = x1src-x0src;
double ydelta = y1src-y0src;
double p = 0, q = 0,r = 0;
for(int edge=0; edge<4; edge++) {
if (edge==0) { p = -xdelta; q = -(edgeLeft-x0src); }
if (edge==1) { p = xdelta; q = (edgeRight-x0src); }
if (edge==2) { p = -ydelta; q = -(edgeTop-y0src);}
if (edge==3) { p = ydelta; q = (edgeBottom-y0src); }
r = q/p;
if(p==0 && q<0) return false;
if(p<0) {
if(r>t1) return false;
else if(r>t0) t0=r;
} else if(p>0) {
if(r<t0) return false;
else if(r<t1) t1=r;
}
}
// Clip
clip[0] = (int)(x0src + t0*xdelta);
clip[1] = (int)(y0src + t0*ydelta);
clip[2] = (int)(x0src + t1*xdelta);
clip[3] = (int)(y0src + t1*ydelta);
return true;
}
@Override
protected void actionRedraw() {
this.canvas.redraw();
}
@Override
protected void eventFilterChanged(ARXResult result, ModelNodeFilter filter) {
initialize(result, filter);
canvas.redraw();
}
@Override
protected void eventModelChanged() {
// Empty by design
}
@Override
protected void eventNodeSelected() {
canvas.redraw();
}
@Override
protected void eventResultChanged(ARXResult result) {
if (getModel().getResult() == null) reset();
}
}