javaswingjgraphx

JGraphX new initialization of graph/graphComponent does not work as expected and causes artefacts


Hi I am using JGraphX to build some kind of Java swing based graph editor application. The application in general works fine and as intended under normal circumstances. In general I have an class called Editor which contains all essential declarations for the graph, as seen in the code example below.

initializing the graph and overriding some of its methods

public class Editor extends JFrame implements Serializable {
Handler handler;
JTabbedPane tabPane;
mxGraphComponent graphComponent;
EntityDataTable dataTable;

protected static mxGraph graph = new mxGraph() {

    // Overrides method to disallow edge label editing
    public boolean isCellEditable(Object cell) {
        if (cell instanceof mxCell) {
            mxCell c = (mxCell) cell;
            if (c.isEdge()) {
                return false;
            } else {
                return false;
            }
        }
        return false;
    }

    // Overrides method to disallow edge selection
    public boolean isCellSelectable(Object cell)
    {
        if (model.isEdge(cell))
        {
            return false;
        }

        return super.isCellSelectable(cell);
    }

    // Overrides method to provide a cell label in the display
    public String convertValueToString(Object cell) {
        if (cell instanceof mxCell) {
            Object value = ((mxCell) cell).getValue();

            if (value instanceof Element) {
                Element elt = (Element) value;

               // String tag = elt.getTagName();
                String tag =  elt.getAttribute("name");


                return tag;

            }
        }

        return super.convertValueToString(cell);
    }

    public String getToolTipForCell(Object cell){

        return "Double Click to Edit";
    }
};

...

restricts certain undoEvents

 protected mxEventSource.mxIEventListener undoHandler = new mxEventSource.mxIEventListener(){
 public void invoke(Object source, mxEventObject evt)
 {

     mxUndoableEdit evt1 = (mxUndoableEdit) evt.getProperty("edit");
     List<mxUndoableEdit.mxUndoableChange> changes = evt1.getChanges();

     Object[] temp = graph.getSelectionCellsForChanges(changes);


      boolean islegal = true;
      for (int i = 0; i < temp.length; i++)
      {
      mxCell cell = (mxCell)temp[i];
      String value = cell.getValue().toString();
      if (value.equals("subprocess")||value.equals("optional")||value.equals("parallel")||value.equals("synchronous")||value.equals("activating")||value.equals("deactivating")){
      //System.out.println("is not legal");
      islegal = false;
      }
      }
      for (int i = 0; i < changes.size(); i++){
          if (changes.get(i).toString().contains("mxValueChange")){
              islegal = false;
          }
      }

      graph.setSelectionCells(graph.getSelectionCellsForChanges(changes));

      if (islegal == true){
      undoManager.undoableEditHappened((mxUndoableEdit) evt
      .getProperty("edit"));
      }else{
     // System.out.println("illegal undo");
      }
  }};

...

 protected boolean modified = false;
protected mxGraphOutline graphOutline;
protected JPanel actionPane;

mxUndoManager undoManager;

public Editor() {
    handler = new Handler(this);
    dataTable = new EntityDataTable(handler);


    initGUI();
    initGraphSettings();
}

public Editor(SaveData saveData) {

    handler = new Handler(this);

    dataTable = new EntityDataTable(handler);

    initGUI();
    initGraphSettings();

    //erst alle entities erstellen und submitten, dann alle verbindungselemente zu den entities hinzufügen und nochmal submit

    //Load entities
    ArrayList<DataSaveElement> saveDataList = saveData.getSaveData(); for (int i = 0; i < saveDataList.size(); i++){

        System.out.println("Loaded "+saveDataList.get(i).getType()+" "+saveDataList.get(i).getName());

        if (saveDataList.get(i).getType().equals("Process")){
            ProcessPopUp temp = new ProcessPopUp(handler, this);
            temp.setGlobalID(saveDataList.get(i).getGlobalID());
            temp.setName(saveDataList.get(i).getName());
            temp.setDesc(saveDataList.get(i).getDescription());
            temp.setType(saveDataList.get(i).getType());

... a lot of code for rebuilding, some graph settings, the gui etc. Inside of the initGui() my mxGraphComponent is initialized

graphComponent = new mxGraphComponent(graph);

Since the graph visualization is only one part of the application and other data exists in the background, while saving all data values are stored including the postions for vertices etc. So when loading a save file a fresh application is build from the ground up simply adding all saved data values step by step. When I close the entire java application, start it again and load my saved file there is no problem at all. The problem occures when loading a saved file while the application is still running like e. g.

menuItem = new JMenuItem("Open...",
            new ImageIcon("images/middle.gif"));
    menuItem.addActionListener(new ActionListener() {
        @java.lang.Override
        public void actionPerformed(ActionEvent e) {
            LoadAndSaveManager manager = new LoadAndSaveManager();
            try {
                Object o = manager.load(new FileChooser(0).getSelectedFile().getAbsolutePath());
                SaveData saveData =(SaveData) o;

                Editor editorNew = new Editor(saveData);
                new MenuBar(editorNew);
                editorNew.setVisible(true);

                editor.dispose();

            } catch (Exception e1) {
                e1.printStackTrace();
            }
        }
    });

    menu.add(menuItem);

My menubar is quite basic an gets the editor als parameter. Since a new Editor is created which creates a new mxGraph, as well as a new mxGraphComponent and finally disposes the old editor there should be no interference...at least as far as I know. However, despite having a new Editor instance which has its own new Graph and GraphComponent the old one is still used somehow. As shown in the example images below.

This will be saved and the application is closed completely. Two nodes and a link for saving:

two nodes

When starting the application and loading the saved data nothing is wrong.

Next I start a new one and add for example three nodes and two links. Three nodes and two links:

Three nodes

Now I load the previously saved data. I would expect that the the window closes and a new one with the prevois data pops up. This is not the case. The data is loaded, but the old graph seems still active somehow and all nodes and links are on the graph. Mixed up data:

mixed

If this would be the only problem I could simply clear the graph and add all "loading data" afterwards, however somehow the graphComponent seems to be broken as well. When dragging nodes the links sometimes are disrupted. Disrupted links:

disrupted

From my observations so far this seems to fix itself when selecting an area (I think this forces the graphComponent to refresh()) Selecting:

selecting

Unfortunately posting the entire code is not that much of an option, so I posted some code I think might be of importance for the problem. If further code is required I will specifically post it afterwards.

I am not sure why this happens and after hours of research I somehow hit a wall and and I am not sure what I am doing wrong. I would really appreciate some advice.


Here is an minimal full code example of the problem regarding the graphComponents interference problem when declaring a new one.

public class Main {

Editor editor;

public Main() {
    editor = new Editor();
    new MenuBar(editor);
    editor.setVisible(true);
}

public static void main(String args[]) {
    new Main();
}}

public class Editor extends JFrame {

mxGraphComponent graphComponent;

protected static mxGraph graph = new mxGraph() {

    // Overrides method to disallow edge label editing
    public boolean isCellEditable(Object cell) {
        if (cell instanceof mxCell) {
            mxCell c = (mxCell) cell;
            if (c.isEdge()) {
                return false;
            } else {
                return false;
            }
        }
        return false;
    }

    // Overrides method to disallow edge selection
    public boolean isCellSelectable(Object cell)
    {
        if (model.isEdge(cell))
        {
            return false;
        }

        return super.isCellSelectable(cell);
    }

    // Overrides method to provide a cell label in the display
    public String convertValueToString(Object cell) {
        if (cell instanceof mxCell) {
            Object value = ((mxCell) cell).getValue();

            if (value instanceof Element) {
                Element elt = (Element) value;

                // String tag = elt.getTagName();
                String tag =  elt.getAttribute("name");


                return tag;

            }
        }

        return super.convertValueToString(cell);
    }

    public String getToolTipForCell(Object cell){

        return "Double Click to Edit";
    }
};

public Editor() {
    initGUI();
    initGraphSettings();
}

public Editor(ArrayList<SaveDataElement> saveData) {
    initGUI();
    initGraphSettings();

    //Load data
    addToGraph(saveData);
}

public void initGUI(){
    setExtendedState(JFrame.MAXIMIZED_BOTH);
    setSize(new Dimension(1200, 900));
    setLocationRelativeTo(null);
    setDefaultCloseOperation(EXIT_ON_CLOSE);

    graphComponent = new mxGraphComponent(graph);

    JPanel graphPanel = new JPanel(new BorderLayout());
    graphPanel.add(graphComponent);

    add(graphPanel);
}
public void initGraphSettings(){

    Map<String, Object> style = graph.getStylesheet().getDefaultEdgeStyle();
    style.put(mxConstants.STYLE_ALIGN, true);
    style.put(mxConstants.STYLE_EDGE, mxConstants.EDGESTYLE_TOPTOBOTTOM);

    graph.setCellsCloneable(false);
    graphComponent.setConnectable(false);
    graphComponent.getViewport().setBackground(Color.WHITE);

    new mxRubberband(graphComponent);
}

public mxGraph getGraph(){
    return graph;
}

public void addToGraph(ArrayList<SaveDataElement> saveData){
    for (int i = 0; i < saveData.size(); i++) {
        String name = saveData.get(i).getName();
        int vertPosX = saveData.get(i).getPosX();
        int vertPosY = saveData.get(i).getPosY();

        new AddGraphNode("node", name, "rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;", vertPosX, vertPosY);
    }

    Object[] cells = graph.getChildVertices(graph.getDefaultParent());
    Object startCell = null;
    Object endCell = null;

    for (int i = 0; i < saveData.size(); i++){
        for (int j = 0; j < cells.length; j++){
            if (((mxCell)cells[j]).getAttribute("name").equals(saveData.get(i).getName()))
                startCell = cells[j];

            for (int k = 0; k < saveData.get(i).getTargets().size(); k++){
                if (((mxCell)cells[j]).getAttribute("name").equals(saveData.get(i).getTargets().get(k))){
                    endCell = cells[j];
                    new AddGraphLink(startCell, endCell,"Link", "endArrow=classic;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;verticalAlign=top;verticalLabelPosition=bottom'");
                }
            }
        }
    }
}}

public class MenuBar extends JMenuBar {


MenuBar(Editor editor){

    JMenuBar menuBar = new JMenuBar();
    JMenuItem menuItem;
    JMenu menu = new JMenu("File");
    menuBar.add(menu);

    menuItem = new JMenuItem("Add");
    menuItem.addActionListener(new ActionListener() {
        @java.lang.Override
        public void actionPerformed(ActionEvent e) {
            //Setting up some data to create nodes and links
            ArrayList<SaveDataElement> saveData = new ArrayList<SaveDataElement>();

            ArrayList<String> targetsForTestX = new ArrayList<String>();
            targetsForTestX.add("Test Y");
            targetsForTestX.add("Test Z");
            saveData.add(new SaveDataElement("Test X", 200, 300, targetsForTestX));

            ArrayList<String> targetsForTestY = new ArrayList<String>();
            saveData.add(new SaveDataElement("Test Y", 300, 420, targetsForTestY));

            ArrayList<String> targetsForTestZ = new ArrayList<String>();
            saveData.add(new SaveDataElement("Test Z", 100, 420, targetsForTestZ));

            editor.addToGraph(saveData);

        }
    });
    menu.add(menuItem);

    menuItem = new JMenuItem("Load 1");
    menuItem.addActionListener(new ActionListener() {
        @java.lang.Override
        public void actionPerformed(ActionEvent e) {
            //Setting up some data to create nodes and links
            ArrayList<SaveDataElement> saveData = new ArrayList<SaveDataElement>();

            ArrayList<String> targetsForTest1 = new ArrayList<String>();
            targetsForTest1.add("Test 2");
            saveData.add(new SaveDataElement("Test 1", 40, 40, targetsForTest1));

            ArrayList<String> targetsForTest2 = new ArrayList<String>();
            saveData.add(new SaveDataElement("Test 2", 200, 40, targetsForTest2));


            Editor editorNew = new Editor(saveData);
            new MenuBar(editorNew);
            editorNew.setVisible(true);

            editor.dispose();
        }
    });

    menu.add(menuItem);

    editor.setJMenuBar(menuBar);
}}

public class SaveDataElement {
String name;
int posX, posY;
ArrayList<String> targets;

public SaveDataElement(String name, int posX, int posY, ArrayList<String> targets){
    this.name = name;
    this.posX = posX;

    this.posY = posY;
    this.targets = targets;
}

public String getName() {
    return name;
}

public int getPosX() {
    return posX;
}

public int getPosY() {
    return posY;
}

public ArrayList<String> getTargets() {
    return targets;
}}

public class AddGraphNode extends Editor {

public AddGraphNode(String tag, String name, String style, int vertPosX, int vertPoxY){

    this.getGraph().getModel().beginUpdate();
    Object parent = this.getGraph().getDefaultParent();

    Document doc = mxDomUtils.createDocument();

    Element entity = doc.createElement(tag);
    entity.setAttribute("name", name);

    try
    {

        Object v1 = this.getGraph().insertVertex(parent, "1",entity, vertPosX, vertPoxY, (int)(name.length()*8) ,
                40, style);

    }
    finally
    {
        this.getGraph().getModel().endUpdate();
    }
}}

public class AddGraphLink extends Editor{

public AddGraphLink(Object v1, Object v2, String relation, String style){

    this.getGraph().getModel().beginUpdate();
    Object parent = this.getGraph().getDefaultParent();

    try
    {
        this.getGraph().insertEdge(parent, null, relation, v1, v2,style);
    }
    finally
    {
        this.getGraph().getModel().endUpdate();
    }
}}

When using the the add menu item some nodes and links are added to the graph and with the load 1 menu item a new Editor will be created (creating a new graph and graphComponent). However, the added nodes and links are still present on the new component.

Regarding the visually broken links I mentioned above, this does not occur...I will investigate this further. Despite that this might also be connected with the graphComponent problem.


Solution

  • I see 3 major problems in your code:

    1. Inappropriate use of a static field: protected static mxGraph graph = new mxGraph() {
    2. Inappropriate inheritance: public class AddGraphLink extends Editor {
    3. And again, inappropriate inheritance: class AddGraphNode extends Editor {

    By making the graph field static, changes made to one variable will be felt in all variables, and this is the likely cause for your so-called "artifacts". And the reason that you feel that you must make the field static is because since the two classes above are inheriting from Editor (again inappropriately). The solution is obvious:

    1. Make the graph field an instance field, not a static one, and
    2. Don't use inheritance where it doesn't belong. Instead your AddGraphXxxx classes should not extend Editor but rather should have Editor fields within them, one that you can set via the constructor, and one whose methods you can call, something like this, for example:

    // protected static mxGraph graph = new mxGraph() { //!!  **** NO ****
    private mxGraph graph = new mxGraph() {             //!!  **** YES ****
         .....
         .....
    

    public class AddGraphNode {
        public AddGraphNode(Editor editor, String tag, String name, String style, int vertPosX, int vertPoxY) {
            // **** note use of the editor parameter below ****
            editor.getGraph().getModel().beginUpdate();
    
            Object parent = editor.getGraph().getDefaultParent();
            Document doc = mxDomUtils.createDocument();
            Element entity = doc.createElement(tag);
            entity.setAttribute("name", name);
            try {
                // **** same here ****
                Object v1 = editor.getGraph().insertVertex(parent, "1", entity, vertPosX, vertPoxY,
                        (int) (name.length() * 8), 40, style);
            } finally {
                // **** and the same here ****
                editor.getGraph().getModel().endUpdate();
            }
        }
    }
    

    and you would create this instance within Editor by passing in the this parameter:

    new AddGraphNode2(this, "node", name,
            "rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;",
            vertPosX, vertPosY);    
    

    You would make simiar changes to the AddGraphLink class.


    Side note, please check out the The Use of Multiple JFrames, Good/Bad Practice? to see why swapping JFrames is not the best program design, and how you can change the code to improve its structure and user experience.