package com.compomics.util.gui.tablemodels; import com.compomics.util.gui.TableMouseWheelListener; import com.compomics.util.gui.TableScrollBarListener; import com.compomics.util.waiting.WaitingHandler; import com.compomics.util.gui.waiting.waitinghandlers.ProgressDialogX; import com.compomics.util.gui.waiting.waitinghandlers.WaitingHandlerDummy; import java.awt.Component; import java.awt.event.AdjustmentListener; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import javax.swing.Icon; import javax.swing.JLabel; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.table.DefaultTableModel; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellRenderer; /** * These table models include a self updating function. Due to instability of * the JTable with this model, it comprises a simple row sorter. Use it at your * own risks and feel free to debug. * * @author Marc Vaudel * @author Harald Barsnes */ public abstract class SelfUpdatingTableModel extends DefaultTableModel { /** * The view start index of the rows being loaded. */ private int rowStartLoading = -1; /** * The view end index of the rows being loaded. */ private int rowEndLoading = -1; /** * The number of rows loaded at a time. */ private static int batchSize = 100; /** * If false, the table will not update automatically. */ private boolean selfUpdating = true; /** * boolean indicating whether an update was scheduled. */ private boolean updateScheduled = false; /** * The last loading runnable. */ private LoadingRunnable lastLoadingRunnable = null; /** * List of view indexes. */ private ArrayList<Integer> viewIndexes = null; /** * Indicates which column was last changed. */ private int lastColumnSorted = -1; /** * If true the current sorting is ascending. */ private boolean sortAscending = false; /** * If true the table has not yet been sorted. */ private boolean unsorted = true; /** * A progress dialog. */ private ProgressDialogX progressDialog; /** * When true this indicates that the user is currently scrolling in the * table and that the table should not update. */ public boolean isScrolling = false; /** * Loads the data needed for objects at rows of the given view indexes. Use * this method to cache data before working on the given lines. * * @param indexes the view indexes to load as a list. Shall not be empty or * null. * @param waitingHandler the waiting handler * @return the last updated row */ protected abstract int loadDataForRows(ArrayList<Integer> indexes, WaitingHandler waitingHandler); /** * Loads the data for a column. Use this method to cache data before working * on the given column. * * @param column the column number * @param waitingHandler a waiting handler used to display progress to the * user or interrupt the process */ protected abstract void loadDataForColumn(int column, WaitingHandler waitingHandler); /** * This method is called whenever an exception is encountered in a separate * thread. * * @param e the exception encountered */ protected abstract void catchException(Exception e); /** * Calling this method indicates that data is missing at the given row. The * data will be loaded in a separate thread and the table updated later on. * * @param row the row number (not the view index) * @throws InterruptedException if an InterruptedException occurs */ protected void dataMissingAtRow(int row) throws InterruptedException { int anticipatedStart = (int) (row + 0.9 * batchSize); if (lastLoadingRunnable == null || lastLoadingRunnable.isFinished() || row < rowStartLoading || row >= anticipatedStart) { rowStartLoading = row; rowEndLoading = Math.min(row + batchSize, getRowCount() - 1); if (lastLoadingRunnable != null) { lastLoadingRunnable.cancel(); } lastLoadingRunnable = new LoadingRunnable(); new Thread(lastLoadingRunnable, "identificationFeatures").start(); } updateContent(); } /** * Checks whether the protein table is filled properly and updates it later * on if not. * * @throws InterruptedException */ private synchronized void updateContent() throws InterruptedException { if (selfUpdating && !updateScheduled) { updateScheduled = true; wait(100); new Thread(new Runnable() { public synchronized void run() { try { if (selfUpdating) { fireTableDataChanged(); } } catch (Exception e) { catchException(e); } updateScheduled = false; } }, "tableUpdate").start(); } } /** * Indicates whether the table is in self update mode. * * @return true if the table is in self update mode */ public boolean isSelfUpdating() { return selfUpdating; } /** * Sets whether the table is in self update mode. * * @param selfUpdating if false the table will not automatically update */ public void setSelfUpdating(boolean selfUpdating) { this.selfUpdating = selfUpdating; } /** * Indicates whether the given column needs an update, i.e. whether a cell * contains waitingContent. * * @param column index of the column of interest * @param waitingContent the waiting content of this table * @return indicates whether the given column needs an update */ public boolean needsUpdate(int column, String waitingContent) { for (int row = getRowCount() - 1; row >= 0; row--) { Object cellContent = getValueAt(row, column); if (cellContent instanceof String && cellContent.equals(waitingContent)) { return true; } } return false; } /** * Initiates the sorter to the current order of the table. */ public void initiateSorter() { int nRows = getRowCount(); viewIndexes = new ArrayList<Integer>(nRows); for (int row = 0; row < nRows; row++) { viewIndexes.add(row); } } /** * Returns the view index of the given row. * * @param row the row of interest * @return the corresponding view index */ public int getViewIndex(int row) { if (viewIndexes == null) { return row; } int nRows = getRowCount(); if (nRows != viewIndexes.size()) { initiateSorter(); } if (row < 0 || row >= nRows) { nRows--; throw new IllegalArgumentException("Row " + row + " must be between 0 and " + nRows); } return viewIndexes.get(row); } /** * Returns the row number of the given view index. * * @param viewIndex view index row of interest * @return the corresponding row */ public int getRowNumber(int viewIndex) { if (viewIndexes == null) { return viewIndex; } int nRows = getRowCount(); if (nRows != viewIndexes.size()) { initiateSorter(); } if (viewIndex < 0 || viewIndex >= nRows) { nRows--; throw new IllegalArgumentException("View index " + viewIndex + " must be between 0 and " + nRows + "."); } return viewIndexes.indexOf(viewIndex); } /** * Sorts the table according to a given column using the built in sorter. * * @param aProgressDialog a progress dialog used to display the progress and * interrupt the process */ public void resetSorting(ProgressDialogX aProgressDialog) { if (!unsorted) { sortColumn(lastColumnSorted, aProgressDialog); if (!sortAscending) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { Collections.reverse(viewIndexes); fireTableDataChanged(); } }); } } } /** * Sorts the table according to a given column using the built in sorter. * * @param column the column of interest * @param aProgressDialog a progress dialog used to display the progress and * interrupt the process */ public void sort(int column, ProgressDialogX aProgressDialog) { if (column == lastColumnSorted) { // initate sorter if needed if (viewIndexes == null || viewIndexes.size() != getRowCount()) { initiateSorter(); } sortAscending = !sortAscending; // same column, change sorting order Collections.reverse(viewIndexes); fireTableDataChanged(); } else { sortAscending = true; // new column, sort acending sortColumn(column, aProgressDialog); } } /** * Sort the given columns in ascending order. * * @param column the column to sort on * @param aProgressDialog a progress dialog */ private void sortColumn(int column, ProgressDialogX aProgressDialog) { final int finalColumn = column; this.progressDialog = aProgressDialog; progressDialog.resetSecondaryProgressCounter(); progressDialog.setTitle("Sorting. Please Wait..."); progressDialog.setPrimaryProgressCounterIndeterminate(false); progressDialog.setMaxPrimaryProgressCounter(getRowCount()); progressDialog.setValue(0); new Thread(new Runnable() { public void run() { try { if (progressDialog != null) { progressDialog.setVisible(true); } } catch (IndexOutOfBoundsException e) { // ignore } } }, "ProgressDialog").start(); new Thread("SortThread") { @Override public void run() { try { setSelfUpdating(false); progressDialog.setDisplayProgress(false); loadDataForColumn(finalColumn, progressDialog); // @TODO: update the progress bar here as well? progressDialog.setDisplayProgress(true); initiateSorter(); lastColumnSorted = 0; HashMap<Comparable, ArrayList<Integer>> valueToRowMap = new HashMap<Comparable, ArrayList<Integer>>(); boolean comparable = false, string = false; for (int row = 0; row < getRowCount() && (progressDialog != null && !progressDialog.isRunCanceled()); row++) { Object tableValue = getValueAt(row, finalColumn); Comparable key; if (tableValue instanceof Comparable) { key = (Comparable) tableValue; comparable = true; } else { key = tableValue.toString(); string = true; } ArrayList<Integer> rows = valueToRowMap.get(key); if (rows == null) { rows = new ArrayList<Integer>(); valueToRowMap.put(key, rows); } rows.add(row); if (progressDialog != null) { progressDialog.increasePrimaryProgressCounter(); } } if (progressDialog != null && progressDialog.isRunCanceled()) { progressDialog.setRunFinished(); return; } ArrayList<Comparable> keys = new ArrayList<Comparable>(valueToRowMap.keySet()); if (string && comparable) { ArrayList<Comparable> stringValues = new ArrayList<Comparable>(); for (Comparable value : keys) { stringValues.add(value.toString()); } keys = stringValues; } if (progressDialog == null || !progressDialog.isRunCanceled()) { viewIndexes = new ArrayList<Integer>(); Collections.sort(keys); for (Comparable key : keys) { viewIndexes.addAll(valueToRowMap.get(key)); } lastColumnSorted = finalColumn; } } catch (Exception ex) { catchException(ex); } finally { setSelfUpdating(true); } if (progressDialog != null) { progressDialog.setRunFinished(); } fireTableDataChanged(); } }.start(); } /** * Convenience method adding a row sorter listener to the given JTable. * * @param jTable the table to add the resetSorting listener to * @param progressDialog progress dialog used to display progress or cancel * while sorting. Can be null. */ public static void addSortListener(JTable jTable, ProgressDialogX progressDialog) { final JTableHeader proteinTableHeader = jTable.getTableHeader(); final JTable finalTable = jTable; final ProgressDialogX progressDialogX = progressDialog; proteinTableHeader.addMouseListener(new java.awt.event.MouseAdapter() { public void mouseClicked(java.awt.event.MouseEvent evt) { if (evt.getButton() == MouseEvent.BUTTON1) { int column = proteinTableHeader.getColumnModel().getColumnIndexAtX(evt.getX()); SelfUpdatingTableModel model = (SelfUpdatingTableModel) finalTable.getModel(); model.sort(column, progressDialogX); model.unsorted = false; } } }); // set the arrows indicating the current resetSorting order final TableCellRenderer r = finalTable.getTableHeader().getDefaultRenderer(); TableCellRenderer wrapper = new TableCellRenderer() { @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Component comp = r.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if (comp instanceof JLabel) { JLabel label = (JLabel) comp; label.setIcon(getSortIcon(table, column)); label.setHorizontalTextPosition(SwingConstants.LEFT); // @TODO: align text to the left and the icon to the right... finalTable.getTableHeader().revalidate(); finalTable.getTableHeader().repaint(); } return comp; } /** * Implements the logic to choose the appropriate icon. */ private Icon getSortIcon(JTable table, int column) { if (table.getModel() instanceof SelfUpdatingTableModel && ((SelfUpdatingTableModel) table.getModel()).lastColumnSorted == column && !((SelfUpdatingTableModel) table.getModel()).unsorted) { if (((SelfUpdatingTableModel) table.getModel()).sortAscending == false) { return UIManager.getIcon("Table.descendingSortIcon"); } else { return UIManager.getIcon("Table.ascendingSortIcon"); } } else { return null; } } }; finalTable.getTableHeader().setDefaultRenderer(wrapper); } /** * Runnable used for the loading and its interruption. */ private class LoadingRunnable implements Runnable { /** * The waiting handler. */ private WaitingHandlerDummy waitingHandler = new WaitingHandlerDummy(); @Override public synchronized void run() { try { ArrayList<Integer> viewIndexes = new ArrayList<Integer>(batchSize); for (int row = rowStartLoading; row <= rowEndLoading; row++) { viewIndexes.add(getViewIndex(row)); } if (!viewIndexes.isEmpty() && !waitingHandler.isRunCanceled()) { rowEndLoading = getRowNumber(loadDataForRows(viewIndexes, waitingHandler)); } } catch (Exception e) { catchException(e); } waitingHandler.setRunFinished(); } /** * Cancels the thread. */ public void cancel() { waitingHandler.setRunCanceled(); } /** * Indicates whether the run is finished. * * @return true if the thread is finished. */ public boolean isFinished() { return waitingHandler.isRunFinished(); } } /** * Indicates whether the table is currently being scrolled. * * @return true if the table is currently being scrolled */ public boolean isScrolling() { return isScrolling; } /** * Set if the user is currently scrolling or not. * * @param isScrolling the isScrolling to set */ public void setIsScrolling(boolean isScrolling) { this.isScrolling = isScrolling; if (isScrolling && lastLoadingRunnable != null) { lastLoadingRunnable.cancel(); } } /** * Add scroll bar and mouse wheel listeners. * * @param table the table * @param scrollBar the scroll bar * @param scrollPane the scroll pane */ public static void addScrollListeners(JTable table, JScrollPane scrollPane, JScrollBar scrollBar) { // add scroll bar listener AdjustmentListener scrollBarListener = new TableScrollBarListener(table); scrollBar.addAdjustmentListener(scrollBarListener); // add mouse wheel listener TableMouseWheelListener mouseWheelListener = new TableMouseWheelListener(table); scrollPane.addMouseWheelListener(mouseWheelListener); } }