/* * Open Source Physics software is free software as described near the bottom of this code file. * * For additional information and documentation on Open Source Physics please see: * <http://www.opensourcephysics.org/> */ package org.opensourcephysics.controls; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Font; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.reflect.Array; import java.util.Collection; import java.util.EventObject; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import javax.swing.AbstractAction; import javax.swing.AbstractCellEditor; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.InputMap; import javax.swing.JColorChooser; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; import org.opensourcephysics.display.CellBorder; import org.opensourcephysics.display.OSPRuntime; import org.opensourcephysics.tools.ArrayInspector; /** * This is a table view of an XML control and its property contents. * * @author Douglas Brown */ public class XMLTable extends JTable { final static Color LIGHT_BLUE = new Color(196, 196, 255); // instance fields XMLTableModel tableModel; XMLCellRenderer xmlRenderer = new XMLCellRenderer(); XMLValueEditor valueEditor = new XMLValueEditor(); Color defaultBackgroundColor = Color.white; Map<String, Color> cellColors = new HashMap<String, Color>(); // maps property to color of cell Map<String, Color> selectedCellColors = new HashMap<String, Color>(); // maps property to color of selected cell Map<String, Color> editingCellColors = new HashMap<String, Color>(); // maps property to color of editing cell Color defaultEditingColor; PropertyChangeListener comboListener; /** * Constructor for XMLControl. * * @param control the XMLcontrol */ public XMLTable(XMLControl control) { tableModel = new XMLTableModel(control); init(); } /** * Constructor for XMLTableModel. * * @param model the XMLTableModel */ public XMLTable(XMLTableModel model) { tableModel = model; init(); } /** * Gets the currently displayed XMLControl. * * @return the XML control */ public XMLControl getControl() { return tableModel.control; } /** * Enables/disables editing for the entire table. * Overrides the editable property of individual parameters. * * @param editable true to enable editing */ public void setEditable(boolean editable) { tableModel.editable = editable; } /** * Returns true if editing is enabled for the entire table. * If table is editiable, editing can still be disabled for individual parameters. * * @return true if editable * @see setEditable(String propName, boolean editable) */ public boolean isEditable() { return tableModel.editable; } /** * Enables/disables editing for a specified property name. * Properties are editable by default. * * @param propName the property name * @param editable true to enable editing */ public void setEditable(String propName, boolean editable) { // add to uneditablePropNames list if editable is false if(!editable) { tableModel.uneditablePropNames.add(propName); } else { tableModel.uneditablePropNames.remove(propName); } } /** * Returns true if editing is enabled for the specified property. * * @param propName the name of the property * @return true if editable */ public boolean isEditable(String propName) { return !tableModel.uneditablePropNames.contains(propName); } /** * Determines whether the given cell is editable. * * @param row the row index * @param col the column index * @return true if editable */ public boolean isCellEditable(int row, int col) { return tableModel.editable&&tableModel.isCellEditable(row, col); } /** * Sets the font. Overrides JTable method * * @param font the font */ public void setFont(Font font) { super.setFont(font); if(xmlRenderer!=null) { xmlRenderer.setFont(font); valueEditor.field.setFont(font); // resize row heights after revalidation Runnable runner = new Runnable() { public void run() { if(getTableHeader().getHeight()>0) { // changed by W. Christian setRowHeight(getTableHeader().getHeight()); } } }; SwingUtilities.invokeLater(runner); } } /** * Sets the color of a selected cell for a specified property name. * May be set to null. * * @param propName the property name * @param color the color of the cell when selected */ public void setSelectedColor(String propName, Color color) { selectedCellColors.put(propName, color); } /** * Gets the color of a selected cell for a specified property name. * * @param propName the property name * @return the color */ public Color getSelectedColor(String propName) { Color color = selectedCellColors.get(propName); return(color==null) ? LIGHT_BLUE : color; } /** * Sets the background color of the value field for a specified property name. * May be set to null. * * @param propName the property name * @param color the color */ public void setBackgroundColor(String propName, Color color) { cellColors.put(propName, color); } /** * Gets the background color for a specified property name. * * @param propName the property name * @return the color */ public Color getBackgroundColor(String propName) { Color color = cellColors.get(propName); return(color==null) ? defaultBackgroundColor : color; } /** * Sets the default color of the editor for a specified property name. * May be set to null. * * @param propName the property name * @param color the color of the cell when being edited */ public void setEditingColor(String propName, Color color) { editingCellColors.put(propName, color); } /** * Sets the color of the editor for a specified property name. * * @param propName the property name * @return the color */ public Color getEditingColor(String propName) { Color color = editingCellColors.get(propName); return(color==null) ? defaultEditingColor : color; } /** * Returns the renderer for a cell specified by row and column. * * @param row the row number * @param column the column number * @return the cell renderer */ public TableCellRenderer getCellRenderer(int row, int column) { return xmlRenderer; } /** * Returns the editor for a cell specified by row and column. * * @param row the row number * @param column the column number * @return the cell editor */ public TableCellEditor getCellEditor(int row, int column) { return valueEditor; } /** * A cell renderer for an xml table. */ class XMLCellRenderer extends DefaultTableCellRenderer { Color lightGreen = new Color(204, 255, 204); // for double-clickable cells Color lightGray = javax.swing.UIManager.getColor("Panel.background"); //$NON-NLS-1$ Font font = new JTextField().getFont(); // Constructor /** * Constructor XMLCellRenderer */ public XMLCellRenderer() { super(); setOpaque(true); // make background visible setForeground(Color.black); setFont(font); } // Returns a label for the specified cell. public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { setForeground(Color.BLACK); if(value==null) { value = ""; // changed by W. Christian to trap for null values //$NON-NLS-1$ } String propName = (String) tableModel.getValueAt(row, 0); Class<?> childClass = null; Object childObj = null; if(column==0) { // property name if(isSelected) { //setBackground(new Color(255, 192, 255)); setBackground(LIGHT_BLUE); } else { setBackground(lightGray); } setHorizontalAlignment(SwingConstants.LEFT); String text = value.toString(); if(OSPRuntime.getTranslator()!=null) { text = OSPRuntime.getTranslator().getProperty(XMLTable.this, text); } setText(text); //setBorder(BorderFactory.createLineBorder(new Color(224, 224, 224),1)); setBorder(BorderFactory.createEmptyBorder(2, 1, 2, 2)); return this; } if(value instanceof XMLProperty) { // object, array or collection type XMLProperty prop = (XMLProperty) value; XMLProperty parent = prop.getParentProperty(); childClass = parent.getPropertyClass(); String className = XML.getSimpleClassName(childClass); XMLControl control = (XMLControl) parent.getParentProperty(); childObj = control.getObject(parent.getPropertyName()); // array type if(parent.getPropertyType().equals("array")) { //$NON-NLS-1$ Object array = childObj; // determine if base component type is primitive and count array elements Class<?> baseType = array.getClass().getComponentType(); int count = Array.getLength(array); int insert = className.indexOf("[]")+1; //$NON-NLS-1$ className = className.substring(0, insert)+count+className.substring(insert); while(baseType.getComponentType()!=null) { baseType = baseType.getComponentType(); array = Array.get(array, 0); if(array==null) { break; } count = Array.getLength(array); insert = className.indexOf("[]", insert)+1; //$NON-NLS-1$ className = className.substring(0, insert)+count+className.substring(insert); } } // general object types if((childClass!=OSPCombo.class // OSPCombo handled below )&&(childClass!=Boolean.class // Boolean handled below )&&(childClass!=Character.class)) { // Char handled below setText(className); setBackground(isInspectable(parent) ? lightGreen : lightGray); //setBorder(BorderFactory.createEmptyBorder(2, 1, 2, 2)); //setBorder(BorderFactory.createLineBorder(new Color(240, 240, 240))); setBorder(new CellBorder(new Color(240, 240, 240))); setHorizontalAlignment(SwingConstants.CENTER); if(isSelected&&isInspectable(parent)) { setBackground(getSelectedColor(propName)); setForeground(Color.RED); } return this; } } // int, double, boolean, string and special object types if(isSelected) { setBackground(getSelectedColor(propName)); setForeground(Color.RED); } else { setBackground(getBackgroundColor(propName)); } setHorizontalAlignment(SwingConstants.LEFT); if((childClass==OSPCombo.class)||(childClass==Boolean.class)||(childClass==Character.class)) { setText(childObj.toString()); } else { setText(value.toString()); } //setBorder(BorderFactory.createLineBorder(new Color(240, 240, 240))); setBorder(new CellBorder(new Color(240, 240, 240))); //setBorder(BorderFactory.createEmptyBorder(2, 1, 2, 2)); if(!tableModel.editable||tableModel.uneditablePropNames.contains(propName)) { setForeground(Color.GRAY); // override all other color settings } return this; } } /** * A cell editor for an xml table. */ class XMLValueEditor extends AbstractCellEditor implements TableCellEditor { JPanel panel = new JPanel(new BorderLayout()); JTextField field = new JTextField(); int keepFocus = -2; OSPCombo combo; // may be null // Constructor. XMLValueEditor() { defaultEditingColor = field.getSelectionColor(); panel.add(field, BorderLayout.CENTER); panel.setOpaque(false); field.setBorder(BorderFactory.createLineBorder(new Color(128, 128, 128), 1)); //field.setBorder(BorderFactory.createEmptyBorder(0, 1, 1, 0)); field.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { stopCellEditing(); keepFocus = -2; } }); field.addKeyListener(new KeyAdapter() { public void keyPressed(KeyEvent e) { if(e.getKeyCode()==KeyEvent.VK_ENTER) { stopCellEditing(); keepFocus = -2; } else if(field.isEnabled()) { field.setBackground(Color.yellow); } } }); field.addFocusListener(new FocusAdapter() { public void focusLost(FocusEvent e) { if(e.isTemporary()) { return; } if(field.getBackground()!=defaultBackgroundColor) { stopCellEditing(); } int i = XMLTable.this.getSelectedRow(); if(keepFocus==i) { keepFocus = -2; } else { XMLTable.this.requestFocusInWindow(); } } }); // OSPCombo case field.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { if(combo!=null) { if(combo.getPropertyChangeListeners("index").length>0) { //$NON-NLS-1$ combo.removePropertyChangeListener("index", comboListener); //$NON-NLS-1$ combo.setVisible(false); stopCellEditing(); } else { // remove and add combo listener combo.removePropertyChangeListener("index", comboListener); //$NON-NLS-1$ combo.addPropertyChangeListener("index", comboListener); //$NON-NLS-1$ combo.showPopup(field); } } } }); } // Gets the component to be displayed while editing. public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { combo = null; String propName = (String) tableModel.getValueAt(row, 0); field.setBackground(defaultBackgroundColor); field.setSelectionColor(getEditingColor(propName)); final int rowNumber = row; final int colNumber = column; if(value instanceof XMLControl) { // object type final XMLControl childControl = (XMLControl) value; // Color case if(childControl.getObjectClass()==Color.class) { Color color = (Color) childControl.loadObject(null); String title = ControlsRes.getString("XMLTable.ColorChooser.Title"); //$NON-NLS-1$ Color newColor = JColorChooser.showDialog(null, title, color); if((newColor!=null)&&!color.equals(newColor)) { childControl.saveObject(newColor); tableModel.fireTableCellUpdated(row, column); } return null; } // Character case if(childControl.getObjectClass()==Character.class) { Character c = (Character) childControl.loadObject(null); field.setEditable(true); field.setText(c.toString()); return panel; } // OSPCombo case if(childControl.getObjectClass()==OSPCombo.class) { combo = (OSPCombo) childControl.loadObject(null); combo.row = row; combo.column = column; field.setText(combo.toString()); field.setEditable(false); return panel; } // Boolean case if(childControl.getObjectClass()==Boolean.class) { Boolean bool = (Boolean) childControl.loadObject(null); int n = bool.booleanValue() ? 0 : 1; combo = new OSPCombo(new String[] {"true", "false"}, n); //$NON-NLS-1$//$NON-NLS-2$ combo.row = row; combo.column = column; field.setText(bool.toString()); field.setEditable(false); combo.addPropertyChangeListener("value", new PropertyChangeListener() { //$NON-NLS-1$ public void propertyChange(PropertyChangeEvent e) { OSPCombo combo = (OSPCombo) e.getSource(); Boolean bool = new Boolean(combo.getSelectedIndex()==0); childControl.saveObject(bool); } }); return panel; } XMLTableInspector inspector = new XMLTableInspector(childControl, isEditable()); // listen for "xmlData" changes in inspector inspector.addPropertyChangeListener(new java.beans.PropertyChangeListener() { public void propertyChange(PropertyChangeEvent e) { // signal listeners when inspector closes and xml data is changed if(e.getPropertyName().equals("xmlData")) { //$NON-NLS-1$ tableModel.fireTableCellUpdated(rowNumber, colNumber); } } }); // offset new inspector relative to parent container Container cont = XMLTable.this.getTopLevelAncestor(); Point p = cont.getLocationOnScreen(); inspector.setLocation(p.x+30, p.y+30); inspector.setVisible(true); return null; } else if(value instanceof XMLProperty) { // collection or array type XMLProperty prop = (XMLProperty) value; XMLProperty parent = prop.getParentProperty(); if(parent.getPropertyType().equals("collection")) { //$NON-NLS-1$ String name = parent.getPropertyName(); parent = parent.getParentProperty(); if(parent instanceof XMLControl) { XMLControl cControl = new XMLControlElement(); Collection<?> c = (Collection<?>) ((XMLControl) parent).getObject(name); Iterator<?> it = c.iterator(); int i = 0; while(it.hasNext()) { Object next = it.next(); cControl.setValue("item_"+i, next); //$NON-NLS-1$ i++; } XMLTableInspector inspector = new XMLTableInspector(cControl); inspector.setTitle(ControlsRes.getString("XMLTable.Inspector.Title")+name+"\""); //$NON-NLS-1$//$NON-NLS-2$ // offset new inspector relative to parent container Container cont = XMLTable.this.getTopLevelAncestor(); Point p = cont.getLocationOnScreen(); inspector.setLocation(p.x+30, p.y+30); inspector.setVisible(true); cont.transferFocus(); } } // display an array inspector if available XMLProperty arrayProp = prop.getParentProperty(); ArrayInspector arrayInspector = ArrayInspector.getInspector(arrayProp); if(arrayInspector!=null) { String name = arrayProp.getPropertyName(); parent = arrayProp.getParentProperty(); while(!(parent instanceof XMLControl)) { name = parent.getPropertyName(); arrayProp = parent; parent = parent.getParentProperty(); } final XMLControl arrayControl = (XMLControl) parent; final String arrayName = name; final Object arrayObj = arrayInspector.getArray(); arrayInspector.setEditable(tableModel.editable); // listen for "cell" and "arrayData" changes in the array inspector arrayInspector.addPropertyChangeListener(new java.beans.PropertyChangeListener() { public void propertyChange(PropertyChangeEvent e) { if(e.getPropertyName().equals("cell")) { //$NON-NLS-1$ // set new value in array control arrayControl.setValue(arrayName, arrayObj); } // signal listeners when inspector closes and array data is changed else if(e.getPropertyName().equals("arrayData")) { //$NON-NLS-1$ tableModel.fireTableCellUpdated(rowNumber, colNumber); } } }); // offset new arrayInspector relative to parent container Container cont = XMLTable.this.getTopLevelAncestor(); Point p = cont.getLocationOnScreen(); arrayInspector.setLocation(p.x+30, p.y+30); arrayInspector.setVisible(true); cont.transferFocus(); } return null; } // end XMLProperty case // value is string field.setEditable(true); if(value!=null) { field.setText(value.toString()); } return panel; } // Determines when editing starts. public boolean isCellEditable(EventObject e) { if(e instanceof MouseEvent) { MouseEvent me = (MouseEvent) e; XMLTable table = (XMLTable) me.getSource(); int row = table.rowAtPoint(me.getPoint()); keepFocus = row; Object value = tableModel.getValueAt(row, 1); // handle special object cases if(value instanceof XMLControl) { XMLControl childControl = (XMLControl) value; if((childControl.getObjectClass()==OSPCombo.class)||(childControl.getObjectClass()==Boolean.class)||(childControl.getObjectClass()==Character.class)) { return true; } } if((value instanceof String)||(me.getClickCount()==2)) { return true; } } else if(e instanceof ActionEvent) { keepFocus = -2; return true; } return false; } // Called when editing is completed. public Object getCellEditorValue() { XMLTable.this.requestFocusInWindow(); if(field.getBackground()!=defaultBackgroundColor) { field.setBackground(defaultBackgroundColor); return field.getText(); } return null; } } // refreshes the table public void refresh() { Runnable runner = new Runnable() { public synchronized void run() { tableChanged(new TableModelEvent(tableModel, TableModelEvent.HEADER_ROW)); } }; if(SwingUtilities.isEventDispatchThread()) { runner.run(); } else { SwingUtilities.invokeLater(runner); } } public void tableChanged(TableModelEvent e) { // pass the tablemodel event to property change listeners firePropertyChange("tableData", null, e); //$NON-NLS-1$ super.tableChanged(e); } /** * Adds a listener that invokes the given method in the given object when the xml data changes. * The method in the target is invoked with the table's variable name passed as a * parameter. The method will be invoked for all parameter changes. * * @param methodName the name of the method * @param target the target for the method */ public void addControlListener(String methodName, final Object target) { addControlListener(null, methodName, target); } /** * Adds a listener that invokes the given method in the given object when the xml data changes. * The method in the target is invoked with the table's variable name passed as a * parameter. The method will be invoked for all parameter changes if the parameter name is null. * * @param parameterName the name of the parameter that will invoke the method * @param methodName the name of the method that will be invoked * @param target the target class for the method */ public void addControlListener(final String parameterName, String methodName, final Object target) { Class<?>[] parameters = new Class[] {String.class}; try { final java.lang.reflect.Method m = target.getClass().getMethod(methodName, parameters); tableModel.addTableModelListener(new TableModelListener() { final String par = parameterName; public void tableChanged(TableModelEvent e) { if((e.getType()!=TableModelEvent.UPDATE)||(e.getColumn()!=1)||(e.getFirstRow()<0)) { return; } String name = getValueAt(e.getFirstRow(), 0).toString(); if((par==null)||par.equals(name)) { // invoke the method for all parameters if the parameter name is null Object[] args = {name}; try { m.invoke(target, args); } catch(Exception ex) { ex.printStackTrace(); } } } }); } catch(NoSuchMethodException nsme) { System.err.println(ControlsRes.getString("XMLTable.ErrorMessage.NoMethod") //$NON-NLS-1$ +" "+methodName+"()"); //$NON-NLS-1$//$NON-NLS-2$ } } // initializes the table private void init() { setModel(tableModel); JTableHeader header = getTableHeader(); header.setReorderingAllowed(false); header.setForeground(Color.BLACK); // set header text color setGridColor(Color.BLACK); // Override the default tab behaviour InputMap im = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); KeyStroke tab = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0); final Action prevTabAction = getActionMap().get(im.get(tab)); Action tabAction = new AbstractAction() { public void actionPerformed(ActionEvent e) { // tab to the next editable cell prevTabAction.actionPerformed(e); JTable table = (JTable) e.getSource(); int rowCount = table.getRowCount(); int row = table.getSelectedRow(); int column = table.getSelectedColumn(); while(!table.isCellEditable(row, column)) { if(column==0) { column = 1; } else { row += 1; } if(row==rowCount) { row = 0; } if((row==table.getSelectedRow())&&(column==table.getSelectedColumn())) { break; } } table.changeSelection(row, column, false, false); } }; getActionMap().put(im.get(tab), tabAction); // enter key starts editing Action enterAction = new AbstractAction() { public void actionPerformed(ActionEvent e) { // start editing JTable table = (JTable) e.getSource(); int row = table.getSelectedRow(); int column = table.getSelectedColumn(); table.editCellAt(row, column, e); Component comp = table.getEditorComponent(); if(comp instanceof JPanel) { JPanel panel = (JPanel) comp; comp = panel.getComponent(0); if(comp instanceof JTextField) { JTextField field = (JTextField) comp; OSPCombo combo = null; Object value = tableModel.getValueAt(row, column); if(value instanceof XMLControl) { final XMLControl control = (XMLControl) value; if(control.getObjectClass()==OSPCombo.class) { combo = (OSPCombo) control.loadObject(null); } else if(control.getObjectClass()==Boolean.class) { Boolean bool = (Boolean) control.loadObject(null); int n = bool.booleanValue() ? 0 : 1; combo = new OSPCombo(new String[] {"true", "false"}, n); //$NON-NLS-1$//$NON-NLS-2$ combo.addPropertyChangeListener("value", new PropertyChangeListener() { //$NON-NLS-1$ public void propertyChange(PropertyChangeEvent e) { OSPCombo combo = (OSPCombo) e.getSource(); Boolean bool = new Boolean(combo.getSelectedIndex()==0); control.saveObject(bool); } }); } } if(combo==null) { field.requestFocus(); field.selectAll(); } else { // add combo listener and show popup combo.row = row; combo.column = column; combo.removePropertyChangeListener("index", comboListener); //$NON-NLS-1$ combo.addPropertyChangeListener("index", comboListener); //$NON-NLS-1$ combo.showPopup(field); } } } } }; KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); getActionMap().put(im.get(enter), enterAction); // create OSPCombo listener comboListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent e) { OSPCombo combo = (OSPCombo) e.getSource(); int n = ((Integer) e.getOldValue()).intValue(); if(n!=combo.getSelectedIndex()) { combo.firePropertyChange("value", n, combo.getSelectedIndex()); //$NON-NLS-1$ tableModel.fireTableCellUpdated(combo.row, combo.column); } combo.removePropertyChangeListener("index", this); //$NON-NLS-1$ valueEditor.stopCellEditing(); } }; } // determines whether the specified property is inspectable private boolean isInspectable(XMLProperty prop) { if(prop.getPropertyType().equals("object")) { //$NON-NLS-1$ return true; } if(prop.getPropertyType().equals("array")) { //$NON-NLS-1$ return ArrayInspector.canInspect(prop); } if(prop.getPropertyType().equals("collection")) { //$NON-NLS-1$ return true; } return false; } } /* * Open Source Physics software is free software; you can redistribute * it and/or modify it under the terms of the GNU General Public License (GPL) as * published by the Free Software Foundation; either version 2 of the License, * or(at your option) any later version. * * Code that uses any portion of the code in the org.opensourcephysics package * or any subpackage (subdirectory) of this package must must also be be released * under the GNU GPL license. * * This software 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; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA * or view the license online at http://www.gnu.org/copyleft/gpl.html * * Copyright (c) 2007 The Open Source Physics project * http://www.opensourcephysics.org */