/* Copyright (C) 2006 EBI This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the itmplied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package org.biomart.builder.view.gui.diagrams; import java.awt.AWTEvent; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Insets; import java.awt.LayoutManager; import java.awt.Point; import java.awt.Rectangle; import java.awt.dnd.Autoscroll; import java.awt.dnd.DnDConstants; import java.awt.dnd.DropTarget; import java.awt.dnd.DropTargetDragEvent; import java.awt.dnd.DropTargetDropEvent; import java.awt.dnd.DropTargetEvent; import java.awt.dnd.DropTargetListener; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; import javax.swing.ImageIcon; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JLayeredPane; import javax.swing.JList; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JViewport; import javax.swing.ListCellRenderer; import javax.swing.Scrollable; import javax.swing.SwingUtilities; import org.biomart.builder.model.Table; import org.biomart.builder.model.DataSet.DataSetTable; import org.biomart.builder.view.gui.MartTabSet.MartTab; import org.biomart.builder.view.gui.MartTabSet.PartitionViewSelectionListener; import org.biomart.builder.view.gui.diagrams.components.BoxShapedComponent; import org.biomart.builder.view.gui.diagrams.components.DiagramComponent; import org.biomart.builder.view.gui.diagrams.contexts.DiagramContext; import org.biomart.common.resources.Log; import org.biomart.common.resources.Resources; import org.biomart.common.utils.InverseMap; import org.biomart.common.utils.Transaction; import org.biomart.common.utils.Transaction.TransactionEvent; import org.biomart.common.utils.Transaction.TransactionListener; import org.biomart.common.view.gui.LongProcess; import org.biomart.common.view.gui.dialogs.ComponentImageSaver; import org.biomart.common.view.gui.dialogs.ComponentPrinter; /** * A diagram represents a collection of database components. It usually contains * components that are tables, which themselves contain other objects which are * keys and columns. The diagram also contains components which are relations, * which link key objects. The diagram remembers all this, and provides a * context-menu handling for the components it displays. {@link DiagramContext} * listeners can be attached to the diagram to customise context menu rendering, * and also to customise rendering of the individual components, for instance in * order to apply alternative colour schemes. * <p> * Specific extensions of this basic diagram class handle the decisions as to * what to add and what to remove from the diagram. This base class simply deals * with the context menus and display of components in the diagram. * <p> * Autoscrolling code borrowed from <a * href="http://www.javalobby.org/java/forums/t53436.html">http://www.javalobby.org/java/forums/t53436.html</a>. * * @author Richard Holland <holland@ebi.ac.uk> * @version $Revision: 1.52 $, $Date: 2007-10-31 13:06:07 $, modified by * $Author: rh4 $ * @since 0.5 */ public abstract class Diagram extends JLayeredPane implements Scrollable, Autoscroll, AdjustmentListener, TransactionListener, PartitionViewSelectionListener { /** * This is inherited by subclasses to indicate they need redrawing when the * next transaction ends. */ protected boolean needsRecalc = false; /** * This is inherited by subclasses to indicate they need repainting when the * next transaction ends. */ protected boolean needsRepaint = false; /** * This is public only so that the diagram picks up component changes. */ public boolean needsSubComps = false; private static final int AUTOSCROLL_INSET = 12; /** * The background colour to use for this diagram. */ public static final Color BACKGROUND_COLOUR = Color.WHITE; /** * The background for the masked checkbox. */ public static final Color MASK_BG_COLOR = Color.WHITE; /** * The layer for always-on-top components. */ public static final int TOP_LAYER = 0; /** * The layer for middle components. */ public static final int TABLE_LAYER = 0; /** * The layer for always-bottom components. */ public static final int RELATION_LAYER = -1; // OK to use maps as it gets cleared out each time, the keys never change. private final Map componentMap = new HashMap(); private DiagramContext diagramContext; private MartTab martTab; private final List selectedItems = new ArrayList(); private JCheckBox hideMasked; private final PropertyChangeListener listener = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent e) { Diagram.this.needsSubComps = true; } }; /** * Creates a new diagram which belongs inside the given mart tab and uses * the given layout manager. The {@link MartTab#getMart()} method will be * used to work out which mart is being interacted with when the user * selects items in the context menus attached to components in this * diagram. * * @param layout * the layout manager to use to layout the diagram. If * <tt>null</tt>, a default manager will be used that does not * recognise the distinction between relations and tables. * @param martTab * the mart tab this diagram will use to discover which mart is * currently visible when working out where to send events. */ public Diagram(final LayoutManager layout, final MartTab martTab) { // Set us up with the layout. super(); if (layout != null) this.setLayout(layout); else this.setLayout(new FlowLayout()); Log.debug("Creating new diagram of type " + this.getClass().getName()); // Enable mouse events to be picked up all over the diagram. this.enableEvents(AWTEvent.MOUSE_EVENT_MASK); this.setDoubleBuffered(true); // Stop flicker. // Remember our settings. this.martTab = martTab; // Create the hide masked box. this.hideMasked = new JCheckBox(Resources.get("hideMaskedTitle")); this.hideMasked.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { Transaction.start(false); Diagram.this.hideMaskedChanged(Diagram.this.hideMasked .isSelected()); Transaction.end(); } }); // It has a semi-transparent background with no border. this.hideMasked.setOpaque(true); this.hideMasked.setBackground(Diagram.MASK_BG_COLOR); // Deal with drops. final DropTargetListener dtListener = new DropTargetListener() { public void dragEnter(DropTargetDragEvent e) { e.rejectDrag(); } public void dragOver(DropTargetDragEvent e) { e.rejectDrag(); } public void dropActionChanged(DropTargetDragEvent e) { e.rejectDrag(); } public void dragExit(DropTargetEvent e) { } public void drop(DropTargetDropEvent e) { e.rejectDrop(); } }; new DropTarget(this, DnDConstants.ACTION_COPY, dtListener, true); // Set our background. this.setBackground(Diagram.BACKGROUND_COLOUR); this.setOpaque(true); // Repaint whenever the mart tab partition filter changes. martTab.addPartitionViewSelectionListener(this); // Register ourselves for transactions. Transaction.addTransactionListener(this); } /** * Creates a new diagram which belongs inside the given mart tab and uses * the default layout manager. The {@link MartTab#getMart()} method will be * used to work out which mart is being interacted with when the user * selects items in the context menus attached to components in this * diagram. * <p> * See {@link #Diagram(LayoutManager, MartTab)} with a null first param. * * @param martTab * the mart tab this diagram will use to discover which mart is * currently visible when working out where to send events. */ public Diagram(final MartTab martTab) { this(null, martTab); } public void partitionViewSelectionChanged() { this.repaintDiagram(); } /** * Override this to find out when the hide masked checkbox changes. * * @param newHideMasked * true if it is now selected. */ protected void hideMaskedChanged(final boolean newHideMasked) { // By default we don't care. } /** * Set the hide masked checkbox. * * @param newHideMasked * true to select it. */ public void setHideMasked(final boolean newHideMasked) { if (this.hideMasked.isSelected() != newHideMasked) this.hideMasked.doClick(); } /** * Is the hide masked checkbox selected? * * @return true if it is. */ public boolean isHideMasked() { return this.hideMasked.isSelected(); } public void setDirectModified(final boolean modified) { // Ignore, for now. } public boolean isDirectModified() { return false; } public void setVisibleModified(final boolean modified) { // Ignore, for now. } public boolean isVisibleModified() { return false; } public void transactionResetVisibleModified() { // Ignore, for now. } public void transactionResetDirectModified() { // Ignore, for now. } public void transactionStarted(final TransactionEvent evt) { // Ignore, for now. } public void transactionEnded(final TransactionEvent evt) { if (this.needsSubComps) this.recalculateSubComps(); if (this.needsRecalc) { // Make sure this is on the Swing event thread. SwingUtilities.invokeLater(new Runnable() { public void run() { new LongProcess() { public void run() { Diagram.this.recalculateDiagram(); } }.start(); } }); } else if (this.needsRepaint) { // Make sure this is on the Swing event thread. SwingUtilities.invokeLater(new Runnable() { public void run() { new LongProcess() { public void run() { Diagram.this.repaintDiagram(); } }.start(); } }); } this.needsRepaint = false; this.needsRecalc = false; this.needsSubComps = false; } private void recalculateSubComps() { final Collection comps = Arrays.asList(this.getComponents()); for (final Iterator i = this.componentMap.entrySet().iterator(); i .hasNext();) { final Map.Entry entry = (Map.Entry) i.next(); if (!comps.contains(entry.getValue())) i.remove(); } final Map subCompMap = new HashMap(); for (final Iterator i = this.componentMap.values().iterator(); i .hasNext();) { final Object o = i.next(); if (o instanceof DiagramComponent) subCompMap.putAll(((DiagramComponent) o).getSubComponents()); } this.componentMap.putAll(subCompMap); } /** * Highlight the given item and dehighlight all others. * * @param item * the item to highlight. */ public void selectOnlyItem(final BoxShapedComponent item) { // Select this item only and clear rest of group. this.deselectAll(); this.selectedItems.add(item); item.select(); } /** * Toggle the given item highlight on/off and dehighlight all others. * * @param item * the item to toggle. */ public void toggleItem(final BoxShapedComponent item) { // (De)select this item only and clear rest of group. boolean selected = this.isSelected(item); this.deselectAll(); if (!selected) { this.selectedItems.add(item); item.select(); } } /** * For each item in the collection, toggle it on/off, whilst preserving the * other selected items not mentioned. * * @param items * the collection of items to toggle. Can be empty but never * <tt>null</tt>. */ public void toggleGroupItems(final Collection items) { // Cancel all renames first. for (final Iterator i = this.selectedItems.iterator(); i.hasNext();) ((BoxShapedComponent) i.next()).cancelRename(); for (final Iterator i = items.iterator(); i.hasNext();) { final BoxShapedComponent item = (BoxShapedComponent) i.next(); // (De)select this item within existing group. if (this.isSelected(item)) { // Unselect it. this.selectedItems.remove(item); item.deselect(); } else { // Select it. if (!this.selectedItems.isEmpty() && !this.selectedItems.get(0).getClass().equals( item.getClass())) this.deselectAll(); this.selectedItems.add(item); item.select(); } } } /** * For this item only, toggle it on/off, whilst preserving the other * selected items. * * @param item * the item to toggle. */ public void toggleGroupItem(final BoxShapedComponent item) { // Cancel all renames first. for (final Iterator i = this.selectedItems.iterator(); i.hasNext();) ((BoxShapedComponent) i.next()).cancelRename(); // (De)select this item within existing group. if (this.isSelected(item)) { // Unselect it. this.selectedItems.remove(item); item.deselect(); } else { // Select it. if (!this.selectedItems.isEmpty() && !this.selectedItems.get(0).getClass().equals( item.getClass())) this.deselectAll(); this.selectedItems.add(item); item.select(); } } /** * Dehighlights all items. */ public void deselectAll() { for (final Iterator i = this.selectedItems.iterator(); i.hasNext();) ((BoxShapedComponent) i.next()).deselect(); this.selectedItems.clear(); } /** * Check to see if the given item is highlighted in this diagram. * * @param item * the item to check. * @return <tt>true</tt> if it is highlighted. */ public boolean isSelected(final BoxShapedComponent item) { return this.selectedItems.contains(item); } /** * Return all currently selected items in this diagram. * * @return the currently selected items. May be empty but never * <tt>null</tt>. */ public Collection getSelectedItems() { return this.selectedItems; } /** * Return all components that intersect a region. The region is defined by * the smallest box possible that contains the entire bounding boxes of the * two components specified. * * @param oneCorner * one of the two components that specify the region. * @param otherCorner * the other component that specifies the region. * @param componentClass * the type of components to search for within the region. * @return the components found in that region. May be empty but never * <tt>null</tt>. Will include the two components used to specify * the region if they are of the correct class. */ public Collection getComponentsInRegion(final Component oneCorner, final Component otherCorner, final Class componentClass) { final Rectangle firstCorner = SwingUtilities.convertRectangle(oneCorner .getParent(), oneCorner.getBounds(), this); final Rectangle secondCorner = SwingUtilities.convertRectangle( otherCorner.getParent(), otherCorner.getBounds(), this); final Rectangle clipRegion = firstCorner.union(secondCorner); final Collection results = new HashSet(); for (final Iterator i = this.componentMap.entrySet().iterator(); i .hasNext();) { final Map.Entry entry = (Map.Entry) i.next(); if (!entry.getKey().getClass().equals(componentClass)) continue; final Component candidate = (Component) entry.getValue(); final Rectangle candRect = SwingUtilities.convertRectangle( candidate.getParent(), candidate.getBounds(), this); if (clipRegion.contains(candRect)) results.add(candidate); } return results; } private Table askUserForTable() { // Pop up a dialog box with a list of tables in it, and ask the // user to select one. Only tables which appear in this diagram will // be in the list. // First, work out what tables are in this diagram. final Map sortedTables = new TreeMap(); for (final Iterator i = this.componentMap.keySet().iterator(); i .hasNext();) { final Object o = i.next(); if (o instanceof DataSetTable) sortedTables.put(((DataSetTable) o).getModifiedName(), o); else if (o instanceof Table) sortedTables.put(((Table) o).getName(), o); } final Map lookup = new InverseMap(sortedTables); // Create a combo box of tables. final JComboBox tableChoice = new JComboBox(); tableChoice.setRenderer(new ListCellRenderer() { public Component getListCellRendererComponent(final JList list, final Object value, final int index, final boolean isSelected, final boolean cellHasFocus) { final Table tbl = (Table) value; final JLabel label = new JLabel((String) lookup.get(tbl)); label.setOpaque(true); label.setFont(list.getFont()); if (isSelected) { label.setBackground(list.getSelectionBackground()); label.setForeground(list.getSelectionForeground()); } else { label.setBackground(list.getBackground()); label.setForeground(list.getForeground()); } return label; } }); for (final Iterator i = sortedTables.values().iterator(); i.hasNext();) tableChoice.addItem(i.next()); // Now, create the choices box, display it, and return the one // that the user selected. If the user didn't select anything, or // cancelled the choice, this will return null. JOptionPane.showMessageDialog(null, tableChoice, Resources .get("findTableName"), JOptionPane.QUESTION_MESSAGE, null); // Return the choice. return (Table) tableChoice.getSelectedItem(); } private JPopupMenu populateContextMenu(final JPopupMenu contextMenu) { // This is the basic context menu that appears no matter where the user // clicks. // If the menu is not empty, add a separator before we // add the context stuff. if (contextMenu.getComponentCount() > 0) contextMenu.addSeparator(); // Add an item that allows the user to search for a particular // table in the diagram, and scroll to that table when selected. final JMenuItem find = new JMenuItem(Resources.get("findTableTitle")); find.setMnemonic(Resources.get("findTableMnemonic").charAt(0)); find.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { final Table table = Diagram.this.askUserForTable(); if (table != null) Diagram.this.findObject(table); } }); contextMenu.add(find); contextMenu.addSeparator(); // Add an item that allows the user to save this diagram as an image. final JMenuItem save = new JMenuItem(Resources.get("saveDiagramTitle"), new ImageIcon(Resources.getResourceAsURL("save.gif"))); save.setMnemonic(Resources.get("saveDiagramMnemonic").charAt(0)); save.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { Diagram.this.saveDiagram(); } }); contextMenu.add(save); // Add an item that allows the user to print this diagram. final JMenuItem print = new JMenuItem(Resources .get("printDiagramTitle"), new ImageIcon(Resources .getResourceAsURL("print.gif"))); print.setMnemonic(Resources.get("printDiagramMnemonic").charAt(0)); print.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { Diagram.this.printDiagram(); } }); contextMenu.add(print); // Return the completed context menu. if (this.diagramContext != null) this.diagramContext.populateContextMenu(contextMenu, this); return contextMenu; } private void printDiagram() { new ComponentPrinter(this).print(); } private void saveDiagram() { new ComponentImageSaver(this).save(); } protected void processMouseEvent(final MouseEvent evt) { boolean eventProcessed = false; if (evt.getButton() > 0) this.deselectAll(); // Is it a right-click? if (evt.isPopupTrigger()) { // Obtain the basic context menu for this diagram. final JPopupMenu contextMenu = new JPopupMenu(); // Add the common diagram stuff. this.populateContextMenu(contextMenu); // If our context menu actually has anything in it now, display it. if (contextMenu.getComponentCount() > 0) { contextMenu.show(this, evt.getX(), evt.getY()); eventProcessed = true; } } // Pass the event on up if we're not interested. if (!eventProcessed) super.processMouseEvent(evt); } protected void addImpl(final Component comp, final Object constraints, final int index) { if (comp instanceof DiagramComponent) { final DiagramComponent dcomp = (DiagramComponent) comp; this.needsSubComps = true; this.componentMap.put(dcomp.getObject(), dcomp); dcomp.getSubComponents().addPropertyChangeListener(this.listener); } super.addImpl(comp, constraints, index); } public void remove(final Component comp) { if (comp instanceof DiagramComponent) this.componentMap.remove(((DiagramComponent) comp).getObject()); super.remove(comp); } public void remove(final int index) { final Object comp = this.getComponent(index); if (comp instanceof DiagramComponent) this.componentMap.remove(((DiagramComponent) comp).getObject()); super.remove(index); } public void removeAll() { // Clear our internal lookup map. this.componentMap.clear(); // Do what the parent JComponent would do. super.removeAll(); } /** * Override this method to actually do the work of recalculating which * components should appear in the diagram. The method should first clear * out all the old components from the diagram, as this will not have been * done already. On return, the diagram should contain a new set of * components, or an updated set of components that correctly reflects its * current state. */ public abstract void doRecalculateDiagram(); /** * Given a particular model object, lookup the diagram component that it * represents, then scroll the diagram so that it is centred on that diagram * component. This depends on the diagram being held within a * {@link JScrollPane} - if it isn't, this method will do nothing. * * @param object * the database object to locate and scroll to. */ public void findObject(final Object object) { // Don't do it if the object is null or if we are not in a viewport. if (object == null) return; // Look up the diagram component for the model object. final JComponent comp = (JComponent) this.getDiagramComponent(object); // If the model object is not in this diagram, don't scroll to it! if (comp == null) return; // Obtain the scrollpane view of this diagram. final JViewport viewport = (JViewport) SwingUtilities .getAncestorOfClass(JViewport.class, this); if (viewport != null) { // Work out the location of the diagram component. final Rectangle compLocation = SwingUtilities.convertRectangle(comp .getParent(), comp.getBounds(), this); // How big is the scrollpane view we are being seen through? final Dimension viewSize = viewport.getExtentSize(); final Dimension ourSize = this.getSize(); // Work out the top-left coordinate of the area of diagram that // should appear in the scrollpane if this diagram component is to // appear in the absolute centre. int newViewPointX = (int) compLocation.getCenterX() - viewSize.width / 2; int newViewPointY = (int) compLocation.getCenterY() - viewSize.height / 2; // Move the scrollpoint if it goes off the bottom-right. if (newViewPointX + viewSize.width > ourSize.width) newViewPointX = ourSize.width - viewSize.width; if (newViewPointY + viewSize.height > ourSize.height) newViewPointY = ourSize.height - viewSize.height; // Move the scrollpoint if it goes off the top-left of the diagram // or if the whole diagram can fit without scrolling. if (newViewPointX < 0) newViewPointX = 0; if (newViewPointY < 0) newViewPointY = 0; // Scroll to that position. viewport.setViewPosition(new Point(newViewPointX, newViewPointY)); } // Select the object. if (comp instanceof BoxShapedComponent) this.toggleItem((BoxShapedComponent) comp); // Repaint newly visible area. this.repaint(this.getVisibleRect()); } /** * Looks up the diagram component in this diagram that is related to the * specified database object. If there is no component related to that * object, then null is returned, otherwise the component is returned. * * @param object * the database object to look up the component for. * @return the diagram component that represents that database object in * this diagram, or null if that model object is not in this diagram * at all. */ public DiagramComponent getDiagramComponent(final Object object) { return (DiagramComponent) this.componentMap.get(object); } /** * Returns the diagram context that is being used to customise colours and * context menus for this diagram. * * @return the diagram context that is being used. */ public DiagramContext getDiagramContext() { return this.diagramContext; } /** * Obtain a reference to the mart tab this diagram was registered with. * * @return the mart tab provided at construction time for this diagram. */ public MartTab getMartTab() { return this.martTab; } /** * This method is called when the diagram needs to be cleared and * repopulated. It remembers the states of all the components in the * diagram, then delegates to {@link #doRecalculateDiagram()} to do the * actual work of clearing out and repopulating the diagram. Finally, it * reapplies the states remembered to any components in the new diagram that * match the components in the old diagram using the * {@link Object#equals(Object)} method. */ public void recalculateDiagram() { Log.debug("Recalculating diagram"); this.deselectAll(); // Remember states. final Map stateMap = new HashMap(); for (final Iterator i = this.componentMap.entrySet().iterator(); i .hasNext();) { final Map.Entry entry = (Map.Entry) i.next(); final Object o = entry.getValue(); if (o instanceof BoxShapedComponent) stateMap.put(entry.getKey(), ((BoxShapedComponent) o) .getState()); } // First of all, remove all our existing components. this.removeAll(); this.componentMap.clear(); // Delegate to do the actual diagram // clear-and-repopulate. this.doRecalculateDiagram(); // Do the subcomp thing. this.recalculateSubComps(); // Reinstate states. for (final Iterator i = stateMap.entrySet().iterator(); i.hasNext();) { final Map.Entry entry = (Map.Entry) i.next(); final BoxShapedComponent o = (BoxShapedComponent) this.componentMap .get(entry.getKey()); if (o != null && entry.getValue() != null) o.setState(entry.getValue()); } // Set up a floating panel with the hide masked box. if (this.isUseHideMasked()) this.add(Diagram.this.hideMasked, null, Diagram.TOP_LAYER); // Resize the diagram to fit our new components. this.resizeDiagram(); // Initial placement of the hide masked button. this.adjustmentValueChanged(null); // Repaint the whole diagram to update the state of any // new bits and remove any ghosts that may be left on // screen. this.repaintDiagram(); } /** * This method walks through the components in the diagram, and calls * {@link DiagramComponent#repaintDiagramComponent()} on each in turn. This * has the effect of updating the appearance of every component and causing * it to redraw. It does not recalculate the location of any of these * components, neither does it recalculate which components are displayed in * the diagram at present. * <p> * This method does not resize the diagram to fit components, so do not use * it if the component size is likely to have changed (eg. show/hide columns * on a table). Use {@link #recalculateDiagram()} instead. */ public void repaintDiagram() { for (final Iterator i = this.componentMap.values().iterator(); i .hasNext();) ((DiagramComponent) i.next()).repaintDiagramComponent(); this.repaint(); } /** * Are we waiting to recalculate? * * @return <tt>true</tt> if we are. */ public boolean isNeedsRecalc() { return this.needsRecalc; } /** * Are we waiting to redraw? * * @return <tt>true</tt> if we are. */ public boolean isNeedsRepaint() { return this.needsRepaint; } /** * Work out the minimum size for this diagram, resize ourselves to that * size, then validate ourselves so our contents get laid out correctly. */ public void resizeDiagram() { // Reset our size to the minimum. this.setSize(this.getPreferredSize()); // Update ourselves. this.revalidate(); } /** * Work out how much space the masked/hidden button needs. * * @return the space it needs. */ protected Dimension getHideMaskedArea() { if (this.isUseHideMasked()) return this.hideMasked.getPreferredSize(); else return new Dimension(0, 0); } public Dimension getPreferredSize() { // Stretch ourselves to fill the viewport if we are smaller than it. Dimension preferredSize = this.getLayout().preferredLayoutSize(this); final JViewport viewport = this.getParent() instanceof JViewport ? (JViewport) this .getParent() : null; if (viewport != null) preferredSize = new Dimension((int) Math.max(preferredSize .getWidth(), viewport.getWidth()), (int) Math.max( preferredSize.getHeight(), viewport.getHeight())); return preferredSize; } /** * Sets the diagram context that will be used to customise colours and * context menus for this diagram. * * @param diagramContext * the diagram context to use. */ public void setDiagramContext(final DiagramContext diagramContext) { Log.debug("Switching diagram context"); // Apply it to ourselves. if (diagramContext != this.diagramContext) { this.diagramContext = diagramContext; this.repaintDiagram(); } } public void autoscroll(final Point cursorLoc) { final JViewport viewport = (JViewport) SwingUtilities .getAncestorOfClass(JViewport.class, this); if (viewport == null) return; final Point viewPos = viewport.getViewPosition(); final int viewHeight = viewport.getExtentSize().height; final int viewWidth = viewport.getExtentSize().width; // perform scrolling if (cursorLoc.y - viewPos.y < Diagram.AUTOSCROLL_INSET) viewport.setViewPosition(new Point(viewPos.x, Math.max(viewPos.y - Diagram.AUTOSCROLL_INSET, 0))); else if (viewPos.y + viewHeight - cursorLoc.y < Diagram.AUTOSCROLL_INSET) // down viewport .setViewPosition(new Point(viewPos.x, Math.min(viewPos.y + Diagram.AUTOSCROLL_INSET, this.getHeight() - viewHeight))); else if (cursorLoc.x - viewPos.x < Diagram.AUTOSCROLL_INSET) // left viewport.setViewPosition(new Point(Math.max(viewPos.x - Diagram.AUTOSCROLL_INSET, 0), viewPos.y)); else if (viewPos.x + viewWidth - cursorLoc.x < Diagram.AUTOSCROLL_INSET) // right viewport.setViewPosition(new Point(Math.min(viewPos.x + Diagram.AUTOSCROLL_INSET, this.getWidth() - viewWidth), viewPos.y)); } public Insets getAutoscrollInsets() { final int height = this.getHeight(); final int width = this.getWidth(); return new Insets(height, width, height, width); } public Dimension getPreferredScrollableViewportSize() { return this.getPreferredSize(); } public int getScrollableBlockIncrement(final Rectangle visibleRect, final int orientation, final int direction) { return this.getScrollableUnitIncrement(visibleRect, orientation, direction) * 4; } public boolean getScrollableTracksViewportHeight() { return false; } public boolean getScrollableTracksViewportWidth() { return false; } public int getScrollableUnitIncrement(final Rectangle visibleRect, final int orientation, final int direction) { return Diagram.AUTOSCROLL_INSET; } /** * Should we hide masked things? * * @return <tt>true</tt> if we should. */ protected boolean isUseHideMasked() { return true; } public void adjustmentValueChanged(final AdjustmentEvent evt) { if (!this.isUseHideMasked()) return; // This panel hangs out top-left regardless of viewport // scrolling. final Dimension buttonSize = this.getHideMaskedArea(); JViewport viewport = null; if (this.getParent() != null && this.getParent() instanceof JViewport) viewport = (JViewport) this.getParent(); if (viewport != null) { final Rectangle viewportOffset = viewport.getViewRect(); this.hideMasked.setBounds(viewportOffset.x + viewportOffset.width - buttonSize.width, viewportOffset.y, buttonSize.width, buttonSize.height); } else this.hideMasked.setBounds(this.getPreferredSize().width - buttonSize.width, 0, buttonSize.width, buttonSize.height); // To wipe out the opaque background a repaint is necessary. Diagram.this.hideMasked.repaint(); } }