// This file is part of PleoCommand: // Interactively control Pleo with psychobiological parameters // // Copyright (C) 2010 Oliver Hoffmann - Hoffmann_Oliver@gmx.de // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Boston, USA. package pleocmd.itfc.gui; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Graphics; import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.InputEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionListener; import java.awt.geom.Line2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.FilteredImageSource; import java.awt.image.ImageFilter; import java.awt.image.RGBImageFilter; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.imageio.ImageIO; import javax.swing.AbstractButton; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JCheckBoxMenuItem; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.MenuElement; import javax.swing.SwingConstants; import javax.swing.ToolTipManager; import javax.swing.filechooser.FileNameExtensionFilter; import pleocmd.ImmutableRectangle; import pleocmd.Log; import pleocmd.StringManip; import pleocmd.cfg.ConfigValue; import pleocmd.exc.InternalException; import pleocmd.exc.PipeException; import pleocmd.exc.StateException; import pleocmd.itfc.gui.BoardPainter.PaintParameters; import pleocmd.itfc.gui.Layouter.Button; import pleocmd.itfc.gui.help.HelpLoader; import pleocmd.itfc.gui.icons.IconLoader; import pleocmd.pipe.Pipe; import pleocmd.pipe.PipePart; import pleocmd.pipe.PipePart.HelpKind; import pleocmd.pipe.PipePartDetection; import pleocmd.pipe.cvt.Converter; import pleocmd.pipe.in.Input; import pleocmd.pipe.out.Output; final class PipeConfigBoard extends JPanel { private static final long serialVersionUID = -4525676341777864359L; /** * Square of distance in pixel to a line to consider it clicked. */ private static final double LINE_CLICK_DIST = 10; /** * Grow repaint clips by this amount to consider labels of connections. */ private static final int GROW_LABEL_REDRAW = 14; /** * Whether to snap components during drag&drop operations to an imaginary * raster.<br> * 0 for no raster, raster distance in pixel otherwise. */ private static final int SNAP_TO_GRID = 10; private final Pipe pipe; private final PaintParameters p = new PaintParameters(); private final BoardPainter painter; private final JPopupMenu menuInput; private final JPopupMenu menuConverter; private final JPopupMenu menuOutput; private int idxMenuAdd; private int idxMenuRepl; private int idxMenuConfPart; private int idxMenuDelPart; private int idxMenuDelPartConn; private int idxMenuDelConn; private int idxMenuToggleDgr; private int idxMenuClearBoard; private int idxMenuLayoutBoard; private int idxMenuExportBoard; private Point lastMenuLocation; private Point handlePoint; private boolean delayedReordering; private Thread layoutThread; private boolean closed; public PipeConfigBoard(final Pipe pipe) { this.pipe = pipe; painter = new BoardPainter(); setPreferredSize(new Dimension(400, 300)); ToolTipManager.sharedInstance().registerComponent(this); menuInput = createMenu("Input", Input.class); menuConverter = createMenu("Converter", Converter.class); menuOutput = createMenu("Output", Output.class); updateState(); addMouseListener(new MouseAdapter() { @Override public void mousePressed(final MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON3) showPopup(e); else updateCurrent(e.getPoint()); } @Override public void mouseReleased(final MouseEvent e) { if (e.getModifiers() == InputEvent.BUTTON1_MASK) switch (e.getClickCount()) { case 1: checkIconClicked(); break; case 2: configureCurrentPart(true); break; } releaseCurrent(); } private void showPopup(final MouseEvent e) { updateCurrent(e.getPoint()); showMenu(PipeConfigBoard.this, e.getX(), e.getY()); } }); addMouseMotionListener(new MouseMotionListener() { @Override public void mouseDragged(final MouseEvent e) { if (e.getModifiers() != InputEvent.BUTTON1_MASK) return; PipeConfigBoard.this.mouseDragged(e.getPoint()); } @Override public void mouseMoved(final MouseEvent e) { PipeConfigBoard.this.mouseMoved(e.getPoint()); } }); addComponentListener(new ComponentAdapter() { @Override public void componentResized(final ComponentEvent e) { updateBounds(getWidth(), getHeight()); } }); EventQueue.invokeLater(new Runnable() { @Override public void run() { assignFromPipe(); } }); } protected void assignFromPipe() { painter.setBounds(getWidth(), getHeight(), false); painter.setPipe(getPipe(), getGraphics(), false); updatePrefBounds(); repaint(); } public Pipe getPipe() { return pipe; } @Override protected void paintComponent(final Graphics g) { p.g = g; painter.paint(p); } private JPopupMenu createMenu(final String name, final Class<? extends PipePart> clazz) { final JPopupMenu menu = new JPopupMenu(); idxMenuAdd = menu.getSubElements().length; final JMenu menuAdd = new JMenu("Add " + name); menuAdd.setIcon(IconLoader.getIcon("list-add")); menu.add(menuAdd); for (final Class<? extends PipePart> pp : PipePartDetection.ALL_PIPEPART) if (clazz.isAssignableFrom(pp)) { final JMenuItem item = new JMenuItem(PipePart.getName(pp), PipePart.getIcon(pp)); menuAdd.add(item); item.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { addPipePart(pp, getLastMenuLocation()); } }); item.setToolTipText(PipePart.getDescription(pp)); } idxMenuRepl = menu.getSubElements().length; final JMenu menuRepl = new JMenu("Replace " + name + " With"); menuRepl.setIcon(IconLoader.getIcon("edit-rename")); menu.add(menuRepl); for (final Class<? extends PipePart> pp : PipePartDetection.ALL_PIPEPART) if (clazz.isAssignableFrom(pp)) { final JMenuItem item = new JMenuItem(PipePart.getName(pp), PipePart.getIcon(pp)); menuRepl.add(item); item.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { replacePipePart(pp); } }); item.setToolTipText(PipePart.getDescription(pp)); } menu.addSeparator(); idxMenuConfPart = menu.getSubElements().length; final JMenuItem itemConfPart = new JMenuItem("Configure This PipePart", BoardPainter.ICON_CONF); itemConfPart.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { configureCurrentPart(false); } }); menu.add(itemConfPart); idxMenuDelPart = menu.getSubElements().length; final JMenuItem itemDelPart = new JMenuItem("Delete This PipePart", IconLoader.getIcon("del-pipepart")); itemDelPart.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { removeCurrentPart(); } }); menu.add(itemDelPart); idxMenuDelPartConn = menu.getSubElements().length; final JMenuItem itemDelPartConn = new JMenuItem( "Delete Connections Of This PipePart", IconLoader.getIcon("del-connection-all")); itemDelPartConn.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { removeCurrentPartsConnections(); } }); menu.add(itemDelPartConn); idxMenuDelConn = menu.getSubElements().length; final JMenuItem itemDelConn = new JMenuItem("Delete This Connection", IconLoader.getIcon("del-connection")); itemDelConn.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { removeCurrentConnection(); } }); menu.add(itemDelConn); idxMenuToggleDgr = menu.getSubElements().length; final JCheckBoxMenuItem itemToggleDgr = new JCheckBoxMenuItem( "Visualize PipePart's Output", BoardPainter.ICON_DGR); itemToggleDgr.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { toggleDiagram(); } }); menu.add(itemToggleDgr); menu.addSeparator(); idxMenuClearBoard = menu.getSubElements().length; final JMenuItem itemClearBoard = new JMenuItem("Clear The Board", IconLoader.getIcon("board-clear")); itemClearBoard.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { clearBoard(); } }); menu.add(itemClearBoard); idxMenuLayoutBoard = menu.getSubElements().length; final JMenuItem itemLayoutBoard = new JMenuItem( "Auto-Layout The Board", IconLoader.getIcon("board-auto")); itemLayoutBoard.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { layoutBoard(); } }); menu.add(itemLayoutBoard); idxMenuExportBoard = menu.getSubElements().length; final JMenuItem itemExportBoard = new JMenuItem("Export The Board ...", IconLoader.getIcon("board-export")); itemExportBoard.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { exportBoard(); } }); menu.add(itemExportBoard); return menu; } protected Point getLastMenuLocation() { return lastMenuLocation; } protected void removeCurrentConnection() { if (hasCurrentPart() && p.currentConnectionsTarget != null && ensureModifyable()) { try { p.currentPart .disconnectFromPipePart(p.currentConnectionsTarget); } catch (final StateException e) { Log.error(e, "Cannot delete connection"); } resetCurrentConnection(); painter.updateSaneConfigCache(); repaint(); } } protected void removeCurrentPartsConnections() { if (hasCurrentPart() && ensureModifyable()) { final Set<PipePart> copy = new HashSet<PipePart>( p.currentPart.getConnectedPipeParts()); try { for (final PipePart pp : copy) p.currentPart.disconnectFromPipePart(pp); } catch (final StateException e) { Log.error(e, "Cannot delete connections"); } resetCurrentConnection(); painter.updateSaneConfigCache(); repaint(); } } protected void removeCurrentPart() { if (hasCurrentPart() && ensureModifyable()) { try { for (final PipePart srcPP : painter.getSet()) if (srcPP.getConnectedPipeParts().contains(p.currentPart)) srcPP.disconnectFromPipePart(p.currentPart); } catch (final StateException e) { Log.error(e, "Cannot delete connections"); } painter.getSet().remove(p.currentPart); try { if (p.currentPart instanceof Input) getPipe().removeInput((Input) p.currentPart); else if (p.currentPart instanceof Converter) getPipe().removeConverter((Converter) p.currentPart); else if (p.currentPart instanceof Output) getPipe().removeOutput((Output) p.currentPart); else throw new InternalException( "Invalid sub-class of PipePart '%s'", p.currentPart); } catch (final StateException e) { Log.error(e, "Cannot remove PipePart '%s'", p.currentPart); } resetCurrentPart(); painter.updateSaneConfigCache(); repaint(); } } protected void configureCurrentPart(final boolean onlyIfNoIcon) { if (hasCurrentPart() && !p.currentPart.getGuiConfigs().isEmpty() && (!onlyIfNoIcon || p.currentIcon == null) && ensureModifyable()) { createConfigureDialog("Configure", p.currentPart, null); painter.recalculatePipePartWidth(p.currentPart, getGraphics()); repaint(); } } protected void checkIconClicked() { if (p.currentIcon == BoardPainter.ICON_CONF) configureCurrentPart(false); if (p.currentIcon == BoardPainter.ICON_DGR) toggleDiagram(); } protected void toggleDiagram() { if (hasCurrentPart()) p.currentPart.setVisualize(!p.currentPart.isVisualize()); } protected void clearBoard() { if (ensureModifyable()) { try { getPipe().reset(); } catch (final PipeException e) { Log.error(e, "Cannot clear the board"); } painter.getSet().clear(); resetCurrentPart(); delayedReordering = false; painter.updateSaneConfigCache(); repaint(); } } protected void layoutBoard() { if (layoutThread != null) return; p.layouter = new BoardAutoLayouter(this); layoutThread = new Thread() { @Override public void run() { layoutThreadRun(); } }; updateState(); layoutThread.start(); } protected void exportBoard() { final JFileChooser fc = new JFileChooser(); fc.setAcceptAllFileFilterUsed(false); fc.addChoosableFileFilter(new FileNameExtensionFilter( "Export Part 1: Image", "png")); if (fc.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) return; File filePNG = fc.getSelectedFile(); if (!filePNG.getName().contains(".")) filePNG = new File(filePNG.getPath() + ".png"); fc.resetChoosableFileFilters(); fc.addChoosableFileFilter(new FileNameExtensionFilter( "Export Part 2: HTML", "html")); fc.addChoosableFileFilter(new FileNameExtensionFilter( "Export Part 2: Latex", "tex")); fc.addChoosableFileFilter(new FileNameExtensionFilter( "Export Part 2: Text", "txt")); fc.setSelectedFile(new File(filePNG.getPath().replace(".png", ""))); fc.setFileFilter(fc.getChoosableFileFilters()[1]); if (fc.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) return; File fileTXT = fc.getSelectedFile(); if (!fileTXT.getName().contains(".") && fc.getFileFilter() instanceof FileNameExtensionFilter) fileTXT = new File( fileTXT.getPath() + "." + ((FileNameExtensionFilter) fc.getFileFilter()) .getExtensions()[0]); exportBoardToFile(filePNG, fileTXT); } private void exportBoardToFile(final File filePNG, final File fileTXT) { try { // export the image final BoardPainter bp = new BoardPainter(); BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); bp.setPipe(pipe, img.getGraphics(), false); final Dimension pref = bp.getPreferredSize(); img = new BufferedImage(pref.width, pref.height, BufferedImage.TYPE_INT_RGB); bp.setBounds(pref.width, pref.height, false); final Graphics g = img.getGraphics(); g.setClip(0, 0, pref.width, pref.height); final PaintParameters pprm = new PaintParameters(); pprm.g = g; pprm.modifyable = true; final Color old1 = BoardConfiguration.CFG_BACKGROUND.getContent(); final Color old2 = BoardConfiguration.CFG_RECT_BACKGROUND .getContent(); try { BoardConfiguration.CFG_BACKGROUND.setContent(Color.WHITE); BoardConfiguration.CFG_RECT_BACKGROUND.setContent(new Color( 245, 245, 255)); bp.paint(pprm); } finally { BoardConfiguration.CFG_BACKGROUND.setContent(old1); BoardConfiguration.CFG_RECT_BACKGROUND.setContent(old2); } ImageIO.write(img, "png", filePNG); // export the text final BufferedWriter out = new BufferedWriter(new FileWriter( fileTXT)); if (fileTXT.getPath().endsWith(".html")) out.write(String.format("<html><h1>%s</h1><br><img src=\"%s\" " + "alt=\"Image of the Board\"><br>", pipe.getTitle(), filePNG.getName())); else if (fileTXT.getPath().endsWith(".tex")) out.write(String.format("\\subsection{%s}\n\n\\imageOwn{%s}" + "{%s}{Overview for the Pipe: %s}" + "{width=\\textwidth}\n", pipe.getTitle(), filePNG .getName().replace(".png", ""), pipe.getTitle(), pipe .getTitle())); else { out.write(pipe.getTitle()); out.write('\n'); for (int i = pipe.getTitle().length(); i > 0; --i) out.write('='); out.write(String.format("\nFor an image of the " + "board see '%s'\n\n", filePNG.getName())); } for (final PipePart pp : pipe.getInputList()) exportPipePart(out, pp, bp, fileTXT); for (final PipePart pp : pipe.getConverterList()) exportPipePart(out, pp, bp, fileTXT); for (final PipePart pp : pipe.getOutputList()) exportPipePart(out, pp, bp, fileTXT); out.close(); } catch (final IOException e) { Log.error(e, "Cannot export board"); } } private static void exportPipePart(final BufferedWriter out, final PipePart pp, final BoardPainter bp, final File file) throws IOException { if (file.getPath().endsWith(".html")) out.write(getPipePartInfoHTML(pp, bp)); else if (file.getPath().endsWith(".tex")) out.write(getPipePartInfoLatex(pp, bp)); else out.write(getPipePartInfoASCII(pp, bp)); } protected void layoutThreadRun() { try { p.layouter.start(); } finally { p.layouter = null; layoutThread = null; if (!closed) { updateState(); checkPipeOrdering(null); painter.updateSaneConfigCache(); repaint(); } } } protected void addPipePart(final Class<? extends PipePart> part, final Point location) { if (!ensureModifyable()) return; try { final PipePart pp = part.newInstance(); createConfigureDialog("Add", pp, new Runnable() { @Override public void run() { final Rectangle r = pp.getGuiPosition().createCopy(); if (location != null) r.setLocation(location); if (SNAP_TO_GRID > 0) { r.x = r.x / SNAP_TO_GRID * SNAP_TO_GRID; r.y = r.y / SNAP_TO_GRID * SNAP_TO_GRID; } getPainter().check(r, pp); pp.setGuiPosition(r); try { if (pp instanceof Input) getPipe().addInput((Input) pp); else if (pp instanceof Converter) getPipe().addConverter((Converter) pp); else if (pp instanceof Output) getPipe().addOutput((Output) pp); else throw new InternalException( "Invalid sub-class of PipePart '%s'", pp); getPainter().addToSet(pp, getGraphics(), true); checkPipeOrdering(null); } catch (final StateException e) { Log.error(e, "Cannot add new PipePart"); } getPainter().updateSaneConfigCache(); repaint(); } }); } catch (final InstantiationException e) { Log.error(e); } catch (final IllegalAccessException e) { Log.error(e); } } protected void replacePipePart(final Class<? extends PipePart> part) { if (!ensureModifyable() || p.currentPart == null) return; try { final PipePart pp = part.newInstance(); createConfigureDialog("Replace With", pp, new Runnable() { @Override public void run() { try { if (pp instanceof Input) getPipe().addInput((Input) pp); else if (pp instanceof Converter) getPipe().addConverter((Converter) pp); else if (pp instanceof Output) getPipe().addOutput((Output) pp); else throw new InternalException( "Invalid sub-class of PipePart '%s'", pp); for (final PipePart srcPP : getPainter().getSet()) if (srcPP.getConnectedPipeParts().contains( getCurrentPart())) srcPP.connectToPipePart(pp); for (final PipePart trgPP : getCurrentPart() .getConnectedPipeParts()) pp.connectToPipePart(trgPP); } catch (final StateException e) { Log.error(e, "Cannot replace PipePart"); } getPainter().addToSet(pp, getGraphics(), true); final Rectangle r = pp.getGuiPosition().createCopy(); r.setLocation(getCurrentPart().getGuiPosition().getX(), getCurrentPart().getGuiPosition().getY()); removeCurrentPart(); getPainter().check(r, pp); pp.setGuiPosition(r); getPainter().updateSaneConfigCache(); checkPipeOrdering(null); } }); } catch (final InstantiationException e) { Log.error(e); } catch (final IllegalAccessException e) { Log.error(e); } } /** * Should be invoked during a Drag&Drop operation if this section is the * source of the operation. Updates the position of the current connection, * if any, or otherwise tries to move the current remembered * {@link PipePart} to the given position. * * @param ps * current cursor position (scaled to screen) */ protected void mouseDragged(final Point ps) { if (!hasCurrentPart() || handlePoint == null) return; final Point pOrg = getOriginal(ps); if (p.currentConnection == null) { // move pipe-part final Rectangle orgPos = p.currentPart.getGuiPosition() .createCopy(); final Rectangle newPos = new Rectangle(orgPos); Rectangle clip = new Rectangle(orgPos); unionConnectionTargets(clip); unionConnectionSources(clip); newPos.setLocation(pOrg.x - handlePoint.x, pOrg.y - handlePoint.y); if (SNAP_TO_GRID > 0) { newPos.x = newPos.x / SNAP_TO_GRID * SNAP_TO_GRID; newPos.y = newPos.y / SNAP_TO_GRID * SNAP_TO_GRID; } painter.check(newPos, p.currentPart); p.currentPart.setGuiPosition(newPos); if (!checkPipeOrdering(null)) p.currentPart.setGuiPosition(orgPos); clip = clip.union(newPos); unionConnectionTargets(clip); unionConnectionSources(clip); // need to take care of labels clip.grow(GROW_LABEL_REDRAW, GROW_LABEL_REDRAW); scaleRect(clip); repaint(clip); updatePrefBounds(); } else { // move connector instead of pipe-part if (!ensureModifyable()) return; if (p.currentConnectionsTarget != null) { try { p.currentPart .disconnectFromPipePart(p.currentConnectionsTarget); } catch (final StateException e) { Log.error(e, "Cannot delete connection"); } if (painter.updateSaneConfigCache()) repaint(); p.currentConnectionsTarget = null; } final Rectangle r = p.currentPart.getGuiPosition().createCopy() .union(p.currentConnection); p.currentConnection.setLocation(pOrg.x - handlePoint.x, pOrg.y - handlePoint.y); p.currentConnection.setSize(0, 0); painter.check(p.currentConnection, null); p.currentConnectionValid = false; for (final PipePart pp : painter.getSet()) if (pp.getGuiPosition().contains( p.currentConnection.getLocation())) { p.currentConnectionValid = p.currentPart .isConnectionAllowed(pp); break; } mouseMoved(ps); Rectangle2D.union(r, p.currentConnection, r); // need to take care of labels r.grow(GROW_LABEL_REDRAW, GROW_LABEL_REDRAW); scaleRect(r); repaint(r); } } private void unionConnectionSources(final Rectangle r) { for (final PipePart srcPP : painter.getSet()) if (srcPP.getConnectedPipeParts().contains(p.currentPart)) unionConnection(r, srcPP); } private void unionConnectionTargets(final Rectangle r) { for (final PipePart trgPP : p.currentPart.getConnectedPipeParts()) unionConnection(r, trgPP); } private void unionConnection(final Rectangle r, final PipePart pp) { final Point pt = new Point(); BoardPainter.calcConnectorPositions(p.currentPart.getGuiPosition(), pp.getGuiPosition(), null, pt); Rectangle2D.union(r, new Rectangle(pt.x, pt.y, 0, 0), r); } /** * Should be called whenever the mouse is moved over this section. * * @param ps * current cursor position (scaled to screen) */ protected void mouseMoved(final Point ps) { final Point porg = getOriginal(ps); PipePart found = null; for (final PipePart pp : painter.getSet()) if (pp.getGuiPosition().contains(porg)) { found = pp; final Object res = BoardPainter.getPipePartElement(pp, porg); p.currentIcon = res instanceof Icon ? (Icon) res : null; break; } if (p.underCursor != null) repaint(scaleRect(p.underCursor.getGuiPosition().createCopy())); p.underCursor = found; if (p.underCursor != null) repaint(scaleRect(p.underCursor.getGuiPosition().createCopy())); } /** * Remembers the {@link PipePart} and the connector which is under the given * cursor position for later use in {@link #mouseDragged(Point)} and during * {@link #paintComponent(Graphics)}. * * @param ps * current cursor position (scaled to screen) */ protected void updateCurrent(final Point ps) { // invoked on click or when a drag&drop operation starts updateCurrent0(ps); repaint(); } private void updateCurrent0(final Point pscr) { resetCurrentPart(); // check all pipe-parts final Point porg = getOriginal(pscr); for (final PipePart pp : painter.getSet()) if (pp.getGuiPosition().contains(porg)) { p.currentPart = pp; final Object res = BoardPainter.getPipePartElement(pp, porg); if (res instanceof Icon) p.currentIcon = (Icon) res; else if (res instanceof Point) handlePoint = (Point) res; else { p.currentConnection = new Rectangle(porg.x, porg.y, 0, 0); handlePoint = new Point(0, 0); } return; } // check all connections for (final PipePart srcPP : painter.getSet()) for (final PipePart trgPP : srcPP.getConnectedPipeParts()) { final Point ps = new Point(); final Point pt = new Point(); BoardPainter.calcConnectorPositions(srcPP.getGuiPosition(), trgPP.getGuiPosition(), ps, pt); if (Line2D.ptSegDistSq(ps.x, ps.y, pt.x, pt.y, porg.x, porg.y) < LINE_CLICK_DIST) { p.currentPart = srcPP; p.currentConnection = new Rectangle(porg.x, porg.y, 0, 0); p.currentConnectionsTarget = trgPP; handlePoint = new Point(porg.x - pt.x, porg.y - pt.y); return; } } } /** * Forgets about the currently remembered {@link PipePart} and connection, * if any. If a connection has been remembered and is currently pointing to * a valid {@link PipePart}, the remembered {@link PipePart} is connected to * the one the connection is pointing to, even if it's outside of this * section. */ protected void releaseCurrent() { // invoked on click or when a drag&drop operation is finished if (p.currentConnection != null && p.currentConnectionsTarget == null && ensureModifyable()) { final Point pOrg = new Point(p.currentConnection.getLocation()); for (final PipePart pp : painter.getSet()) if (pp.getGuiPosition().contains(pOrg) && p.currentPart.isConnectionAllowed(pp)) { try { p.currentPart.connectToPipePart(pp); } catch (final StateException e) { Log.error(e, "Cannot create connection"); } painter.updateSaneConfigCache(); break; } } resetCurrentTransients(); repaint(); } /** * Sorts all {@link PipePart}s according to their location on the board, * from top to down, from left to right. * * @param <T> * subclass of {@link PipePart} (compile-time) * @param clazz * subclass of {@link PipePart} (run-time) * @return sorted list of {@link PipePart}s */ @SuppressWarnings("unchecked") private <T extends PipePart> List<T> getSortedParts(final Class<T> clazz) { final List<T> res = new ArrayList<T>(); for (final PipePart pp : painter.getSet()) if (clazz.isInstance(pp)) res.add((T) pp); Collections.sort(res, new Comparator<T>() { @Override public int compare(final T pp1, final T pp2) { final ImmutableRectangle r1 = pp1.getGuiPosition(); final ImmutableRectangle r2 = pp2.getGuiPosition(); int cmp = r1.getY() - r2.getY(); if (cmp == 0) cmp = r1.getX() - r2.getX(); return cmp; } }); return res; } /** * Checks whether {@link PipePart}s in the {@link Pipe} need to be reordered * to reflect their GUI positions. If reordering is needed but the board is * in read-only state (as the Pipe is running), this method will return * false and delay the reordering until the Pipe has finished running. * * @param warnIfNeeded * if not empty, a message will be printed if reordering is * needed * @return true if reordering was not needed or succeeded, false if * reordering was needed and failed. * @see #updateState() */ protected boolean checkPipeOrdering(final String warnIfNeeded) { // is reordering needed? final List<Input> orderInput = getSortedParts(Input.class); final List<Converter> orderConverter = getSortedParts(Converter.class); final List<Output> orderOutput = getSortedParts(Output.class); if (getPipe().getInputList().equals(orderInput) && getPipe().getConverterList().equals(orderConverter) && getPipe().getOutputList().equals(orderOutput)) { Log.detail("Pipe ordering has not changed"); return true; } if (warnIfNeeded != null && !warnIfNeeded.isEmpty()) Log.warn(warnIfNeeded); if (!ensureModifyable()) { Log.detail("Pipe ordering has changed, reordering will be delayed"); // delay until pipe has finished ... delayedReordering = true; // ... and stop drag&drop operation (if any) return false; } Log.detail("Pipe ordering has changed and will be reordered"); // reorder try { getPipe().reorderInputs(orderInput); getPipe().reorderConverter(orderConverter); getPipe().reorderOutputs(orderOutput); } catch (final StateException e) { Log.error(e, "Cannot reorder PipeParts"); } catch (final IllegalArgumentException e) { Log.error(e, "Cannot reorder PipeParts"); } delayedReordering = false; repaint(); return true; } protected void updatePrefBounds() { setPreferredSize(painter.getPreferredSize()); revalidate(); } protected void updateBounds(final int width, final int height) { painter.setBounds(width, height, true); checkPipeOrdering("Resizing the board changed the ordering of the Pipe !!! " + "Please check ordering of all PipeParts."); repaint(); } protected void showMenu(final Component invoker, final int x, final int y) { lastMenuLocation = getOriginal(new Point(x, y)); if (x <= painter.getBorder1(true)) showMenu(menuInput, invoker, x, y); else if (x >= painter.getBorder2(true)) showMenu(menuOutput, invoker, x, y); else showMenu(menuConverter, invoker, x, y); } protected void showMenu(final JPopupMenu menu, final Component invoker, final int x, final int y) { final MenuElement[] items = menu.getSubElements(); ((AbstractButton) items[idxMenuAdd]).setEnabled(p.modifyable && !hasCurrentPart()); ((AbstractButton) items[idxMenuRepl]).setEnabled(p.modifyable && hasCurrentPart()); ((AbstractButton) items[idxMenuConfPart]).setEnabled(p.modifyable && hasCurrentPart() && p.currentConnection == null && !p.currentPart.getGuiConfigs().isEmpty()); ((AbstractButton) items[idxMenuDelPart]).setEnabled(p.modifyable && hasCurrentPart() && p.currentConnection == null); ((AbstractButton) items[idxMenuDelPartConn]).setEnabled(p.modifyable && hasCurrentPart() && p.currentConnection == null && !p.currentPart.getConnectedPipeParts().isEmpty()); ((AbstractButton) items[idxMenuDelConn]).setEnabled(p.modifyable && p.currentConnection != null); ((AbstractButton) items[idxMenuToggleDgr]).setEnabled(hasCurrentPart()); ((AbstractButton) items[idxMenuToggleDgr]).setSelected(hasCurrentPart() && p.currentPart.isVisualize()); ((AbstractButton) items[idxMenuClearBoard]).setEnabled(p.modifyable && !hasCurrentPart()); ((AbstractButton) items[idxMenuLayoutBoard]) .setEnabled(!hasCurrentPart() && layoutThread == null); ((AbstractButton) items[idxMenuExportBoard]) .setEnabled(!hasCurrentPart()); menu.show(invoker, x, y); } protected void createConfigureDialog(final String prefix, final PipePart pp, final Runnable runIfOK) { // no need to configure if no values assigned if (pp.getGroup().isEmpty()) { if (runIfOK != null) runIfOK.run(); return; } final JDialog dlg = new JDialog(); dlg.setTitle(String.format("%s %s", prefix, pp.getName())); final JPanel cfgItemPanel = new JPanel(); final Layouter lay = new Layouter(cfgItemPanel); boolean hasGreedy = false; int idx = 0; for (final ConfigValue v : pp.getGuiConfigs()) { // each config-value gets its own JPanel so they don't // interfere with each other. // LBL1 SUB1 // LBL2 SUB2 // LBL3 SUB3 // BUTTONS final JPanel sub = new JPanel(); final int cmpCnt = sub.getComponentCount(); final boolean greedy = v.insertGUIComponents(new Layouter(sub)); final String compLabel = v.getLabel() + ":"; final JLabel lbl = new JLabel(compLabel, SwingConstants.RIGHT); lbl.setVerticalAlignment(SwingConstants.TOP); lay.add(lbl, false); lay.addWholeLine(sub, greedy); hasGreedy |= greedy; for (int i = cmpCnt; i < sub.getComponentCount(); ++i) if (sub.getComponent(i) instanceof JComponent) ((JComponent) sub.getComponent(i)).setToolTipText(pp .getConfigHelp(idx)); ++idx; } if (!hasGreedy) lay.addVerticalSpacer(); final JPanel buttons = new JPanel(); final Layouter lb = new Layouter(buttons); dlg.setLayout(new BorderLayout()); dlg.add(cfgItemPanel, BorderLayout.CENTER); dlg.add(buttons, BorderLayout.SOUTH); final Icon cfgImage = pp.getConfigImage(); if (cfgImage instanceof ImageIcon) { final JLabel lbl = new JLabel( createTransparentImage((ImageIcon) cfgImage), SwingConstants.RIGHT); lbl.setVerticalAlignment(SwingConstants.TOP); dlg.add(lbl, BorderLayout.WEST); } final String helpFile = pp.getHelpFile(); final JButton btnHelp = lb.addButton(Button.Help, Layouter.help(dlg, helpFile)); btnHelp.setEnabled(HelpLoader.isHelpAvailable(helpFile)); lb.addSpacer(); dlg.getRootPane().setDefaultButton( lb.addButton(Button.Ok, new Runnable() { @Override public void run() { if (saveConfigChanges(dlg, pp, true)) { dlg.dispose(); if (runIfOK != null) runIfOK.run(); } } })); lb.addButton(Button.Apply, new Runnable() { @Override public void run() { saveConfigChanges(dlg, pp, false); } }); lb.addButton(Button.Cancel, new Runnable() { @Override public void run() { dlg.dispose(); } }); dlg.pack(); dlg.setLocationRelativeTo(null); dlg.setModal(true); HelpDialog.closeHelpIfOpen(); dlg.setVisible(true); HelpDialog.closeHelpIfOpen(); } private Icon createTransparentImage(final ImageIcon image) { final ImageFilter filter = new RGBImageFilter() { @Override public int filterRGB(final int x, final int y, final int rgb) { // replace opaque white with transparent return rgb == 0xFFFFFFFF ? 0 : rgb; } }; return new ImageIcon(Toolkit.getDefaultToolkit().createImage( new FilteredImageSource(image.getImage().getSource(), filter))); } protected boolean saveConfigChanges(final Component dlg, final PipePart pp, final boolean continueable) { if (!ensureModifyable()) return false; for (final ConfigValue v : pp.getGuiConfigs()) v.setFromGUIComponents(); pp.configValuesChanged(); if (painter.updateSaneConfigCache()) repaint(); final String cfgRes = pp.isCachedConfigSane(); if (cfgRes != null && !cfgRes.isEmpty()) { Log.warn("Configuration is invalid: %s", cfgRes); if (JOptionPane.showOptionDialog(dlg, String.format( "Configuration is invalid: %s%s", cfgRes, continueable ? "\nIgnore and continue?" : ""), null, continueable ? JOptionPane.YES_NO_OPTION : JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE, null, null, null) != JOptionPane.YES_OPTION) return false; } try { pp.configure(); } catch (final PipeException e) { Log.error(e); return false; } return true; } @Override public String getToolTipText(final MouseEvent event) { if (p.underCursor == null) return null; return getPipePartInfoHTML(p.underCursor, painter); } public void updateState() { final boolean modifNow = !MainFrame.the().isPipeRunning() && layoutThread == null && !closed; if (p.modifyable ^ modifNow) { p.modifyable = modifNow; if (delayedReordering) checkPipeOrdering(null); repaint(); } } private boolean ensureModifyable() { if (!p.modifyable) Log.error("Configuration board is read-only as " + "the Pipe or the Auto-Layouter is currently " + "running."); return p.modifyable; } public void setZoom(final double zoom) { if (zoom > 0) painter.setScale(1 + zoom * 9); else painter.setScale(zoom < 0 ? 1 / (1 + -zoom * 9) : 1); updatePrefBounds(); repaint(); MainFrame.the().getMainPipePanel().getPipeFlowVisualization() .modified(); } private Point getOriginal(final Point scaled) { final double scale = painter.getScale(); return new Point((int) (scaled.x / scale), (int) (scaled.y / scale)); } private Rectangle scaleRect(final Rectangle r) { final double scale = painter.getScale(); r.x *= scale; r.y *= scale; r.width *= scale; r.height *= scale; return r; } public void closed() { closed = true; updateState(); if (layoutThread != null && p.layouter != null) { p.layouter.interrupt(); layoutThread.interrupt(); } } public boolean hasCurrentPart() { return p.currentPart != null; } public void resetCurrentTransients() { handlePoint = null; p.currentConnection = null; p.currentConnectionValid = false; p.currentIcon = null; } public void resetCurrentPart() { p.currentPart = null; p.currentIcon = null; handlePoint = null; resetCurrentConnection(); } public void resetCurrentConnection() { p.currentConnection = null; p.currentConnectionsTarget = null; p.currentConnectionValid = false; } public BoardPainter getPainter() { return painter; } public PipePart getCurrentPart() { return p.currentPart; } void setPipeflow(final Collection<PipeFlow> pipeflow) { p.pipeflow = pipeflow; } private static String getPipePartInfoASCII(final PipePart pp, final BoardPainter bp) { final StringBuilder sb = new StringBuilder(""); sb.append(pp.getName()); sb.append("\n\t"); sb.append(pp.getDescription()); sb.append("\n"); for (final ConfigValue v : pp.getGuiConfigs()) { sb.append("\t"); sb.append(v.getLabel()); sb.append("\t"); sb.append(v.asString().replace("\n", "\n\t\t")); sb.append("\n"); } sb.append("\n"); final String sc = bp.getSaneConfigCache().get(pp); if (sc != null) { sb.append("\tBad configuration:\n\t"); sb.append(sc.replace("\n", "\n\t")); } return sb.toString(); } private static String getPipePartInfoHTML(final PipePart pp, final BoardPainter bp) { final StringBuilder sb = new StringBuilder("<html><b>"); sb.append(StringManip.safeHTML(pp.getName())); sb.append("</b><p>"); sb.append(StringManip.safeHTML(pp.getDescription())); sb.append("<table border=1>"); for (final ConfigValue v : pp.getGuiConfigs()) { sb.append("<tr><td align=right>"); sb.append(StringManip.safeHTML(v.getLabel())); sb.append("</td><td align=left>"); sb.append(StringManip.safeHTML(v.asString())); sb.append("</td></tr>"); } sb.append("</table>"); final String sc = bp.getSaneConfigCache().get(pp); if (sc != null) { sb.append("<p style=\"color:red\"><b>Bad configuration:</b><br>"); sb.append(StringManip.safeHTML(sc)); sb.append("</p>"); } sb.append("<p style=\"color:blue\"><b>Statistics:</b><br>"); sb.append(pp.getFeedback().getHTMLTable()); sb.append("</p>"); sb.append("</html>"); return sb.toString(); } private static String getPipePartInfoLatex(final PipePart pp, final BoardPainter bp) { final StringBuilder sb = new StringBuilder(""); String icoName = PipePartDetection.callHelp(pp.getClass(), HelpKind.Icon); if (icoName == null) icoName = pp.getClass().getSimpleName() + "-icon.png"; sb.append(String.format("\n\\subsubsection*{\\protect\\mbox{\\protect" + "\\includegraphics[width=5mm]{%s}} %s}\n\n", icoName.replace(".png", ""), StringManip.safeTex(pp.getName()))); sb.append(StringManip.safeTex(pp.getDescription())); sb.append("\n\n"); sb.append("\\begin{longtable}{p{0.25\\textwidth} | " + "p{0.75\\textwidth}}\n"); // sb.append("\\hline\n"); for (final ConfigValue v : pp.getGuiConfigs()) { sb.append(StringManip.safeTex(v.getLabel())); sb.append(" & "); sb.append(StringManip.safeTex(v.asString()).replace("\n", "\n & ")); sb.append(" \\\\\n"); // sb.append(" \\\\ \\hline\n"); } sb.append("\\end{longtable}\n"); final String sc = bp.getSaneConfigCache().get(pp); if (sc != null) { sb.append("\\textcolor{red}{Bad configuration:\\\\\n"); sb.append(StringManip.safeTex(sc)); sb.append("\n}\n"); } return sb.toString(); } }