/* This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program; if not, see http://www.gnu.org/licenses or write to the Free Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 */ package com.servoy.j2db.gui; import java.awt.Component; import java.awt.Graphics; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.util.EventObject; import java.util.Vector; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JEditorPane; import javax.swing.JLabel; import javax.swing.JTable; import javax.swing.KeyStroke; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.event.ListSelectionEvent; import javax.swing.table.TableColumnModel; import javax.swing.table.TableModel; import com.servoy.j2db.IApplication; import com.servoy.j2db.ISmartClientApplication; import com.servoy.j2db.dataprocessing.FoundSet; import com.servoy.j2db.smart.dataui.CellAdapter; import com.servoy.j2db.util.IDelegate; import com.servoy.j2db.util.UIUtils; import com.servoy.j2db.util.model.AlwaysRowSelectedSelectionModel; /** * @author jcompagner */ public class FixedJTable extends JTable { //private boolean editable = true; private boolean autoScroll; protected final IApplication application; /** * Constructor for TheRightTable. */ public FixedJTable(IApplication application) { super(); this.application = application; } /** * Constructor for TheRightTable. * * @param dm */ public FixedJTable(IApplication application, TableModel dm) { super(dm); this.application = application; } /** * Constructor for TheRightTable. * * @param dm * @param cm */ public FixedJTable(IApplication application, TableModel dm, TableColumnModel cm) { super(dm, cm); this.application = application; } /** * Constructor for TheRightTable. * * @param dm * @param cm * @param sm */ public FixedJTable(IApplication application, TableModel dm, TableColumnModel cm, ListSelectionModel sm) { super(dm, cm, sm); this.application = application; } /** * @return Returns the editable. */ public boolean isEditable() { return true; } /** * @see javax.swing.JComponent#processMouseEvent(java.awt.event.MouseEvent) */ @Override protected void processMouseEvent(MouseEvent e) { if (e.getID() == MouseEvent.MOUSE_PRESSED && ((getSelectionModel().getSelectionMode() == ListSelectionModel.SINGLE_SELECTION) || ((getSelectionModel().getSelectionMode() == ListSelectionModel.MULTIPLE_INTERVAL_SELECTION) && !UIUtils.isCommandKeyDown(e) && !e.isShiftDown())) && isEnabled()) { Point p = e.getPoint(); int row = rowAtPoint(p); if (row != -1 && getSelectedRow() != row) { ListSelectionModel rsm = getSelectionModel(); if (rsm instanceof AlwaysRowSelectedSelectionModel && !((AlwaysRowSelectedSelectionModel)rsm).canChangeSelection()) return; if (!isEditing() || getCellEditor().stopCellEditing()) { rsm.setSelectionInterval(row, row); } } } MouseEvent me = e; if (e.isAltDown()) { me = new MouseEvent(e.getComponent(), e.getID(), e.getWhen(), e.getModifiers() | e.getModifiersEx(), e.getX(), e.getY(), e.getClickCount(), e.isPopupTrigger()) { @Override public int getModifiers() { return (super.getModifiers() | super.getModifiersEx()); } }; } super.processMouseEvent(me); } /** * @see javax.swing.JTable#changeSelection(int, int, boolean, boolean) */ @Override public void changeSelection(int rowIndex, int columnIndex, boolean toggle, boolean extend) { if (cellEditor != null && getEditingRow() != rowIndex && !cellEditor.stopCellEditing()) { // don't change selection if there is a celleditor hanging around. return; } ListSelectionModel rsm = getSelectionModel(); boolean isSelected = false; for (int index : getSelectedRows()) if (index == rowIndex) isSelected = true; if (!isSelected && rsm instanceof AlwaysRowSelectedSelectionModel && !((AlwaysRowSelectedSelectionModel)rsm).canChangeSelection()) return; ListSelectionModel csm = getColumnModel().getSelectionModel(); // get the value so it will be loaded first. TableModel model = getModel(); if (model instanceof FoundSet) ((FoundSet)model).getRecord(rowIndex); changeSelectionModel(csm, columnIndex, toggle, extend, false, false); changeSelectionModel(rsm, rowIndex, toggle, extend, rsm.getSelectionMode() != ListSelectionModel.SINGLE_SELECTION ? rsm.isSelectedIndex(rowIndex) : false, true); // Scroll after changing the selection as blit scrolling is immediate, // so that if we cause the repaint after the scroll we end up painting // everything! if (getAutoscrolls()) { Rectangle cellRect = getCellRect(rowIndex, columnIndex, false); if (cellRect != null) { scrollRectToVisible(cellRect); } } } private void changeSelectionModel(ListSelectionModel sm, int index, boolean toggle, boolean extend, boolean selected, boolean row) { if (extend) { if (toggle) { sm.setAnchorSelectionIndex(index); } else { int anchorIndex = getAdjustedIndex(sm.getAnchorSelectionIndex(), row); if (anchorIndex == -1) { anchorIndex = 0; } sm.setSelectionInterval(anchorIndex, index); } } else { if (toggle) { if (selected && sm.getMinSelectionIndex() != sm.getMaxSelectionIndex()) { sm.removeSelectionInterval(index, index); } else { sm.addSelectionInterval(index, index); } } else { sm.setSelectionInterval(index, index); } } } private int getAdjustedIndex(int index, boolean row) { int compare = row ? getRowCount() : getColumnCount(); return index < compare ? index : -1; } /** * @param editable The editable to set. */ public void setEditable(boolean editable) { if (!editable && isEditing()) { getCellEditor().stopCellEditing(); } } /* * @see javax.swing.JTable#isCellEditable(int, int) */ @Override public boolean isCellEditable(int row, int column) { Object o = getCellEditor(row, column); if (o instanceof CellAdapter) { Component comp = ((CellAdapter)o).getEditor(); boolean isReadOnlyEditor = (comp instanceof IDelegate) && (((IDelegate)comp).getDelegate() instanceof JEditorPane) && !((JEditorPane)((IDelegate)comp).getDelegate()).isEditable(); if ((comp instanceof JButton || comp instanceof JLabel || isReadOnlyEditor) && comp.isEnabled()) { return true; } } return super.isCellEditable(row, column); } private Object[] editRequest; @Override public boolean editCellAt(int row, int column, EventObject e) { // te normal implementation retuns and does nothing at all when reqestion // focus on next cell called from within editingStop, we want the request // being noted and caried out later if (cellEditor != null) { // if the current one is already editing then don't try to stop it. if (getEditingRow() == row && getEditingColumn() == column) return true; if (!cellEditor.stopCellEditing() || cellEditor != null) { // do edit request after normal removeEditor is finished editRequest = new Object[] { new Integer(row), new Integer(column), e }; // doggy return value, if someone depends on the return // value, we can for sure not return true (even if // processed later) return false; } } // have to "call selection is adjusting first"/select row first if row != // selected row, it is a strange impl from sun, setting the selected row // later than the editor, we need other way around if (!setSelectedRow(row, e)) { // selected index not actually changed, probably due to validation failed return false; } // if we are in multiselect we only edit when we click in single selected row; otherwise we just select (no edit) if (e instanceof MouseEvent && ((MouseEvent)e).getID() == MouseEvent.MOUSE_PRESSED && getSelectionModel().getSelectionMode() == ListSelectionModel.MULTIPLE_INTERVAL_SELECTION) { MouseEvent me = (MouseEvent)e; if (UIUtils.isCommandKeyDown((MouseEvent)e) || me.isShiftDown()) { return false; } else { ListSelectionModel sm = getSelectionModel(); if (sm instanceof AlwaysRowSelectedSelectionModel) { AlwaysRowSelectedSelectionModel fsm = (AlwaysRowSelectedSelectionModel)sm; boolean selectedRow = fsm.setSelectedRow(row); if (!selectedRow) return false; } } } return super.editCellAt(row, column, e); } private boolean setSelectedRow(int row, EventObject event) { ListSelectionModel sm = getSelectionModel(); if (sm instanceof AlwaysRowSelectedSelectionModel && sm.getSelectionMode() == ListSelectionModel.SINGLE_SELECTION) { AlwaysRowSelectedSelectionModel fsm = (AlwaysRowSelectedSelectionModel)sm; return fsm.setSelectedRow(row); } return true; } @Override public void removeEditor() { super.removeEditor(); if (editRequest != null)// if there was edit requested during edit stop, // execute now { final int row = ((Integer)editRequest[0]).intValue(); final int column = ((Integer)editRequest[1]).intValue(); final EventObject eventObject = (EventObject)editRequest[2]; editRequest = null; SwingUtilities.invokeLater(new Runnable() { public void run() { if (!(isEditing() && getEditingRow() == row && getEditingColumn() == column)) { editCellAt(row, column, eventObject); } } }); } } /** * @see javax.swing.JTable#columnSelectionChanged(javax.swing.event.ListSelectionEvent) */ @Override public void columnSelectionChanged(ListSelectionEvent e) { // If table is in single selection model then only the column selection of // the current // selected row can be changed.. So not repainting from row 0 to rowCount // but only selected row. if (getSelectionModel().getSelectionMode() == ListSelectionModel.SINGLE_SELECTION) { int columnCountMin1 = getColumnCount() - 1; if (getRowCount() <= 0 || columnCountMin1 < 0) { return; } int selectedRow = getSelectedRow(); int firstIndex = Math.min(columnCountMin1, Math.max(e.getFirstIndex(), 0)); int lastIndex = Math.min(columnCountMin1, Math.max(e.getLastIndex(), 0)); Rectangle firstColumnRect = getCellRect(selectedRow, firstIndex, false); Rectangle lastColumnRect = getCellRect(selectedRow, lastIndex, false); repaint(firstColumnRect.union(lastColumnRect)); } else { super.columnSelectionChanged(e); } } /* * (non-Javadoc) * * @see javax.swing.JComponent#paintImmediately(int, int, int, int) */ @Override public void paintImmediately(int x, int y, int w, int h) { if (autoScroll) { autoScroll = false; Rectangle cellRect = getCellRect(getSelectedRow(), getSelectedColumn(), false); if (cellRect != null) { scrollRectToVisible(cellRect); } } super.paintImmediately(x, y, w, h); } /* * @see javax.swing.JComponent#repaint(long, int, int, int, int) */ @Override public void repaint(long tm, int x, int y, int width, int height) { // if (autoScroll) // { // autoScroll = false; // Rectangle cellRect = getCellRect(getSelectedRow(), getSelectedColumn(), false); // if (cellRect != null) // { // scrollRectToVisible(cellRect); // } // } super.repaint(tm, x, y, width, height); } /** * @see javax.swing.JTable#valueChanged(javax.swing.event.ListSelectionEvent) */ @Override public void valueChanged(ListSelectionEvent e) { if (getSelectionModel().getSelectionMode() == ListSelectionModel.SINGLE_SELECTION) { if (e.getValueIsAdjusting()) return; int rowCount = getRowCount() - 1; int columnCount = getColumnCount() - 1; if (rowCount < 0 || columnCount < 0) { return; } int firstIndex = Math.min(rowCount, Math.max(e.getFirstIndex(), 0)); int lastIndex = Math.min(rowCount, Math.max(e.getLastIndex(), 0)); Rectangle firstRowRect1 = getCellRect(firstIndex, 0, false); Rectangle firstRowRect2 = getCellRect(firstIndex, columnCount, false); int paintImmediately = 0; if (application instanceof ISmartClientApplication) { paintImmediately = ((ISmartClientApplication)application).getPaintTableImmediately(); } boolean valid = paintImmediately <= 0 && isValid(); if (valid) { Rectangle currentRect = getVisibleRect(); if (currentRect.y > firstRowRect1.y) { valid = false; } else if ((currentRect.y + currentRect.height) <= (firstRowRect1.y + firstRowRect2.height)) { valid = false; } } if (getAutoscrolls()) { if (true) { final Rectangle cellRect = getCellRect(getSelectedRow(), getSelectedColumn(), false); if (cellRect != null) { SwingUtilities.invokeLater(new Runnable() { public void run() { scrollRectToVisible(cellRect); } }); } } else { autoScroll = true; } } if (valid) { super.paintImmediately(firstRowRect1.union(firstRowRect2)); } else { repaint(firstRowRect1.union(firstRowRect2)); } Rectangle lastRowRect1 = getCellRect(lastIndex, 0, false); Rectangle lastRowRect2 = getCellRect(lastIndex, columnCount, false); if (valid) { super.paintImmediately(lastRowRect1.union(lastRowRect2)); } else { repaint(lastRowRect1.union(lastRowRect2)); } } else { super.valueChanged(e); } } /** * Constructor for TheRightTable. * * @param numRows * @param numColumns */ public FixedJTable(IApplication application, int numRows, int numColumns) { super(numRows, numColumns); this.application = application; } /** * Constructor for TheRightTable. * * @param rowData * @param columnNames */ public FixedJTable(IApplication application, Vector rowData, Vector columnNames) { super(rowData, columnNames); this.application = application; } /** * Constructor for TheRightTable. * * @param rowData * @param columnNames */ public FixedJTable(IApplication application, Object[][] rowData, Object[] columnNames) { super(rowData, columnNames); this.application = application; } /** * @see javax.swing.JComponent#processKeyBinding(KeyStroke, KeyEvent, int, boolean) */ @Override protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) { if (e.getKeyCode() == KeyEvent.VK_ENTER && e.getID() == KeyEvent.KEY_PRESSED) { int row = getSelectionModel().getAnchorSelectionIndex(); int column = getColumnModel().getSelectionModel().getAnchorSelectionIndex(); if (getEditingRow() != row && getEditingColumn() != column) { editCellAt(row, column, e); Component comp = getEditorComponent(); if (comp != null) { comp.requestFocus(); if (comp instanceof JComboBox) { ((JComboBox)comp).setPopupVisible(true); } // If we have a button inside the cell, fire its action, don't waste // the first VK_ENTER for just starting the edit. else if (comp instanceof JButton) { final JButton btn = (JButton)comp; SwingUtilities.invokeLater(new Runnable() { public void run() { btn.doClick(); } }); } } return true; } return false; } else if (isEditing() && e.getKeyCode() == KeyEvent.VK_ESCAPE) { getCellEditor().stopCellEditing(); return false; } else if ((e.getKeyCode() == KeyEvent.VK_DOWN || e.getKeyCode() == KeyEvent.VK_UP) && e.getModifiers() == InputEvent.ALT_MASK) { return false; } else if (e.getKeyCode() == KeyEvent.VK_F && e.getModifiers() == InputEvent.CTRL_MASK) { return false; } else { return super.processKeyBinding(ks, e, condition, pressed); } } @Override public boolean getSurrendersFocusOnKeystroke() { return true; } // print grid lines as last step @Override protected void printComponent(Graphics g) { boolean bShowHorizontalLines = getShowHorizontalLines(); boolean bShowVerticalLines = getShowVerticalLines(); this.setShowHorizontalLines(false); this.setShowVerticalLines(false); super.printComponent(g); this.setShowHorizontalLines(bShowHorizontalLines); this.setShowVerticalLines(bShowVerticalLines); printGridLines(g, bShowHorizontalLines, bShowVerticalLines); } private void printGridLines(Graphics g, boolean bShowHorizontalLines, boolean bShowVerticalLines) { Rectangle clip = g.getClipBounds(); Rectangle bounds = this.getBounds(); // account for the fact that the graphics has already been translated // into the table's bounds bounds.x = bounds.y = 0; if (this.getRowCount() <= 0 || this.getColumnCount() <= 0 || // this check prevents us from painting // when the clip doesn't intersect our bounds at all !bounds.intersects(clip)) { return; } boolean ltr = this.getComponentOrientation().isLeftToRight(); Point upperLeft = clip.getLocation(); if (!ltr) { upperLeft.x++; } Point lowerRight = new Point(clip.x + clip.width - (ltr ? 1 : 0), clip.y + clip.height); int rMin = this.rowAtPoint(upperLeft); int rMax = this.rowAtPoint(lowerRight); // This should never happen (as long as our bounds intersect the clip, // which is why we bail above if that is the case). if (rMin == -1) { rMin = 0; } // If the table does not have enough rows to fill the view we'll get -1. // (We could also get -1 if our bounds don't intersect the clip, // which is why we bail above if that is the case). // Replace this with the index of the last row. if (rMax == -1) { rMax = this.getRowCount() - 1; } int cMin = this.columnAtPoint(ltr ? upperLeft : lowerRight); int cMax = this.columnAtPoint(ltr ? lowerRight : upperLeft); // This should never happen. if (cMin == -1) { cMin = 0; } //If the table does not have enough columns to fill the view we'll get -1. // Replace this with the index of the last column. if (cMax == -1) { cMax = this.getColumnCount() - 1; } // Paint the grid. g.setColor(this.getGridColor()); Rectangle minCell = this.getCellRect(rMin, cMin, true); Rectangle maxCell = this.getCellRect(rMax, cMax, true); Rectangle damagedArea = minCell.union(maxCell); if (bShowHorizontalLines) { int tableWidth = damagedArea.x + damagedArea.width; int y = damagedArea.y; for (int row = rMin; row <= rMax; row++) { y += this.getRowHeight(row); g.drawLine(damagedArea.x, y - 1, tableWidth - 1, y - 1); } } if (bShowVerticalLines) { TableColumnModel cm = this.getColumnModel(); int tableHeight = damagedArea.y + damagedArea.height; int x; if (this.getComponentOrientation().isLeftToRight()) { x = damagedArea.x; for (int column = cMin; column <= cMax; column++) { int w = cm.getColumn(column).getWidth(); x += w; g.drawLine(x - 1, 0, x - 1, tableHeight - 1); } } else { x = damagedArea.x; for (int column = cMax; column >= cMin; column--) { int w = cm.getColumn(column).getWidth(); x += w; g.drawLine(x - 1, 0, x - 1, tableHeight - 1); } } } } }