package org.knime.knip.cellviewer; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; import javax.swing.JToggleButton; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import javax.swing.SwingWorker; import javax.swing.border.BevelBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import org.knime.core.data.DataValue; import org.knime.core.node.BufferedDataTableHolder; import org.knime.core.node.KNIMEConstants; import org.knime.core.node.NodeLogger; import org.knime.core.node.NodeModel; import org.knime.core.node.NodeView; import org.knime.core.node.tableview.TableContentModel; import org.knime.core.node.tableview.TableContentView; import org.knime.core.node.tableview.TableView; import org.knime.knip.cellviewer.interfaces.CellView; import org.knime.knip.cellviewer.interfaces.CellViewFactory; import org.knime.knip.cellviewer.panels.CellViewerHelpDialog; import org.knime.knip.cellviewer.panels.NavigationPanel; /* * ------------------------------------------------------------------------ * * Copyright (C) 2003, 2016 * University of Konstanz, Germany and * KNIME GmbH, Konstanz, Germany * Website: http://www.knime.org; Email: contact@knime.org * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, Version 3, as * published by the Free Software Foundation. * * 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, see <http://www.gnu.org/licenses>. * * Additional permission under GNU GPL version 3 section 7: * * KNIME interoperates with ECLIPSE solely via ECLIPSE's plug-in APIs. * Hence, KNIME and ECLIPSE are both independent programs and are not * derived from each other. Should, however, the interpretation of the * GNU GPL Version 3 ("License") under any applicable laws result in * KNIME and ECLIPSE being a combined program, KNIME GMBH herewith grants * you the additional permission to use and propagate KNIME together with * ECLIPSE with only the license terms in place for ECLIPSE applying to * ECLIPSE and the GNU GPL Version 3 applying for KNIME, provided the * license terms of ECLIPSE themselves allow for the respective use and * propagation of ECLIPSE together with KNIME. * * Additional permission relating to nodes for KNIME that extend the Node * Extension (and in particular that are based on subclasses of NodeModel, * NodeDialog, and NodeView) and that only interoperate with KNIME through * standard APIs ("Nodes"): * Nodes are deemed to be separate and independent programs and to not be * covered works. Notwithstanding anything to the contrary in the * License, the License does not apply to Nodes, you are not required to * license Nodes under the License, and you are granted a license to * prepare and propagate Nodes, in each case even if such Nodes are * propagated with or for interoperation with KNIME. The owner of a Node * may freely choose the license terms applicable to such Node, including * when such Node is propagated with or for interoperation with KNIME. * ------------------------------------------------------------------------ */ /** * * @author <a href="mailto:dietzc85@googlemail.com">Christian Dietz</a> * @author <a href="mailto:horn_martin@gmx.de">Martin Horn</a> * @author <a href="mailto:andreas.burger@uni-konstanz.de">Andreas Burger</a> * @author <a href="mailto:jonathan.hale@uni.kn">Jonathan Hale</a> * * @param <T> * {@link NodeModel} subclass this {@link CellNodeView} belongs to. */ public class CellNodeView<T extends NodeModel & BufferedDataTableHolder> extends NodeView<T> { private static final NodeLogger LOGGER = NodeLogger.getLogger(CellNodeView.class); protected Map<String, CellView> m_cellViewCache; protected final int m_portIdx; protected JTabbedPane m_viewsPane; List<DataValue> m_cellValues; protected JPanel m_currentView; protected CellView m_currentProvider; protected JPanel m_contentPanel; protected JSplitPane m_splitPane; protected int m_col = -1; protected int m_row = -1; protected TableContentView m_tableContentView; protected TableContentModel m_tableModel; protected TableView m_tableView; protected boolean m_hiliteAdded = false; protected boolean m_tableExpanded = false; private JLabel m_statusLabel; protected static final String m_defStatusBarText = "Click on a cell or drag and select multiple cells to continue ..."; private NavigationPanel m_navPanel; private boolean m_updatingTabs; private int[] m_prevRows; private int[] m_prevCols; public CellNodeView(final T nodeModel) { this(nodeModel, 0); } /** * {@inheritDoc} */ public CellNodeView(final T nodeModel, final int portIdx) { super(nodeModel); m_portIdx = portIdx; final JLabel load = new JLabel("Loading port content ..."); load.setPreferredSize(new Dimension(1024, 1024)); setComponent(load); } /** * Helper function to check if a call to getValueAt(row, col) would return * an IOOBEx **/ private boolean cellExists(final int row, final int col) { if (m_tableModel == null) { return false; } else { return (col >= 0 && col < m_tableModel.getColumnCount() && row >= 0 && row < m_tableModel.getRowCount()); } } /** * Use this method to change the selection to a single Cell * * @param row * The row-index of the cell * @param col * The column-index of the cell */ private void cellSelectionChanged(final int row, final int col) { if (cellExists(row, col)) { int[] cols = new int[] { col }; int[] rows = new int[] { row }; rowColIntervalSelectionChanged(rows, cols); } } /** * This method is called whenever the selection changes. * * @param rowIndices * An array holding the row-indices of the selected cells * @param colIndices * An array holding the column-indices of the selected cells */ private void rowColIntervalSelectionChanged(final int[] rowIndices, final int[] colIndices) { if (rowIndices.length == 0 || colIndices.length == 0 || (Arrays.equals(rowIndices, m_prevRows) && Arrays.equals(colIndices, m_prevCols))) return; m_prevRows = rowIndices; m_prevCols = colIndices; // Extract classes and count of selected Cells List<Class<? extends DataValue>> preferredClasses; preferredClasses = generateClassList(rowIndices, colIndices); m_cellValues = generateValuesList(rowIndices, colIndices); // Get providing factories of registered compatible views List<CellViewFactory> compatibleFactories; compatibleFactories = CellViewsManager.getInstance().getCompatibleFactories(preferredClasses); // Update the navigation panel if (rowIndices.length == 1 && colIndices.length == 1) { m_col = colIndices[0]; m_row = rowIndices[0]; m_navPanel.updatePosition(m_tableModel.getColumnCount(), m_tableModel.getRowCount(), m_col + 1, m_row + 1, m_tableContentView.getColumnName(m_col), m_tableModel.getRowKey(m_row).toString()); } else { m_navPanel.disableButtons(); } if (compatibleFactories.isEmpty()) return; // Check for cached instances of the compatible views. In case of // miss, // instantiate and cache for (CellViewFactory fac : compatibleFactories) { if (!m_cellViewCache.containsKey(fac.getCellViewName())) { m_cellViewCache.put(fac.getCellViewName(), fac.createCellView()); } } // From here on, compatible views are guaranteed to be cached String selected = ""; if (m_viewsPane.getTabCount() > 0) { int index = m_viewsPane.getSelectedIndex(); if (index != -1) selected = m_viewsPane.getTitleAt(index); m_viewsPane.removeAll(); } m_updatingTabs = true; // Add all compatible views to the tabbed pane for (CellViewFactory fac : compatibleFactories) { try { CellView p = m_cellViewCache.get(fac.getCellViewName()); m_viewsPane.insertTab(fac.getCellViewName(), null, p.getViewComponent(), "", m_viewsPane.getTabCount()); } catch (Exception e) { LOGGER.error("Error while adding tab " + fac.getCellViewName()); } } // Keep the current viewer selected if possible. if (!selected.isEmpty()) { int index = m_viewsPane.indexOfTab(selected); if (index != -1) m_viewsPane.setSelectedIndex(index); else m_viewsPane.setSelectedIndex(0); } else { m_viewsPane.setSelectedIndex(0); } m_updatingTabs = false; // Update the viewers contents CellView p = m_cellViewCache.get(m_viewsPane.getTitleAt(m_viewsPane.getSelectedIndex())); p.updateComponent(m_cellValues); // Ensure the content is shown, not the table overview if (!getComponent().equals(m_contentPanel)) setComponent(m_contentPanel); } /** * Generates a list of preferred classes by checking the given * cell-coordinates. */ private List<Class<? extends DataValue>> generateClassList(final int[] rowIndices, final int[] colIndices) { List<Class<? extends DataValue>> result = new LinkedList<Class<? extends DataValue>>(); for (int i : rowIndices) { for (int j : colIndices) { if (cellExists(i, j)) { result.add( m_tableContentView.getContentModel().getValueAt(i, j).getType().getPreferredValueClass()); } } } return result; } /** * Fetches a list of values from the table using the provided indices. */ private List<DataValue> generateValuesList(final int[] rowIndices, final int[] colIndices) { List<DataValue> result = new LinkedList<DataValue>(); for (int i : rowIndices) { for (int j : colIndices) { if (cellExists(i, j)) { result.add(m_tableContentView.getContentModel().getValueAt(i, j)); } } } return result; } protected void initViewComponents() { // Initialize tableView initializeView(); m_contentPanel = createPanel(); // Load data into view. Disable a waiting indicator while doing so. final JPanel loadpanel = new JPanel(new BorderLayout()); loadpanel.setPreferredSize(new Dimension(1024, 768)); final JLabel loadlabel = new JLabel("Loading ..."); loadlabel.setHorizontalAlignment(JLabel.CENTER); loadpanel.add(loadlabel, BorderLayout.CENTER); // Show waiting indicator and work in background. SwingWorker<T, Integer> worker = new SwingWorker<T, Integer>() { @Override protected T doInBackground() throws Exception { m_tableContentView.setModel(m_tableModel); return null; } @Override protected void done() { setComponent(m_tableView); } }; worker.execute(); while (!worker.isDone()) { // do nothing } // Temporarily add loadpanel as component, so that the ui stays // responsive. setComponent(loadpanel); } /** * Creates the underlying TableView and registers it to this dialog. */ private void initializeView() { m_tableContentView = new TableContentView(); m_tableContentView.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); ListSelectionListener listener = new ListSelectionListener() { @Override public void valueChanged(final ListSelectionEvent e) { // Ensures that the listener fires exactly once per selection if (m_tableContentView.getSelectionModel().getValueIsAdjusting() && m_tableContentView.getColumnModel().getSelectionModel().getValueIsAdjusting()) { return; } rowColIntervalSelectionChanged(m_tableContentView.getSelectedRows(), m_tableContentView.getSelectedColumns()); } }; m_tableContentView.getSelectionModel().addListSelectionListener(listener); m_tableContentView.getColumnModel().getSelectionModel().addListSelectionListener(listener); m_tableView = new TableView(m_tableContentView); m_tableView.setHiLiteHandler(getNodeModel().getInHiLiteHandler(0)); if (!m_hiliteAdded) { getJMenuBar().add(m_tableView.createHiLiteMenu()); m_hiliteAdded = true; } // Set preferred height to ~ 1 row m_tableView.setPreferredSize(new Dimension(0, (m_tableView.getRowHeight() + 16))); m_cellViewCache = new HashMap<String, CellView>(); } /** * {@inheritDoc} */ @Override protected void modelChanged() { if ((getNodeModel().getInternalTables() == null) || (getNodeModel().getInternalTables().length == 0) || (getNodeModel().getInternalTables()[m_portIdx] == null)) { if (m_cellViewCache != null) { for (final CellView v : m_cellViewCache.values()) { v.onReset(); } } m_row = -1; m_col = -1; final JLabel nodata = new JLabel("No data table available!"); nodata.setPreferredSize(new Dimension(500, 500)); setComponent(nodata); } else { m_tableModel = new TableContentModel(); m_tableModel.setDataTable(getNodeModel().getInternalTables()[m_portIdx]); initViewComponents(); } } /** * {@inheritDoc} */ @Override protected void onClose() { if (m_cellViewCache != null) { for (final CellView v : m_cellViewCache.values()) { v.onClose(); } } m_cellViewCache = null; m_tableContentView = null; m_tableModel = null; m_tableView = null; } /** * {@inheritDoc} */ @Override protected void onOpen() { } public void toggleOverview() { if (getComponent().equals(m_contentPanel)) { setComponent(m_tableView); m_tableExpanded = false; m_tableContentView.clearSelection(); m_prevRows = new int[] { -1 }; m_prevCols = new int[] { -1 }; } } /** * Creates the main panel of the dialog. * * @return The main panel of the dialog-frame. */ private JPanel createPanel() { JPanel result = new JPanel(); result.setLayout(new BorderLayout()); m_splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); result.add(m_splitPane, BorderLayout.CENTER); m_splitPane.setTopComponent(createTopPanel()); m_splitPane.setResizeWeight(1); JPanel statusBar = new JPanel(); statusBar.setBorder(new BevelBorder(BevelBorder.LOWERED)); result.add(statusBar, BorderLayout.SOUTH); statusBar.setPreferredSize(new Dimension(0, 16)); statusBar.setLayout(new BoxLayout(statusBar, BoxLayout.X_AXIS)); m_statusLabel = new JLabel(m_defStatusBarText); m_statusLabel.setHorizontalAlignment(SwingConstants.LEFT); statusBar.add(m_statusLabel); return result; } /** * Creates the top component of the split pane, i.e. the actual panel * containing the viewers and the navigation. * * @return The top panel of the split pane. */ private JComponent createTopPanel() { JPanel result = new JPanel(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridheight = 1; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.fill = GridBagConstraints.BOTH; gbc.weightx = 1; gbc.weighty = 1; gbc.gridx = 0; m_viewsPane = new JTabbedPane(); result.add(m_viewsPane, gbc); gbc.gridheight = GridBagConstraints.REMAINDER; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weighty = 0; JComponent navbar = createNavBar(); result.add(navbar, gbc); m_viewsPane.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { if (m_viewsPane.getSelectedIndex() == -1 || m_updatingTabs) return; String title = m_viewsPane.getTitleAt(m_viewsPane.getSelectedIndex()); CellView p = m_cellViewCache.get(title); p.updateComponent(m_cellValues); } }); return result; } /** * Creates the Navigation panel at the bottom of the view. * * @return The navigation panel */ private JComponent createNavBar() { Box navbar = new Box(BoxLayout.X_AXIS); navbar.add(Box.createRigidArea(new Dimension(10, 40))); Box firstPanel = new Box(BoxLayout.X_AXIS); JButton overviewButton = new JButton("Back to Table"); overviewButton.setMnemonic(KeyEvent.VK_B); firstPanel.add(overviewButton); overviewButton.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { toggleOverview(); } }); firstPanel.add(Box.createHorizontalStrut(20)); Box quickViewButtonPanel = new Box(BoxLayout.X_AXIS); JToggleButton quickViewButton = new JToggleButton("Expand Table View"); quickViewButtonPanel.add(quickViewButton); firstPanel.add(quickViewButtonPanel); firstPanel.add(Box.createHorizontalStrut(20)); Box colourButtonPanel = new Box(BoxLayout.X_AXIS); firstPanel.add(colourButtonPanel); navbar.add(firstPanel); navbar.add(Box.createHorizontalGlue()); quickViewButton.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { if (!m_tableExpanded) showTableView(); else hideTableView(); m_tableExpanded = !m_tableExpanded; } }); m_navPanel = new NavigationPanel(); navbar.add(m_navPanel); navbar.add(Box.createHorizontalGlue()); Box thirdPanel = new Box(BoxLayout.X_AXIS); thirdPanel.add(Box.createHorizontalGlue()); JButton helpButton = new JButton("?"); thirdPanel.add(helpButton); helpButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JDialog helpFrame = new CellViewerHelpDialog(); if (KNIMEConstants.KNIME16X16 != null) helpFrame.setIconImage(KNIMEConstants.KNIME16X16.getImage()); helpFrame.pack(); helpFrame.setVisible(true); } }); thirdPanel.add(Box.createHorizontalStrut(15)); navbar.add(thirdPanel); m_navPanel.getUpButton().addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { m_tableContentView.changeSelection(m_row - 1, m_col, false, false); } }); m_navPanel.getDownButton().addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { m_tableContentView.changeSelection(m_row + 1, m_col, false, false); } }); m_navPanel.getLeftButton().addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { m_tableContentView.changeSelection(m_row, m_col - 1, false, false); } }); m_navPanel.getRightButton().addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { m_tableContentView.changeSelection(m_row, m_col + 1, false, false); } }); return navbar; } private void showTableView() { m_splitPane.setBottomComponent(m_tableView); m_splitPane.setDividerLocation(m_contentPanel.getHeight() - (m_tableView.getColumnHeaderViewHeight() + m_tableView.getRowHeight() + m_splitPane.getDividerSize() + 4 + 16 /* Navbar */)); m_tableView.scrollRectToVisible(m_tableContentView.getCellRect(m_row, 0, true)); } private void hideTableView() { m_splitPane.remove(2); } }