package ini.trakem2.display.d3d; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Enumeration; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.table.AbstractTableModel; import javax.swing.table.TableColumn; import org.scijava.java3d.BranchGroup; import org.scijava.java3d.Canvas3D; import org.scijava.java3d.View; import org.scijava.vecmath.Color3f; import ij3d.Content; import ij3d.Image3DUniverse; import ij3d.ImageWindow3D; import ij3d.UniverseListener; import ini.trakem2.display.Display; import ini.trakem2.display.Display3D; import ini.trakem2.display.Displayable; import ini.trakem2.display.LayerSet; import ini.trakem2.persistence.DBObject; import ini.trakem2.utils.IntegerField; import ini.trakem2.utils.Utils; public class Display3DGUI { private final Image3DUniverse univ; public Display3DGUI(final Image3DUniverse univ) { this.univ = univ; } public Image3DUniverse getUniverse() { return univ; } public ImageWindow3D init() { // Extract the Canvas3D from the ImageWindow3D final ImageWindow3D frame = new ImageWindow3D("TrakEM2 3D Display", this.univ); frame.getContentPane().removeAll(); // New layout final JPanel all = new JPanel(); all.setBackground(Color.white); all.setPreferredSize(new Dimension(768, 512)); final GridBagConstraints c = new GridBagConstraints(); final GridBagLayout gb = new GridBagLayout(); all.setLayout(gb); // Add Canvas3D final Canvas3D canvas = this.univ.getCanvas(); c.anchor = GridBagConstraints.NORTHWEST; c.fill = GridBagConstraints.BOTH; c.weightx = 1; c.weighty = 1; c.gridheight = 4; gb.setConstraints(canvas, c); all.add(canvas); // 1. Panel to edit color and assign random colors c.fill = GridBagConstraints.HORIZONTAL; c.weightx = 0; c.weighty = 0; c.gridheight = 1; c.gridx = 1; final JPanel p1 = newPanelColors(this.univ); gb.setConstraints(p1, c); all.add(p1); // 2. Panel to delete all whose name matches a regex c.gridy = 1; c.insets = new Insets(10, 0, 0, 0); final JPanel p2 = newPanelRemoveContents(this.univ); gb.setConstraints(p2, c); all.add(p2); // 3. Filterable selection list c.gridy = 2; c.weighty = 1; c.fill = GridBagConstraints.BOTH; final JPanel p3 = newPanelFilterableTable(this.univ); gb.setConstraints(p3, c); all.add(p3); frame.getContentPane().add(all); return frame; } /** The {@link Image3DUniverse#getContents()} returns an unordered list, because the * list are the values of a {@link Map}; this method circumvents that by getting * the list from the enumeration of children elements in the scene of the {@code univ}, * which is a {@link BranchGroup}. * * @param univ * @return */ static private final List<Content> getOrderedContents(final Image3DUniverse univ) { final ArrayList<Content> cs = new ArrayList<Content>(); final Enumeration<?> seq = univ.getScene().getAllChildren(); while (seq.hasMoreElements()) { final Object o = seq.nextElement(); if (o instanceof Content) cs.add((Content)o); } return cs; } static private final void addTitledLineBorder(final JPanel p, final String title) { p.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black, 1), title)); } static private final class SliderTyperLink extends KeyAdapter { private final Image3DUniverse univ; private final JScrollBar slider; private final JTextField typer; SliderTyperLink(final Image3DUniverse univ, final JScrollBar slider, final JTextField typer) { this.slider = slider; this.typer = typer; this.univ = univ; } @Override public void keyPressed(final KeyEvent ke) { final String txt = typer.getText(); if (txt.length() > 0) { final int val = Integer.parseInt(txt); slider.setValue(val); final Content content = univ.getSelected(); if (null != content) { slider.setValue(val); // will also set the color } } } } static private final JPanel newPanelColors(final Image3DUniverse univ) { final JPanel p = new JPanel(); p.setBackground(Color.white); final GridBagLayout gb = new GridBagLayout(); final GridBagConstraints c = new GridBagConstraints(); c.anchor = GridBagConstraints.NORTHWEST; c.fill = GridBagConstraints.HORIZONTAL; p.setLayout(gb); final String[] labels = new String[]{"Red", "Green", "Blue"}; final JScrollBar[] sliders = new JScrollBar[3]; final JTextField[] typers = new JTextField[3]; for (int i=0; i<3; ++i) { final JScrollBar slider = new JScrollBar(JScrollBar.HORIZONTAL, 255, 1, 0, 256); sliders[i] = slider; final JTextField typer = new IntegerField(255, 3); typers[i] = typer; final int k = i; slider.addAdjustmentListener(new AdjustmentListener() { @Override public void adjustmentValueChanged(final AdjustmentEvent e) { final Content content = univ.getSelected(); if (null == content) { Utils.log("Nothing selected!"); return; } Color3f color = content.getColor(); if (null == color) color = new Color3f(1, 1, 0); // default to yellow final float[] co = new float[3]; color.get(co); co[k] = e.getValue() / 255.0f; content.setColor(new Color3f(co)); typer.setText(Integer.toString(e.getValue())); } }); typer.addKeyListener(new SliderTyperLink(univ, slider, typer)); final JLabel l = new JLabel(labels[i]); // Layout c.gridx = 0; c.gridy = i; gb.setConstraints(l, c); p.add(l); c.gridx = 1; c.weightx = 1; c.fill = GridBagConstraints.HORIZONTAL; gb.setConstraints(slider, c); p.add(slider); c.gridx = 2; c.weightx = 0; c.fill = GridBagConstraints.NONE; gb.setConstraints(typer, c); p.add(typer); } // Alpha slider c.gridx = 0; c.gridy += 1; final JLabel aL = new JLabel("Alpha:"); gb.setConstraints(aL, c); p.add(aL); c.gridx = 1; c.fill = GridBagConstraints.HORIZONTAL; c.weightx = 1; final JScrollBar alphaSlider = new JScrollBar(JScrollBar.HORIZONTAL, 255, 1, 0, 256); gb.setConstraints(alphaSlider, c); p.add(alphaSlider); c.gridx = 2; c.fill = GridBagConstraints.NONE; c.weightx = 0; final JTextField alphaTyper = new IntegerField(255, 3); gb.setConstraints(alphaTyper, c); p.add(alphaTyper); alphaSlider.addAdjustmentListener(new AdjustmentListener() { @Override public void adjustmentValueChanged(final AdjustmentEvent e) { final Content content = univ.getSelected(); if (null == content) { Utils.log("Nothing selected!"); return; } final float alpha = e.getValue() / 255.0f; content.setTransparency(1 - alpha); alphaTyper.setText(Integer.toString(e.getValue())); } }); alphaTyper.addKeyListener(new SliderTyperLink(univ, alphaSlider, alphaTyper)); // Button to colorize randomly c.gridx = 0; c.gridy += 1; c.gridwidth = 3; c.weightx = 1; c.insets = new Insets(15, 4, 4, 4); final JButton r = new JButton("Assign random colors to all"); r.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { randomizeColors(univ); } }); gb.setConstraints(r, c); p.add(r); addTitledLineBorder(p, "Colors"); univ.addUniverseListener(new UniverseListener() { @Override public void universeClosed() {} @Override public void transformationUpdated(final View arg0) {} @Override public void transformationStarted(final View arg0) {} @Override public void transformationFinished(final View arg0) {} @Override public void contentSelected(final Content arg0) { if (null == arg0) { return; } Color3f color = arg0.getColor(); if (null == color) color = new Color3f(1, 1, 0); // default to yellow final float[] co = new float[3]; color.get(co); for (int i=0; i<3; ++i) { // Disallow the slider from firing an event when its value is adjusted sliders[i].setValueIsAdjusting(true); final int val = (int)(co[i] * 255); typers[i].setText(Integer.toString(val)); sliders[i].setValue(val); } // After all are set, re-enable, which triggers events (the color will be set three times...) for (int i=0; i<3; ++i) { sliders[i].setValueIsAdjusting(false); } // Alpha slider: alphaSlider.setValueIsAdjusting(true); final int alpha = (int)((1 - arg0.getTransparency()) * 255); alphaTyper.setText(Integer.toString(alpha)); alphaSlider.setValue(alpha); alphaSlider.setValueIsAdjusting(false); } @Override public void contentRemoved(final Content arg0) {} @Override public void contentChanged(final Content arg0) { if (arg0 == univ.getSelected()) { contentSelected(arg0); } } @Override public void contentAdded(final Content arg0) {} @Override public void canvasResized() {} }); return p; } static private final float[][] colors = new float[][]{ new float[]{255, 255, 0}, // yellow new float[]{255, 0, 0}, // red new float[]{255, 0, 255}, // magenta new float[]{0, 255, 0}, // blue new float[]{0, 255, 255}, // cyan new float[]{0, 255, 0}, // green new float[]{255, 255, 255},// white new float[]{255, 128, 0}, // orange new float[]{255, 0, 128}, new float[]{128, 255, 0}, new float[]{128, 0, 255}, new float[]{0, 255, 128}, new float[]{0, 128, 255}, new float[]{128, 128, 128},// grey }; static public final void randomizeColors(final Image3DUniverse univ) { final ArrayList<Content> cs = new ArrayList<Content>(getOrderedContents(univ)); for (int i=0; i<cs.size(); ++i) { if (i < colors.length) { cs.get(i).setColor(new Color3f(colors[i])); } else { cs.get(i).setColor(new Color3f((float)Math.random(), (float)Math.random(), (float)Math.random())); } } // Update the color bars if something is selected: final Content content = univ.getSelected(); if (null != content) univ.fireContentChanged(content); } static private final JPanel newPanelRemoveContents(final Image3DUniverse univ) { final JPanel p = new JPanel(); p.setBackground(Color.white); final GridBagLayout gb = new GridBagLayout(); final GridBagConstraints c = new GridBagConstraints(); c.anchor = GridBagConstraints.SOUTHWEST; c.fill = GridBagConstraints.HORIZONTAL; p.setLayout(gb); final JLabel label = new JLabel("RegEx:"); final JTextField regex = new JTextField(); final JButton remove = new JButton("X"); final ActionListener a = new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { String s = regex.getText(); if (0 == s.length()) return; if (!s.startsWith("^")) s = "^.*" + s; if (!s.endsWith("$")) s = s + ".*$"; Pattern pattern = null; try { pattern = Pattern.compile(s); } catch (final PatternSyntaxException pse) { JOptionPane.showMessageDialog(univ.getWindow(), "Error parsing the regular expression:\n" + pse.getMessage()); return; } for (final Content c : new ArrayList<Content>(getOrderedContents(univ))) { if (pattern.matcher(c.getName()).matches()) { univ.removeContent(c.getName()); Utils.log("Removed " + c.getName()); } } } }; remove.addActionListener(a); regex.addActionListener(a); gb.setConstraints(label, c); p.add(label); c.gridx = 1; c.weightx = 1; c.fill = GridBagConstraints.BOTH; gb.setConstraints(regex, c); p.add(regex); c.gridx = 2; c.weightx = 0; c.fill = GridBagConstraints.NONE; gb.setConstraints(remove, c); p.add(remove); addTitledLineBorder(p, "Remove content"); return p; } static private final class ContentTableModel extends AbstractTableModel { private List<Content> contents; private final Image3DUniverse univ; private final JTextField regexField; public ContentTableModel(final Image3DUniverse univ, final JTextField regexField) { this.univ = univ; this.regexField = regexField; this.contents = new ArrayList<Content>(getOrderedContents(univ)); } @Override public int getRowCount() { return contents.size(); } @Override public int getColumnCount() { return 2; } @Override public String getColumnName(final int columnIndex) { switch(columnIndex) { case 0: return "nth"; case 1: return "Name"; } return null; } @Override public boolean isCellEditable(final int rowIndex, final int columnIndex) { return false; } @Override public Object getValueAt(final int rowIndex, final int columnIndex) { switch (columnIndex) { case 0: return rowIndex + 1; case 1: return contents.get(rowIndex).getName(); } return null; } @Override public void setValueAt(final Object aValue, final int rowIndex, final int columnIndex) {} private void sortByName() { final TreeMap<String, Content> m = new TreeMap<String, Content>(); for (final Content c : contents) { m.put(c.getName(), c); } // Swap this.contents = new ArrayList<Content>(m.values()); fireTableDataChanged(); } private void update(final ContentTable table) { Utils.invokeLater(new Runnable() { @Override public void run() { final ArrayList<Content> cs = new ArrayList<Content>(); try { final RegExFilter f = new RegExFilter(regexField.getText()); for (final Object ob : getOrderedContents(univ)) { final Content c = (Content)ob; if (f.accept(c.getName())) { cs.add(c); } } } catch (final PatternSyntaxException pse) { JOptionPane.showMessageDialog(univ.getWindow(), "Error parsing the regular expression:\n" + pse.getMessage()); cs.addAll(getOrderedContents(univ)); } ContentTableModel.this.contents = cs; fireTableDataChanged(); // Adjust cell width: final TableColumn tc = table.getColumnModel().getColumn(0); final Component label = table.getDefaultRenderer(getColumnClass(0)) .getTableCellRendererComponent(table, cs.size()-1, false, false, cs.size()-1, 0); tc.setMaxWidth(label.getBounds().width + 10); // 10 pixels of padding }}); } } static private class ContentTable extends JTable { private static final long serialVersionUID = 1L; ContentTable(final Image3DUniverse univ) { super(); getTableHeader().addMouseListener(new MouseAdapter() { @Override public void mouseClicked(final MouseEvent me) { if (2 != me.getClickCount()) return; final int viewColumn = getColumnModel().getColumnIndexAtX(me.getX()); final int column = convertColumnIndexToModel(viewColumn); if (-1 == column) return; if (1 == column) { ((ContentTableModel)getModel()).sortByName(); } } }); addMouseListener(new MouseAdapter() { @Override public void mousePressed(final MouseEvent me) { final int row = ContentTable.this.rowAtPoint(me.getPoint()); if (2 == me.getClickCount()) { univ.select(((ContentTableModel)getModel()).contents.get(row)); } else if (Utils.isPopupTrigger(me)) { final List<Content> data = ((ContentTableModel)getModel()).contents; final ArrayList<Content> cs = new ArrayList<Content>(); for (final int i : getSelectedRows()) { cs.add(data.get(i)); } final JPopupMenu jp = new JPopupMenu(); JMenuItem item = new JMenuItem("Select in 3D view"); jp.add(item); item.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { univ.select(((ContentTableModel)getModel()).contents.get(row)); } }); item = new JMenuItem("Select in TrakEM2"); jp.add(item); item.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { Utils.log("Selecting in TrakEM2:"); final Hashtable<LayerSet,Display3D> ht = Display3D.getMasterTable(); LayerSet ls = null; for (final Map.Entry<LayerSet,Display3D> entry : ht.entrySet()) { if (entry.getValue().getUniverse() == univ) { ls = entry.getKey(); break; } } if (null == ls) { Utils.log("Could not find an appropriate TrakEM2 project!"); return; } Display front = Display.getFront(); if (front.getLayerSet() != ls) { for (final Display display : Display.getDisplays()) { if (display.getLayerSet() == ls) { front = display; break; } } if (front.getLayerSet() != ls) { Utils.log("Could not find an open display for the appropriate TrakEM2 project!"); return; } } for (final Content c : cs) { final String name = c.getName(); int start = name.lastIndexOf('#'); if (-1 == start) { Utils.log("..skipped " + name); continue; } final StringBuilder sb = new StringBuilder(10); start += 1; char ch; while (start < name.length() && Character.isDigit(ch = name.charAt(start))) { sb.append(ch); start += 1; } if (sb.length() > 0) { final long id = Long.parseLong(sb.toString()); final DBObject dbo = ls.findById(id); if (null == dbo || !(dbo instanceof Displayable)) { Utils.log("Could not find an object with id #" + id); continue; } front.getSelection().add((Displayable)dbo); Utils.log("Selected: #" + id); } else { Utils.log("..skipped " + name); continue; } } } }); item = new JMenuItem("Remove from 3D view"); jp.add(item); item.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { for (final Content c : cs) { univ.removeContent(c.getName()); } } }); jp.show(ContentTable.this, me.getX(), me.getY()); } } }); } @Override public String getToolTipText(final MouseEvent me) { return ((ContentTableModel)getModel()).contents .get(ContentTable.this.rowAtPoint(me.getPoint())).getName(); } } static private final class TableUniverseListener implements UniverseListener { private final ContentTable table; TableUniverseListener(final ContentTable table) { this.table = table; } @Override public void universeClosed() {} @Override public void transformationUpdated(final View arg0) {} @Override public void transformationStarted(final View arg0) {} @Override public void transformationFinished(final View arg0) {} @Override public void contentSelected(final Content c) { Utils.invokeLater(new Runnable() { @Override public void run() { final int i = ((ContentTableModel)table.getModel()).contents.indexOf(c); table.getSelectionModel().setSelectionInterval(i, i); }}); } @Override public void contentRemoved(final Content arg0) { ((ContentTableModel)table.getModel()).update(table); } @Override public void contentChanged(final Content arg0) { ((ContentTableModel)table.getModel()).update(table); } @Override public void contentAdded(final Content arg0) { ((ContentTableModel)table.getModel()).update(table); } @Override public void canvasResized() {} } static private final class RegExFilter { final Pattern pattern; RegExFilter(String regex) { if (0 == regex.length()) { this.pattern = null; return; } if (!regex.startsWith("^")) regex = "^.*" + regex; if (!regex.endsWith("$")) regex = regex + ".*$"; this.pattern = Pattern.compile(regex); } final boolean accept(final String s) { if (null == pattern) return true; return pattern.matcher(s).matches(); } } static private final JPanel newPanelFilterableTable(final Image3DUniverse univ) { final JPanel p = new JPanel(); p.setBackground(Color.white); final GridBagConstraints c = new GridBagConstraints(); final GridBagLayout gb = new GridBagLayout(); p.setLayout(gb); c.anchor = GridBagConstraints.NORTHWEST; c.fill = GridBagConstraints.HORIZONTAL; final JLabel label = new JLabel("RegEx: "); final JTextField regexField = new JTextField(); final ContentTable table = new ContentTable(univ); final ContentTableModel ctm = new ContentTableModel(univ, regexField); table.setModel(ctm); univ.addUniverseListener(new TableUniverseListener(table)); regexField.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { ctm.update(table); } }); gb.setConstraints(label, c); p.add(label); c.gridx = 1; c.weightx = 1; gb.setConstraints(regexField, c); p.add(regexField); c.gridx = 0; c.gridy = 1; c.gridwidth = 2; c.weighty = 1; c.fill = GridBagConstraints.BOTH; final JScrollPane jsp = new JScrollPane(table); gb.setConstraints(jsp, c); p.add(jsp); addTitledLineBorder(p, "Contents"); return p; } }