package com.limegroup.gnutella.gui.tables; import java.awt.Color; import java.awt.Dimension; import java.awt.IllegalComponentStateException; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.Arrays; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.CellEditor; import javax.swing.Icon; import javax.swing.InputMap; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.border.BevelBorder; import javax.swing.event.ListSelectionListener; import javax.swing.event.MouseInputListener; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellRenderer; import com.limegroup.gnutella.Assert; import com.limegroup.gnutella.gui.ButtonRow; import com.limegroup.gnutella.gui.GUIConstants; import com.limegroup.gnutella.gui.PaddedPanel; import com.limegroup.gnutella.gui.themes.ThemeFileHandler; import com.limegroup.gnutella.gui.themes.ThemeObserver; import com.limegroup.gnutella.util.StringUtils; import com.limegroup.gnutella.licenses.License; /** * The basics of a ComponentMediator for a Table. * Used for: * Associating a LimeJTable (TABLE) with a DataLineModel (DATA_MODEL). * Associating a JPopupMenu & ButtonRow (BUTTON_ROW) * with the DATA_MODEL. * Holding common Action/Mouse/ListSelection listeners. * (REMOVE_LISTENER, DEFAULT_LISTENER, HEADER_LISTENER, SELECTION_LISTENER) * Holding common TableCellRenderers. [static] * (PROGRESS_BAR_RENDERER, CHAT_RENDERER) * Building a JPanel (MAIN_PANEL) of the LimeJTable, ButtonRow & JPopupMenu. * Handling mouse interactions and displaying the appropriate menus. * A popup menu if right-click over table. * ColumnSelectionMenu if right-click over the header. * Sorting the DATA_MODEL if left-click over the header. * Refreshing the DATA_MODEL from the RefreshListener's call. * @author Sam Berlin */ public abstract class AbstractTableMediator implements ComponentMediator, HeaderMouseObserver, ThemeObserver { /** * The ID that uniquely defines this table. */ protected final String ID; /** * Variable to the main component displaying this Table. * MUST be initialized in setupConstants() */ protected PaddedPanel MAIN_PANEL; /** * Variable to the DataLineList containg the underlying data for this table. * MUST be initialized in setupConstants() */ protected DataLineModel DATA_MODEL; /** * Variable to the LimeJTable for this table. * MUST be initialized in setupConstants() */ protected LimeJTable TABLE; /** * Variable to the ButtonRow for this table. */ protected ButtonRow BUTTON_ROW; /** * Variable for the RemoveListener for this component. */ public ActionListener REMOVE_LISTENER; /** * Variable for the DefaultMouseListener for this component. */ public MouseListener DEFAULT_LISTENER; /** * Variable for the HeaderMouseListener for this component. */ public MouseInputListener HEADER_LISTENER; /** * Variable for the ListSelectionListener for this component. */ public ListSelectionListener SELECTION_LISTENER; /** * KeyListener for moving based on typing. */ public KeyListener AUTO_NAVIGATION_KEY_LISTENER; /** * Variable for the TableSettings for this component. */ public TableSettings SETTINGS; /** * Variable for the SpeedRenderer for all components. */ protected static final TableCellRenderer SPEED_RENDERER = new SpeedRenderer(); /** * Variable for the ProgressBarRenderer for all components. */ protected static final TableCellRenderer PROGRESS_BAR_RENDERER = new ProgressBarRenderer(); /** * Variable for the ChatRenderer for all components. */ protected static final TableCellRenderer CHAT_RENDERER = new ChatRenderer(); /** * Variable for the ColorRenderer for all components. */ protected static final TableCellRenderer COLOR_RENDERER = new ColorRenderer(); /** * Variable for the IconRenderer for all components. */ protected static final TableCellRenderer ICON_RENDERER = new IconRenderer(); /** * Variable for the IconAndNameRenderer for all components. */ protected static final TableCellRenderer ICON_AND_NAME_RENDERER = new IconAndNameRenderer(); /** * Variable for the default renderer for all components. */ protected static final TableCellRenderer DEFAULT_RENDERER = new DefaultTableCellRenderer(); /** * Variable for the centered renderer. */ protected static final TableCellRenderer CENTER_RENDERER = new CenteredRenderer(); /** * Variable for the License renderer. */ protected static final TableCellRenderer LICENSE_RENDERER = new LicenseRenderer(); /** * A zero dimension to be used in all tables. */ protected static final Dimension ZERO_DIMENSION = new Dimension(0, 0); /** * Resorter -- for doing real-time resorts. */ protected Resorter RESORTER = new Resorter(); /** * The <tt>Component</tt> containing the <tt>JScrollPane</tt> for the * table. */ protected JComponent TABLE_PANE; /** * The <tt>JScrollPane</tt> instance for scrolling through the table. */ protected JScrollPane SCROLL_PANE; /** * Is true when the table is currently resorted. Should only be used by * package internal event listeners, which want to suppress events during * resorting. */ protected boolean isResorting = false; /** * Basic constructor that uses a Template Pattern to delegate the * setup functions to individual methods. The following methods * are called in the order they are listed.<p> * <ul> * <li> updateSplashScreen </li> * <li> buildSettings </li> * <li> buildListeners </li> * <li> setupConstants </li> * <li> setupTable </li> * <li> setupDragAndDrop </li> * <li> addActions </li> * <li> addListeners </li> * <li> setDefaultRenderers </li> * <li> setDefaultEditors </li> * <li> setupMainPanel </li> * <li> setupTableHeaders </li> * <li> handleNoSelection </li> * </ul> * Of these, some have are already written as default implementions. * The extending class should call GUIMediator.addRefreshListener(this) * if they want to be a refreshListener, * and ThemeMediator.addThemeObserver(this) if they want to be * a ThemeObserver. */ protected AbstractTableMediator(String id) { this.ID = id; this.updateSplashScreen(); this.buildSettings(); this.buildListeners(); this.setupConstants(); //Assert.that(MAIN_PANEL != null, "MAIN_PANEL not set."); Assert.that(DATA_MODEL != null, "DATA_MODEL not set."); Assert.that(TABLE != null, "TABLE not set."); this.setupTable(); this.setupDragAndDrop(); this.addActions(); this.addListeners(); this.setDefaultRenderers(); this.setDefaultEditors(); this.setupMainPanel(); this.setupTableHeaders(); this.handleNoSelection(); } /** * Sets up Drag & Drop for the table. * Default implementation does nothing. * * This is called prior to addListeners, because D&D wraps all * Mouse[Motion]Listeners behind a proxy, and we don't need to * proxy listeners added from here. */ protected void setupDragAndDrop() {} /** * Intended for updating the splash screen while this component loads. */ protected abstract void updateSplashScreen(); /** * Retrieves or builds the correct settings. */ protected void buildSettings() { SETTINGS = new TableSettings(ID); } /** * Intended for setting the DATA_MODEL and TABLE constants. * Optionally, MAIN_PANEL, BUTTON_ROW, and POPUP_MENU can also be set. */ protected abstract void setupConstants(); /** * Assigns the listeners to their slots. * This must be done _before_ the individual components * are created, incase any of them want to use a listener. * Extending components that want to build extra listeners * should call super.buildListeners and then build their own. * DEFAULT_LISTENER, SELECTION_LISTENER, HEADER_LISTENER, * and REMOVE_LISTENER are created by default. */ protected void buildListeners() { DEFAULT_LISTENER = new DefaultMouseListener(this); SELECTION_LISTENER = new SelectionListener(this); HEADER_LISTENER = new HeaderMouseListener(this); REMOVE_LISTENER = new RemoveListener(this); AUTO_NAVIGATION_KEY_LISTENER = new KeyTypedMover(); } /** * Adds the listeners to the table. * Extending components that want to add extra listeners * should call super.addListeners and then add their own. * DEFAULT_LISTENER, SELECTION_LISTENER and HEADER_LISTENER * are added by default. */ protected void addListeners() { TABLE.addMouseListener(DEFAULT_LISTENER); TABLE.getSelectionModel().addListSelectionListener(SELECTION_LISTENER); TABLE.getTableHeader().addMouseListener(HEADER_LISTENER); TABLE.getTableHeader().addMouseMotionListener(HEADER_LISTENER); TABLE.addKeyListener(AUTO_NAVIGATION_KEY_LISTENER); } /** * Sets row heights a little larger than normal, turns off * the display of the grid and disallows column selections. */ protected void setupTable() { TABLE.setRowHeight(TABLE.getRowHeight() + 1); TABLE.setShowGrid(false); TABLE.setIntercellSpacing(ZERO_DIMENSION); TABLE.setColumnSelectionAllowed(false); TABLE.setTableSettings(SETTINGS); TABLE.getTableHeader().addMouseListener(new FlexibleColumnResizeAdapter()); } /** * Add input/action events to the table. * * Currently sets the 'action' key to call 'handleActionKey'. */ protected void addActions() { InputMap map = TABLE.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); Action enter = new AbstractAction() { public void actionPerformed(ActionEvent e) { handleActionKey(); } }; installAction(map, enter, KeyEvent.VK_ENTER, "limewire.action"); Action delete = new AbstractAction() { public void actionPerformed(ActionEvent e) { if(!TABLE.isEditing()) removeSelection(); } }; installAction(map, delete, KeyEvent.VK_BACK_SPACE, "limewire.delete"); installAction(map, delete, KeyEvent.VK_DELETE, "limewire.delete"); } /** * Installs an action to the table with the specified key & signifier. */ private void installAction(InputMap map, Action action, int key, String sig) { KeyStroke stroke = KeyStroke.getKeyStroke(key, 0); Object obj = map.get(stroke); // If the action already exists in the input map, just refocus // the action map. if(obj != null) { TABLE.getActionMap().put(obj, action); } else { // Otherwise, install a new entry into both the input & action map. map.put(stroke, sig); TABLE.getActionMap().put(sig, action); } } /** * Intended for adding default renderers to the table. * Extending components that want to add extra default renderers * should call super.setDefaultRenderers and then add their own. * By default, PROGRESS_BAR_RENDERER is assigned to * ProgressBarHolder.class, CHAT_RENDERER is assigned to * ChatHolder.class, COLOR_RENDERER is assigned to * ColoredCell.class, ICON_RENDERER is assigned to Icon.class, * and ICON_AND_NAME_RENDERER is assigned to IconAndNameHolder.class. */ protected void setDefaultRenderers() { TABLE.setDefaultRenderer(ProgressBarHolder.class, PROGRESS_BAR_RENDERER); TABLE.setDefaultRenderer(ChatHolder.class, CHAT_RENDERER); TABLE.setDefaultRenderer(ColoredCell.class, COLOR_RENDERER); TABLE.setDefaultRenderer(Icon.class, ICON_RENDERER); TABLE.setDefaultRenderer(IconAndNameHolder.class, ICON_AND_NAME_RENDERER); TABLE.setDefaultRenderer(Object.class, DEFAULT_RENDERER); TABLE.setDefaultRenderer(CenteredHolder.class, CENTER_RENDERER); TABLE.setDefaultRenderer(License.class, LICENSE_RENDERER); TABLE.setDefaultRenderer(SpeedRenderer.class, SPEED_RENDERER); } /** * Intended for setting up default editors. By default, * no editors are added. */ protected void setDefaultEditors() { } /** * Sets up the MAIN_PANEL to have a uniform look among all tables. * If the MAIN_PANEL is not created, nothing will happen. */ protected void setupMainPanel() { if (MAIN_PANEL != null) { MAIN_PANEL.add(getScrolledTablePane()); if (BUTTON_ROW != null) { MAIN_PANEL.add(Box.createVerticalStrut(GUIConstants.SEPARATOR)); MAIN_PANEL.add(BUTTON_ROW); } MAIN_PANEL.setMinimumSize(ZERO_DIMENSION); } } /** * Organizes the table headers so that they're sized correctly, * in the correct order, and are either visible or not visible, * depending on the user's preferences. */ protected void setupTableHeaders() { ColumnPreferenceHandler cph = createDefaultColumnPreferencesHandler(); cph.setWidths(); cph.setOrder(); cph.setVisibility(); TABLE.setColumnPreferenceHandler(cph); } /** * Creates the ColumnPreferencesHandler. * * Extending classes should override this to use a custom * ColumnPreferencesHandler. */ protected ColumnPreferenceHandler createDefaultColumnPreferencesHandler() { return new DefaultColumnPreferenceHandler(TABLE); } /** * Sets up & gets the table inside a JScrollPanel inside a JPanel. */ protected JComponent getScrolledTablePane() { // if it already exists, return it if (TABLE_PANE != null) return TABLE_PANE; JPanel tablePane = new JPanel(); tablePane.setLayout(new BoxLayout(tablePane, BoxLayout.Y_AXIS)); SCROLL_PANE = new JScrollPane(TABLE); //Add the nice 3d bevel to the top-right corner JPanel corner = new JPanel(); corner.setBackground(TABLE.getTableHeader().getBackground()); corner.setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED)); SCROLL_PANE.setCorner(JScrollPane.UPPER_RIGHT_CORNER, corner); tablePane.add(SCROLL_PANE); TABLE_PANE = tablePane; updateTheme(); return tablePane; } // inherit doc comment public void updateTheme() { Color tableColor = ThemeFileHandler.TABLE_BACKGROUND_COLOR.getValue(); if (TABLE_PANE == null) return; TABLE_PANE.setBackground(tableColor); TABLE.setBackground(tableColor); TABLE.setOpaque(true); SCROLL_PANE.getViewport().setBackground(tableColor); } /** * Add a new DataLine initialized by Object o to the list, * tell the dataModel there was a row inserted, * and unselect the first row (to address the java bug) */ public void add(Object o) { if (TABLE.isEditing()) { CellEditor editor = TABLE.getCellEditor(); editor.cancelCellEditing(); } boolean inView = TABLE.isSelectionVisible(); int addedAt; if (SETTINGS.REAL_TIME_SORT.getValue() && DATA_MODEL.isSorted()) addedAt = DATA_MODEL.addSorted(o); else addedAt = DATA_MODEL.add(o); // if it was added... fixSelection(addedAt, inView); } /** * Forces the object to be added unsorted. */ public void addUnsorted(Object o) { if (TABLE.isEditing()) { CellEditor editor = TABLE.getCellEditor(); editor.cancelCellEditing(); } boolean inView = TABLE.isSelectionVisible(); int addedAt = DATA_MODEL.add(o); fixSelection(addedAt, inView); } /** * Removes the selection from where the row was added, * and puts the focus on a previously selected row. */ protected void fixSelection(int addedAt, boolean inView) { if ( addedAt >= 0 && addedAt < DATA_MODEL.getRowCount() ) { // unselect the row to address a Java bug // (if the previous row was selected, // then the newly added one will be selected also) TABLE.removeRowSelectionInterval(addedAt, addedAt); // (and must reselect an older row, 'cause unselecting moves // the traversing focus) int selected = TABLE.getSelectedRow(); if(selected >= 0 && selected < DATA_MODEL.getRowCount()) { TABLE.addRowSelectionInterval(selected, selected); if(inView) TABLE.ensureRowVisible(selected); } } } /** * Removes the row associated with the Object o * Delegates to removeRow(int) * If no matching row is found, nothing is done. */ public void remove(Object o) { int idx = DATA_MODEL.getRow(o); if (idx != -1) removeRow(idx); } /** * Removes the row. */ public void removeRow(int row) { DATA_MODEL.remove(row); } /** * Implements RefreshListener * Wraps the doRefresh call so that extending classes * can maintain the resort & isShowing checks. * Extending classes should NOT override this. * Instead, they should override doRefresh. */ public void refresh() { // We only want to refresh if the table is showing. // This saves on traversing through every item in non visible tables. // This will cause at most a one second lag on updating // statistics if a user switches to the tab. // There probably are ways around the lag (such as intercepting the // call and manually running a refresh) if necessary. // The lag will only be visible, currently, on the // Upload & Download tables, since they cache data instead // of acting directly on the respective loaders. if (TABLE.isShowing()) { doRefresh(); resort(); } } /** * Exists for extending classes to overwrite. * Mostly used for updating the values of buttons/menu-items * With the return value of the DATA_MODEL's refresh. */ protected void doRefresh() { DATA_MODEL.refresh(); } /** * Tells the model to update a specific DataLine */ public void update(Object o) { DATA_MODEL.update(o); resort(); } /** * Resorts the underlying data. Maintains highlighted rows. */ public void resort() { RESORTER.doResort(false); } /** * Resorts the underlying data, regardless of if the column is dynamic. */ public void forceResort() { RESORTER.doResort(true); } /** * @return The main component which has all components */ public JComponent getComponent() { return MAIN_PANEL; } /** * Removes all selected rows from the list * and fires deletions through the dataModel */ public void removeSelection() { int[] sel = TABLE.getSelectedRows(); Arrays.sort(sel); for (int counter = sel.length - 1; counter >= 0; counter--) { int i = sel[counter]; DATA_MODEL.remove(i); } clearSelection(); } /** * Creates a new ColumnSelectionMenu JPopupMenu. * If the table wants to use a custom popup menu, override this * method and do something different. */ protected JPopupMenu createColumnSelectionMenu() { return (new ColumnSelectionMenu(TABLE)).getComponent(); } /** * Forwards a double click to the 'action key'. */ public void handleMouseDoubleClick(MouseEvent e) { handleActionKey(); } /** * Changes the selection in the table in response to a right-mouse click. * This was contributed by Chance Moore originally for the search package. */ public void handleRightMouseClick(MouseEvent e) { Point p = e.getPoint(); int row = TABLE.rowAtPoint(p); //check its valid, should always be but cheap to check if (row < 0) return; if (!TABLE.getSelectionModel().isSelectedIndex(row)) { //if right clicked row is not selected, make it so TABLE.getSelectionModel().setSelectionInterval(row, row); } } /** * Shows the popup menu at Point p */ public void handlePopupMenu(MouseEvent e) { Point p = e.getPoint(); handleRightMouseClick(e); JPopupMenu menu = createPopupMenu(); if(menu != null) { try { menu.show(TABLE, p.x+1, p.y-6); } catch(IllegalComponentStateException icse) { // happens occasionally, ignore. } } } /** * Sorts the column whose header maps to the given point */ public void handleHeaderColumnLeftClick(Point p) { JTableHeader th = TABLE.getTableHeader(); int col = th.columnAtPoint(p); int c = TABLE.convertColumnIndexToModel(col); int oldC = DATA_MODEL.getSortColumn(); if (c != -1) { sortAndMaintainSelection(c); // force the headers to repaint with new icons. th.repaint(th.getHeaderRect(col)); if(oldC != -1 && oldC != c) { int oldCol = TABLE.convertColumnIndexToView(oldC); th.repaint(th.getHeaderRect(oldCol)); } } } /** * Show the column selection menu. */ public void handleHeaderPopupMenu(Point p) { createColumnSelectionMenu().show(TABLE.getTableHeader(), p.x+1, p.y-6); } /** * Tell the table something is pressed. */ public void handleHeaderColumnPressed(Point p) { JTableHeader th = TABLE.getTableHeader(); int col = th.columnAtPoint(p); int c = TABLE.convertColumnIndexToModel(col); if (c != -1) { TABLE.setPressedColumnIndex(c); // force the table to redraw the column header th.repaint(th.getHeaderRect(col)); } } /** * Tell the table something is not pressed. */ public void handleHeaderColumnReleased(Point p) { TABLE.setPressedColumnIndex(-1); JTableHeader th = TABLE.getTableHeader(); int col = th.columnAtPoint(p); if (col != -1) // force the table to redraw the column header th.repaint(th.getHeaderRect(col)); } /** * Delegates the setButtonEnabled call to the ButtonRow */ public void setButtonEnabled(int buttonIdx, boolean enabled) { if (BUTTON_ROW != null) BUTTON_ROW.setButtonEnabled(buttonIdx, enabled); } /** * Gets the size of the underlying model. */ public int getSize() { return DATA_MODEL.getRowCount(); } /** * Clear the table of all items */ public void clearTable() { DATA_MODEL.clear(); handleNoSelection(); } /** * Helper-function to clear selected items. */ protected void clearSelection() { TABLE.clearSelection(); handleNoSelection(); } /** * Sorts the DATA_MODEL and maintains selections in the TABLE. * If columnToSort is -1, it will resort the current column. * Otherwise it will sort columnToSort. */ protected void sortAndMaintainSelection(int columnToSort) { // store the currently selected rows int[] rows = TABLE.getSelectedRows(); DataLine[] dls = new DataLine[rows.length]; Object inView = null; for (int i = 0; i < rows.length; i++) { dls[i] = DATA_MODEL.get(rows[i]); if(inView == null && TABLE.isRowVisible(rows[i])) inView = dls[i]; } // do the sorting if (columnToSort == -1) DATA_MODEL.resort(); else DATA_MODEL.sort(columnToSort); // reselect the rows. for(int i = 0; i < rows.length; i++) { int sel = DATA_MODEL.getRow(dls[i]); TABLE.addRowSelectionInterval(sel, sel); if(inView == dls[i]) { TABLE.ensureRowVisible(sel); inView = null; } } } /** * Abstract method for creating a right-click popup menu for the * table. If an implemention does not support a right-click * popup menu, it should return <tt>null</tt>. * * @return a new <tt>JPopupMenu</tt> to display on right-click */ protected abstract JPopupMenu createPopupMenu(); protected final class Resorter implements Runnable { private boolean active = false; private boolean force = false; /** * To save processor usage, only resort those tables that are * currently showing, are sorting in real time, and are not * actively being sorted. */ public void doResort(boolean isForce) { // TABLE.isShowing() should be checked last, since it's a // recursive call (and thus the most expensive) // through all the parents of the table. if ( !active && SETTINGS.REAL_TIME_SORT.getValue() && TABLE.isShowing() ) { active = true; SwingUtilities.invokeLater(this); } force |= isForce; } /** * Iff the data model needs resorting (meaning the selected * column is a 'dynamic' column), then do the resort. * We need to remember what was selected because resorting * will invalidate the selections. * We cannot do it while the table is editing because * editing overrides what is displaying in that cell, * making the user think they are still editing the correct * line, when infact it has moved. */ public void run() { try { if (!TABLE.isEditing() && (force || DATA_MODEL.needsResort())) { isResorting = true; sortAndMaintainSelection(-1); isResorting = false; } } catch (Exception e) {} active = false; force = false; } } /** * Convenience method to generically compare any two comparable * things. * * Handles comparison uniquely for 'native' types. * We want to compare strings by lowercase comparison * Note that non-integer comparisons must specifically * check if the difference is less or greater than 0 * so that rounding won't be wrong. * Of the native types, we check 'Integer' first since * that's the most common, Boolean, * then Double or Float, and finally, the rest will be caught in * 'Number', which just uses an int comparison. */ static int compare(Object o1, Object o2) { int retval; if ( o1 == null && o2 == null ) { retval = 0; } else if ( o1 == null ) { retval = -1; } else if ( o2 == null ) { retval = 1; } else if ( o1.getClass() == String.class ) { retval = StringUtils.compareFullPrimary( (String)o1, (String)o2 ); } else if( o1 instanceof java.lang.Comparable ) { retval = ((java.lang.Comparable)o1).compareTo(o2); } else { retval = 0; } return retval; } /** * Returns whether the table is resorting. */ boolean isResorting() { return isResorting; } }