Collapsible/Expandable UML Class Node Representation

Tips & Tricks

Summary

Sample NodeRealizer implementation to represent UML class nodes.
For a better user experience, please go to the integrated documentation viewer to read this article.

Description

Representations for UML class nodes can become quite huge when a class has many attributes and/or methods. To counteract, a UML class node should be collapsible and expandable to reduce its size when needed.

The following sample code presents a NodeRealizer implementation that provides such a collapse/expand feature. It includes an inner class that defines a customized ViewMode implementation to toggle the state of a UML class node.

package demo.view;

import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import javax.swing.JFrame;

import y.base.NodeCursor;
import y.geom.YPoint;
import y.util.D;
import y.util.YVersion;
import y.view.EditMode;
import y.view.Graph2D;
import y.view.Graph2DView;
import y.view.NodeLabel;
import y.view.NodeRealizer;
import y.view.ShapeNodeRealizer;
import y.view.ViewMode;
import y.view.YLabel;
import y.view.hierarchy.GroupNodeRealizer;


/**
 * NodeRealizer implementation that represents a UML class node. 
 * This node realizer displays the following properties of a class 
 * in UML notation: 
 * <ul>
 *  <li>class name
 *  <li>stereotype property
 *  <li>constraint property
 *  <li>attribute list
 *  <li>method list
 * <ul>
 *
 * Executing this class will display a sample instance of this realizer. 
 */
public class UMLClassNodeRealizer extends ShapeNodeRealizer
{
  private NodeLabel aLabel; // attributeLabel 
  private NodeLabel mLabel; // methodLabel 
  private NodeLabel sLabel; // stateLabel

  private boolean clipContent;
  private boolean omitDetails;

  private String constraint = "";
  private String stereotype = "";
  private static NodeLabel constraintLabel = new NodeLabel(); 
  private static NodeLabel stereotypeLabel = new NodeLabel();

  /**
   * Instantiates a new UMLClassNodeRealizer. 
   */ 
  public UMLClassNodeRealizer()
  {
    init();
  }

  void init()
  {
    setShapeType(RECT_3D);

    getLabel().setModel(NodeLabel.INTERNAL);
    getLabel().setPosition(NodeLabel.TOP);

    getLabel().setFontSize(13);
    getLabel().setFontStyle(Font.BOLD);

    aLabel = new NodeLabel();
    aLabel.bindRealizer(this);
    aLabel.setAlignment(YLabel.ALIGN_LEFT);
    aLabel.setModel(NodeLabel.FREE);

    mLabel = new NodeLabel();
    mLabel.bindRealizer(this);
    mLabel.setAlignment(YLabel.ALIGN_LEFT);
    mLabel.setModel(NodeLabel.FREE);

    clipContent = true;
    omitDetails = false;

    sLabel = new NodeLabel();
    sLabel.bindRealizer(this);
    sLabel.setPosition(NodeLabel.TOP_RIGHT);
    sLabel.setIcon(GroupNodeRealizer.defaultOpenGroupIcon);
  }

  /**
   * Instantiates a new UMLClassNodeRealzier as a copy of a given 
   * realizer.
   */ 
  public UMLClassNodeRealizer(NodeRealizer r)
  {
    super(r);
    if (r instanceof UMLClassNodeRealizer)
    {
      UMLClassNodeRealizer cnr = (UMLClassNodeRealizer)r;
      aLabel = (NodeLabel)cnr.aLabel.clone();
      aLabel.bindRealizer(this);
      mLabel = (NodeLabel)cnr.mLabel.clone();
      mLabel.bindRealizer(this);
      constraint = cnr.constraint;
      stereotype = cnr.stereotype;
      clipContent = cnr.clipContent;
      omitDetails = cnr.omitDetails;  
      sLabel = (NodeLabel)cnr.sLabel.clone();
      sLabel.bindRealizer(this);
    }
    else
      init();
  }

  /**
   * Returns a UMLClassNodeRealizer that is a copy of the given 
   * realizer. 
   */
  public NodeRealizer createCopy(NodeRealizer r)
  {
    return new UMLClassNodeRealizer(r);
  }

  //////////////////////////////////////////////////////////////////////////////
  // SETTER & GETTER ///////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////

  /**
   * Set the class name to be displayed by this realizer. 
   */
  public void setClassName(String name)
  {
    setLabelText(name);
  }

  /**
   * Returns the class name to be displayed by this realizer. 
   */
  public String getClassName()
  {
    return getLabelText();
  }

  /**
   * Sets the constraint property of this realizer. 
   */
  public void setConstraint(String constraint)
  {
    this.constraint = constraint;
  }

  /**
   * Sets the stereotype property of this realizer. 
   */
  public void setStereotype(String stereotype)
  {
    this.stereotype = stereotype;
  }

  /**
   * Returns the constraint property of this realizer.
   */
  public String getConstraint()
  {
    return constraint;
  }

  /**
   * Returns the stereotype property of this realizer.
   */
  public String getStereotype()
  {
    return stereotype;
  }

  /**
   * Returns the node label that represents all added 
   * method strings. 
   */
  public NodeLabel getMethodLabel()
  {
    return mLabel;
  }

  /**
   * Returns the node label that represents all added 
   * attribute strings. 
   */
  public NodeLabel getAttributeLabel()
  {
    return aLabel;
  }

  /**
   * Returns the node label that represents the omitDetails 
   * state. 
   */
  public NodeLabel getStateLabel()
  {
    return sLabel;
  }

  /**
   * Returns whether or not the display of the labels should be 
   * clipped with the bounding box of the realizer. 
   */
  public boolean getClipContent()
  {
    return clipContent;
  }

  /**
   * Sets whether or not the display of the labels should be 
   * clipped with the bounding box of the realizer. 
   */
  public void setClipContent(boolean clipping)
  {
    clipContent = clipping;
  }

  /**
   * Set whether or not this realizer should omit details when being displayed. 
   */
  public void setOmitDetails(boolean b)
  {
    omitDetails = b;
    if (omitDetails)
      sLabel.setIcon(GroupNodeRealizer.defaultClosedGroupIcon);
    else
      sLabel.setIcon(GroupNodeRealizer.defaultOpenGroupIcon);
  }

  /**
   * Returns whether or not this realizer should omit details when being displayed. 
   */ 
  public boolean getOmitDetails()
  {
    return omitDetails;
  }

  private void addToLabel(NodeLabel l, String s)
  {
    if (l.getText().length() > 0)
      l.setText(l.getText() + "\n" + s);
    else
      l.setText(s);
  }

  /**
   * Adds a class method label to this realizer.
   */ 
  public void addMethod(String method)
  {
    addToLabel(mLabel, method);
  }

  /**
   * Adds a class attribute label to this realizer.
   */ 
  public void addAttribute(String attr)
  {
    addToLabel(aLabel, attr);
  }

  /**
   * Set the size of this realizer automatically. This method will adapt the size 
   * of this realizer so that the labels defined for it will fit within its 
   * bounding box. 
   */
  public void fitContent()
  {
    double height = 3.0;
    double width = getLabel().getWidth() + 10;

    if (stereotype.length() > 0)
    {
      NodeLabel l = new NodeLabel();
      l.setText("<<" + getStereotype() + ">>");
      l.setModel(NodeLabel.FREE);
      l.bindRealizer(this);
      height += l.getHeight() + 5;
      width = Math.max(l.getWidth() + 10, width);
    }

    height += getLabel().getHeight() + 3;

    if (constraint.length() > 0)
    {
      NodeLabel l = new NodeLabel();
      l.setText("{" + getConstraint() + "}");
      l.setModel(NodeLabel.FREE);
      height += l.getHeight() + 5;
      width = Math.max(l.getWidth() + 10, width);
    }

    if (!omitDetails && !(aLabel.getText().equals("") && mLabel.getText().equals("")))
    {
      height += 3;
      height += aLabel.getHeight() + 3;
      width = Math.max(aLabel.getWidth() + 10, width);
      height += 3;
      height += mLabel.getHeight() + 3;
      width = Math.max(mLabel.getWidth() + 10, width);
    }

    setSize(width, height);
  }

  //////////////////////////////////////////////////////////////////////////////
  // GRAPHICS  /////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////

  /**
   * Paint the labels associated with this realizer.
   */
  public void paintText(Graphics2D gfx)
  {    
    Rectangle oldClip = null;
    if (clipContent)
    {
      oldClip = gfx.getClipBounds();
      gfx.clipRect((int)x, (int)y, (int)width, (int)height);
    }

    sLabel.paint(gfx);

    double yoff = 3.0;

    if (stereotype.length() > 0)
    {
      NodeLabel l = new NodeLabel();
      l.setText("<<" + getStereotype() + ">>");
      l.setModel(NodeLabel.FREE);
      l.setOffset((getWidth() - l.getWidth()) / 2.0, yoff);
      l.bindRealizer(this);
      l.paint(gfx);
      yoff += l.getHeight() + 5;
    }

    NodeLabel label = getLabel();
    label.setOffset((getWidth() - label.getWidth()) / 2.0, yoff);
    label.paint(gfx);
    yoff += label.getHeight() + 3;

    if (constraint.length() > 0)
    {
      NodeLabel l = new NodeLabel();
      l.setText("{" + getConstraint() + "}");
      l.setModel(NodeLabel.FREE);
      l.setOffset(getWidth() - l.getWidth() - 5.0, yoff);
      l.bindRealizer(this);
      l.paint(gfx);
      yoff += l.getHeight() + 5;
    }

    if (!omitDetails && !(aLabel.getText().equals("") && mLabel.getText().equals("")))
    {
      gfx.setColor(getLineColor());
      gfx.drawLine((int)x + 1, (int)(y + yoff), (int)(x + width - 1), (int)(y + yoff));
      yoff += 3;
      aLabel.setOffset(3, yoff);
      aLabel.paint(gfx);
      yoff += aLabel.getHeight() + 3;
      gfx.drawLine((int)x + 1, (int)(y + yoff), (int)(x + width - 1), (int)(y + yoff));
      yoff += 3;
      mLabel.setOffset(3, yoff);
      mLabel.paint(gfx);
    }

    if (clipContent)
    {
      gfx.setClip(oldClip);
    }
  }

  //////////////////////////////////////////////////////////////////////////////
  // SERIALIZATION /////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////

  /**
   * Serialization routine that allows this realizer to be written out 
   * in YGF graph format. 
   */ 
  public void write(ObjectOutputStream out) throws IOException 
  {
    out.writeByte(YVersion.VERSION_1);
    super.write(out);
    aLabel.write(out);
    mLabel.write(out);
    out.writeBoolean(clipContent);
    out.writeBoolean(omitDetails);
    out.writeObject(getStereotype());
    out.writeObject(getConstraint());
  }

  /**
   * Deserialization routine that allows this realizer to be read in 
   * from YGF graph format. 
   */ 
  public void read(ObjectInputStream in) throws IOException, ClassNotFoundException 
  {
    switch (in.readByte())
    {
      case YVersion.VERSION_1:
        super.read(in);
        init();
        aLabel.read(in);
        mLabel.read(in);
        clipContent = in.readBoolean();
        omitDetails = in.readBoolean();
        stereotype = (String)in.readObject();
        constraint = (String)in.readObject();
        break;
      default:
        D.fatal("Unsupported Format");
    }
  }

  static class ToggleDetailsMode extends ViewMode
  {  
    public void mouseClicked(MouseEvent ev)
    {
      double x = translateX(ev.getX());
      double y = translateY(ev.getY());
      Graph2D graph = getGraph2D();

      for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next())
      {
        if (graph.getRealizer(nc.node()) instanceof UMLClassNodeRealizer)
        {
          UMLClassNodeRealizer cnr = (UMLClassNodeRealizer)graph.getRealizer(nc.node());
          NodeLabel l = cnr.getStateLabel();

          if (l.getBox().contains(x, y))
          {
            YPoint p = graph.getLocation(nc.node());
            cnr.setOmitDetails(!cnr.getOmitDetails());
            cnr.fitContent();
            graph.setLocation(nc.node(), p);
            graph.updateViews();
            break;
          }
        }
      }
    }
  }

  /**
   * Launcher method. Execute this method to see a sample instantiation of 
   * this node realizer in action. 
   */
  public static void main(String[] args)
  {
    JFrame frame = new JFrame();
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    Graph2DView view = new Graph2DView();

    view.addViewMode(new ToggleDetailsMode());

    frame.setContentPane(view);

    UMLClassNodeRealizer r = new UMLClassNodeRealizer();
    r.setClassName("com.mycompany.MyClass");
    r.setConstraint("abstract");
    r.setStereotype("factory");
    r.addAttribute("-graph");
    r.addAttribute("-id");
    r.addMethod("+setGraph(Graph)");
    r.addMethod("+getGraph():Graph");
    r.addMethod("+setID(int)");
    r.addMethod("+getID():int");
    r.fitContent();

    view.getGraph2D().setDefaultNodeRealizer(r);

    view.getGraph2D().createNode();
    view.addViewMode(new EditMode());
    view.fitContent();

    frame.pack();
    frame.setVisible(true);
  }
}

Resources

Categories this article belongs to:
yFiles for Java > yFiles Viewer > Displaying and Editing Graphs > Bringing Graph Elements to Life: The Realizer Concept
Applies to:
yFiles for Java 2: 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10, 2.11, 2.12, 2.13, 2.14, 2.15, 2.16, 2.17, 2.18
Keywords:
UML - class - NodeRealizer - node - representation - rendering - graphical - collapse - expand - collapsing - expanding