/* * 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.tools; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Font; import java.awt.Frame; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; 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.awt.event.MouseListener; import java.awt.event.MouseMotionAdapter; import java.awt.font.FontRenderContext; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collection; import java.util.EventObject; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import javax.swing.AbstractAction; import javax.swing.AbstractButton; import javax.swing.AbstractCellEditor; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRootPane; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.JTextPane; import javax.swing.KeyStroke; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.border.Border; import javax.swing.border.TitledBorder; import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; import javax.swing.text.Style; import javax.swing.text.StyleConstants; import javax.swing.text.StyleContext; import javax.swing.text.StyledDocument; import javax.swing.undo.AbstractUndoableEdit; import javax.swing.undo.CannotUndoException; import javax.swing.undo.UndoableEdit; import org.opensourcephysics.controls.OSPLog; import org.opensourcephysics.controls.XMLControl; import org.opensourcephysics.controls.XMLControlElement; import org.opensourcephysics.controls.XMLProperty; import org.opensourcephysics.display.OSPRuntime; import org.opensourcephysics.display.TeXParser; /** * A JPanel that manages a table of objects with editable names and expressions. * * @author Douglas Brown */ public class FunctionEditor extends JPanel implements PropertyChangeListener { // static constants @SuppressWarnings("javadoc") public final static String THETA = TeXParser.parseTeX("$\\theta$"); //$NON-NLS-1$ public final static String OMEGA = TeXParser.parseTeX("$\\omega$"); //$NON-NLS-1$ public final static String DEGREES = "\u00B0"; //$NON-NLS-1$ public final static int ADD_EDIT = 0; public final static int REMOVE_EDIT = 1; public final static int NAME_EDIT = 2; public final static int EXPRESSION_EDIT = 3; final static Color LIGHT_BLUE = new Color(204, 204, 255); final static Color MEDIUM_RED = new Color(255, 160, 180); final static Color LIGHT_RED = new Color(255, 180, 200); final static Color LIGHT_GRAY = javax.swing.UIManager.getColor("Panel.background"); //$NON-NLS-1$ final static Color DARK_RED = new Color(220, 0, 0); // static fields static NumberFormat decimalFormat; static DecimalFormat sciFormat; protected static boolean undoEditsEnabled = true; protected static String[] editTypes = {"add row", //$NON-NLS-1$ "delete row", "edit name", "edit expression"}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ static FontRenderContext frc = new FontRenderContext(null, // no AffineTransform false, // no antialiasing false); // no fractional metrics // instance fields protected ParamEditor paramEditor; protected ArrayList<Object> objects = new ArrayList<Object>(); protected String[] names = new String[0]; protected ArrayList<Object> sortedObjects = new ArrayList<Object>(); protected HashSet<String> forbiddenNames = new HashSet<String>(); protected boolean removablesAtTop = false; protected Collection<Object> circularErrors = new HashSet<Object>(); protected Collection<Object> errors = new HashSet<Object>(); protected List<Object> evaluate = new ArrayList<Object>(); protected Table table; protected TableModel tableModel = new TableModel(); protected CellEditor tableCellEditor = new CellEditor(); protected CellRenderer tableCellRenderer = new CellRenderer(); protected JScrollPane tableScroller; protected JButton newButton; protected JButton cutButton; protected JButton copyButton; protected JButton pasteButton; protected JPanel buttonPanel; protected JLabel dragLabel; protected TitledBorder titledBorder; protected FunctionPanel functionPanel; protected AbstractButton[] customButtons; protected boolean anglesInDegrees; protected boolean usePopupEditor = true; protected boolean confirmChanges = true; static { decimalFormat = NumberFormat.getInstance(); decimalFormat.setMaximumFractionDigits(4); decimalFormat.setMinimumFractionDigits(0); decimalFormat.setMaximumIntegerDigits(3); decimalFormat.setMinimumIntegerDigits(1); sciFormat = new DecimalFormat("0.0000E0"); //$NON-NLS-1$ } /** * No-arg constructor */ public FunctionEditor() { super(new BorderLayout()); createGUI(); refreshGUI(); } /** * Gets the table. * * @return the table */ public Table getTable() { return table; } /** * Override getPreferredSize(). * * @return the table size plus button and instruction heights */ public Dimension getPreferredSize() { Dimension dim = table.getPreferredSize(); dim.height += table.getTableHeader().getHeight(); dim.height += buttonPanel.getPreferredSize().height; dim.height += 1.25*table.getRowHeight()+14; return dim; } /** * Replaces the current objects with new ones. * * @param newObjects a list of objects */ public void setObjects(java.util.List<Object> newObjects) { // determine row and column selected int row = table.getSelectedRow(); int col = table.getSelectedColumn(); objects.clear(); objects.addAll(newObjects); evaluateAll(); tableModel.fireTableStructureChanged(); // select same cell if(row<table.getRowCount()) { table.rowToSelect = row; table.columnToSelect = col; } table.requestFocusInWindow(); refreshGUI(); } /** * Gets a shallow clone of the objects list. * * @return a list of objects */ public List<Object> getObjects() { return new ArrayList<Object>(objects); } /** * Gets an array containing the names of the objects. * * @return an array of names */ public String[] getNames() { return names; } /** * Returns the name of the object. * * @param obj the object * @return the name */ public String getName(Object obj) { return null; } /** * Returns the expression of the object. * * @param obj the object * @return the expression */ public String getExpression(Object obj) { return null; } /** * Returns the description of the object. * * @param obj the object * @return the description */ public String getDescription(Object obj) { return null; } /** * Sets the description of the object. Subclasses should override and call this * AFTER changing the object description. * * @param obj the object * @param desc the description */ public void setDescription(Object obj, String desc) { if (obj instanceof Parameter) { firePropertyChange("param_description", null, null); //$NON-NLS-1$ } firePropertyChange("description", null, null); //$NON-NLS-1$ } /** * Returns a tooltip for the object. * * @param obj the object * @return the tooltip */ public String getTooltip(Object obj) { return null; } /** * Gets an existing object with specified name. May return null. * * @param name the name * @return the object */ public Object getObject(String name) { if((name==null)||name.equals("")) { //$NON-NLS-1$ return null; } Iterator<Object> it = objects.iterator(); while(it.hasNext()) { Object next = it.next(); if(name.equals(getName(next))) { return next; } } return null; } /** * Sets the expression of an existing named object, if any. * * @param name the name * @param expression the expression * @param postEdit true to post an undoable edit */ public void setExpression(String name, String expression, boolean postEdit) { if((name==null)||name.equals("")) { //$NON-NLS-1$ return; } for(int row = 0; row<objects.size(); row++) { Object obj = objects.get(row); if(name.equals(getName(obj))&&!getExpression(obj).equals(expression)) { String prev = getExpression(obj); obj = createObject(name, expression, obj); objects.remove(row); objects.add(row, obj); evaluateAll(); tableModel.fireTableStructureChanged(); // select row if(row>=0) { table.changeSelection(row, 1, false, false); } // inform and pass undoable edit to listeners UndoableEdit edit = null; if(postEdit && undoEditsEnabled) { edit = getUndoableEdit(EXPRESSION_EDIT, expression, row, 1, prev, row, 1, getName(obj)); } firePropertyChange("edit", getName(obj), edit); //$NON-NLS-1$ } } } /** * Gets the confirmChanges flag. * * @return true if users are required to confirm changes to function names */ public boolean getConfirmChanges() { return confirmChanges; } /** * Sets the confirmChanges flag. * * @param confirm true to require users to confirm changes to function names */ public void setConfirmChanges(boolean confirm) { confirmChanges= confirm; } /** * Adds an object. * * @param obj the object * @param postEdit true to post an undoable edit * @return the added object */ public Object addObject(Object obj, boolean postEdit) { if(obj==null) { return null; } // determine row number int row = objects.size(); // end of table if(isRemovable(obj)) { if(removablesAtTop) { row = getRemovableRowCount(); // after removable rows } } else if(!removablesAtTop) { row = row-getRemovableRowCount(); // before removable rows } return addObject(obj, row, postEdit, true); } /** * Adds an object at a specified row. * * @param obj the object * @param row the row * @param postEdit true to post an undoable edit * @param firePropertyChange true to fire a property change event * @return the added object */ public Object addObject(Object obj, int row, boolean postEdit, boolean firePropertyChange) { obj = createUniqueObject(obj, getName(obj), confirmChanges); if(obj==null) { return null; } int undoRow = table.getSelectedRow(); int undoCol = table.getSelectedColumn(); java.util.List<Object> newObjects = new ArrayList<Object>(objects); newObjects.add(row, obj); setObjects(newObjects); // select new object table.columnToSelect = 0; table.rowToSelect = row; table.selectOnFocus = true; table.requestFocusInWindow(); // inform and pass undoable edit to listeners UndoableEdit edit = null; if(postEdit && undoEditsEnabled) { edit = getUndoableEdit(ADD_EDIT, obj, row, 0, obj, undoRow, undoCol, getName(obj)); } if(firePropertyChange) { firePropertyChange("edit", getName(obj), edit); //$NON-NLS-1$ } refreshGUI(); return obj; } /** * Removes an object. * * @param obj the object to remove * @param postEdit true to post an undoable edit * @return the removed object */ public Object removeObject(Object obj, boolean postEdit) { if((obj==null)||!isRemovable(obj)) { return null; } int undoCol = table.getSelectedColumn(); for(int undoRow = 0; undoRow<objects.size(); undoRow++) { Object next = objects.get(undoRow); if(next.equals(obj)) { objects.remove(obj); tableModel.fireTableStructureChanged(); // select new row int row = (undoRow==objects.size()) ? undoRow-1 : undoRow; if(row>=0) { table.changeSelection(row, 0, false, false); } // inform and pass undoable edit to listeners UndoableEdit edit = null; if(postEdit) { edit = getUndoableEdit(REMOVE_EDIT, obj, row, 0, obj, undoRow, undoCol, getName(obj)); } evaluateAll(); firePropertyChange("edit", getName(obj), edit); //$NON-NLS-1$ refreshGUI(); } } return obj; } /** * Refreshes button strings based on current locale. */ public void refreshStrings() { refreshGUI(); } /** * Responds to property change events. * * @param e the event */ public void propertyChange(PropertyChangeEvent e) { String name = e.getPropertyName(); if(name.equals("focus")||name.equals("edit")) { //$NON-NLS-1$ //$NON-NLS-2$ // another table gained focus or changed table.clearSelection(); table.rowToSelect = 0; table.columnToSelect = 0; table.selectOnFocus = false; refreshButtons(); } else if(e.getPropertyName().equals("clipboard")) { //$NON-NLS-1$ // clipboard contents have changed refreshButtons(); } } /** * Sets custom buttons on the button panel. Setting buttons to null * removes all buttons from this editor. */ public void setCustomButtons(AbstractButton[] buttons) { customButtons = buttons; if((buttons==null)||(buttons.length==0)) { remove(buttonPanel); return; } buttonPanel.removeAll(); for(int i = 0; i<buttons.length; i++) { buttonPanel.add(buttons[i]); } add(buttonPanel, BorderLayout.NORTH); } /** * sets the usePopupEditor flag. * * @param popup true to use the popup editor. */ public void setUsePopupEditor(boolean popup) { usePopupEditor = popup; } /** * Gets an undoable edit. * * @param type may be ADD_EDIT, REMOVE_EDIT, NAME_EDIT, or EXPRESSION_EDIT * @param redo the new state * @param redoRow the newly selected row * @param redoCol the newly selected column * @param undo the previous state * @param undoRow the previously selected row * @param undoCol the previously selected column * @param name the name of the edited object */ protected UndoableEdit getUndoableEdit(int type, Object redo, int redoRow, int redoCol, Object undo, int undoRow, int undoCol, String name) { if(type==EXPRESSION_EDIT) { ArrayList<AbstractButton> selectedButtons = new ArrayList<AbstractButton>(); undo = new Object[] {undo, selectedButtons}; redo = new Object[] {redo, selectedButtons}; if(customButtons!=null) { for(AbstractButton b : customButtons) { if(b.isSelected()) { selectedButtons.add(b); } } } } return new DefaultEdit(type, redo, redoRow, redoCol, undo, undoRow, undoCol, name); } /** * Determines if an object's name is editable. * * @param obj the object * @return true if the name is editable */ public boolean isNameEditable(Object obj) { return true; } /** * Determines if an object's expression is editable. * * @param obj the object * @return true if the expression is editable */ public boolean isExpressionEditable(Object obj) { return true; } /** * Determines if an object is removable. * * @param obj the object * @return true if removable */ protected boolean isRemovable(Object obj) { return !isImportant(obj)&&isNameEditable(obj)&&isExpressionEditable(obj); } /** * Determines if an object is important. * * @param obj the object * @return true if important */ protected boolean isImportant(Object obj) { return false; } /** * Sets the anglesInDegrees flag. Angles are displayed in degrees * when true, radians when false. * * @param degrees true to display angles in degrees */ public void setAnglesInDegrees(boolean degrees) { anglesInDegrees = degrees; table.repaint(); } /** * Evaluates all current objects. */ public void evaluateAll() { // refresh names array if(names.length!=objects.size()) { names = new String[objects.size()]; } for(int i = 0; i<names.length; i++) { names[i] = getName(objects.get(i)); } // sort the objects by name length sortedObjects.clear(); if(objects.size()>0) { sortedObjects.add(objects.get(0)); for(int i = 1; i<objects.size(); i++) { int size = sortedObjects.size(); for(int j = 0; j<size; j++) { Object obj = objects.get(i); String name = getName(obj); if(name.length()>getName(sortedObjects.get(j)).length()) { sortedObjects.add(j, obj); break; } else if(j==size-1) { sortedObjects.add(obj); } } } } // check for circular references circularErrors.clear(); for(Iterator<Object> it = objects.iterator(); it.hasNext(); ) { Object next = it.next(); String name = getName(next); if(getReferences(name, null).contains(name)) { circularErrors.add(next); } } // find all functions that reference circular errors errors.clear(); for(Iterator<Object> it = objects.iterator(); it.hasNext(); ) { Object next = it.next(); String name = getName(next); for(Iterator<Object> it2 = circularErrors.iterator(); it2.hasNext(); ) { String badName = getName(it2.next()); if(getReferences(name, null).contains(badName)) { errors.add(next); break; } } } // establish evaluation order evaluate.clear(); ArrayList<Object> temp = new ArrayList<Object>(objects); temp.removeAll(errors); ArrayList<String> names = new ArrayList<String>(); while(!temp.isEmpty()) { for(int i = 0; i<temp.size(); i++) { Object next = temp.get(i); String name = getName(next); Set<String> references = getReferences(name, null); if(names.containsAll(references)) { evaluate.add(next); names.add(name); } } temp.removeAll(evaluate); } } /** * Gets the names of functions referenced in a named function expression * either directly or indirectly. * * @param name the name of the function * @param references a Set to add references to (may be null) * @return the set filled with names of referenced functions */ protected Set<String> getReferences(String name, Set<String> references) { if(references==null) { references = new HashSet<String>(); } Object obj = getObject(name); if(obj!=null) { String eqn = getExpression(obj); List<Object> directReferences = new ArrayList<Object>(); for(Iterator<Object> it = sortedObjects.iterator(); it.hasNext(); ) { Object next = it.next(); if(next==obj) { continue; } name = getName(next); if (obj instanceof UserFunction) { // replace function names with # to prevent finding "x" in "exp", etc UserFunction func = (UserFunction)obj; String[] functionNames = func.getFunctionNames(); for (String s: functionNames) { eqn = eqn.replaceAll(s, "#"); //$NON-NLS-1$ } } if(eqn.indexOf(name)>-1) { directReferences.add(next); if(!references.contains(name)) { references.add(name); references.addAll(getReferences(name, references)); } } } setReferences(obj, directReferences); } return references; } /** * Subclasses implement to set objects referenced in an object's expression. */ protected void setReferences(Object obj, List<Object> referencedObjects) { /** empty block */ } /** * Creates the GUI. */ protected void createGUI() { titledBorder = BorderFactory.createTitledBorder(""); //$NON-NLS-1$ setBorder(titledBorder); // create table and scroller table = new Table(tableModel); tableScroller = new JScrollPane(table); tableScroller.createHorizontalScrollBar(); add(tableScroller, BorderLayout.CENTER); buttonPanel = new JPanel(new FlowLayout()); newButton = new JButton(); newButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { String name = getDefaultName(); Object obj = createUniqueObject(null, name, false); addObject(obj, true); } }); cutButton = new JButton(); cutButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Object[] array = getSelectedObjects(); copy(array); for(int i = array.length; i>0; i--) { removeObject(array[i-1], true); } evaluateAll(); } }); copyButton = new JButton(); copyButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { copy(getSelectedObjects()); } }); pasteButton = new JButton(); pasteButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { paste(); } }); buttonPanel.add(newButton); buttonPanel.add(copyButton); buttonPanel.add(cutButton); buttonPanel.add(pasteButton); add(buttonPanel, BorderLayout.NORTH); MouseListener tableFocuser = new MouseAdapter() { public void mousePressed(MouseEvent e) { table.requestFocusInWindow(); functionPanel.clearSelection(); } }; table.getTableHeader().addMouseListener(tableFocuser); tableScroller.addMouseListener(tableFocuser); buttonPanel.addMouseListener(tableFocuser); buttonPanel.addMouseListener(tableFocuser); addMouseListener(tableFocuser); } /** * Refreshes the GUI. */ protected void refreshGUI() { int[] rows = table.getSelectedRows(); int col = table.getSelectedColumn(); tableModel.fireTableStructureChanged(); // refreshes table header strings revalidate(); for(int i = 0; i<rows.length; i++) { table.addRowSelectionInterval(rows[i], rows[i]); } if(rows.length>0) { table.setColumnSelectionInterval(col, col); table.requestFocusInWindow(); } newButton.setText(ToolsRes.getString("FunctionEditor.Button.New")); //$NON-NLS-1$ newButton.setToolTipText(ToolsRes.getString("FunctionEditor.Button.New.Tooltip")); //$NON-NLS-1$ cutButton.setText(ToolsRes.getString("FunctionEditor.Button.Cut")); //$NON-NLS-1$ cutButton.setToolTipText(ToolsRes.getString("FunctionEditor.Button.Cut.Tooltip")); //$NON-NLS-1$ copyButton.setText(ToolsRes.getString("FunctionEditor.Button.Copy")); //$NON-NLS-1$ copyButton.setToolTipText(ToolsRes.getString("FunctionEditor.Button.Copy.Tooltip")); //$NON-NLS-1$ pasteButton.setText(ToolsRes.getString("FunctionEditor.Button.Paste")); //$NON-NLS-1$ pasteButton.setToolTipText(ToolsRes.getString("FunctionEditor.Button.Paste.Tooltip")); //$NON-NLS-1$ titledBorder.setTitle(ToolsRes.getString("FunctionEditor.Border.Title")); //$NON-NLS-1$ refreshButtons(); } /** * Sets the border title. */ public void setBorderTitle(String title) { titledBorder.setTitle(title); } /** * Refreshes button states. */ protected void refreshButtons() { boolean b = getSelectedObject()!=null; copyButton.setEnabled(b); cutButton.setEnabled(b&&isRemovable(getSelectedObject())); pasteButton.setEnabled(getClipboardContents()!=null); } /** * Gets the param editor that defines parameters for functions. */ protected ParamEditor getParamEditor() { return paramEditor; } /** * Sets the param editor that defines parameters for functions. * By default, the editor pased in is ignored unless not yet set. */ protected void setParamEditor(ParamEditor editor) { if((paramEditor==null)&&(editor!=null)) { paramEditor = editor; evaluateAll(); refreshGUI(); } } /** * Gets the FunctionPanel that manages this editor. */ public FunctionPanel getFunctionPanel() { return functionPanel; } /** * Sets the FunctionPanel that contains this editor. * * @param panel the function panel */ public void setFunctionPanel(FunctionPanel panel) { functionPanel = panel; } /** * Returns the default name for newly created objects. */ protected String getDefaultName() { return ToolsRes.getString("FunctionEditor.New.Name.Default"); //$NON-NLS-1$ } /** * Returns a String with the names of variables available for expressions. * This default returns the names of all objects in this panel * except the selected object. */ protected String getVariablesString(String separator) { StringBuffer vars = new StringBuffer(""); //$NON-NLS-1$ int init = vars.length(); boolean firstItem = true; String nameToSkip = getName(getSelectedObject()); for(int i = 0; i<names.length; i++) { if(names[i].equals(nameToSkip)) { continue; } if(!firstItem) { vars.append(" "); //$NON-NLS-1$ } vars.append(names[i]); firstItem = false; } if(vars.length()==init) { return ToolsRes.getString("FunctionPanel.Instructions.Help"); //$NON-NLS-1$ } return ToolsRes.getString("FunctionPanel.Instructions.ValueCell") //$NON-NLS-1$ +separator+vars.toString(); } /** * Returns the number of removable rows. */ private int getRemovableRowCount() { int i = 0; for(Iterator<Object> it = objects.iterator(); it.hasNext(); ) { Object obj = it.next(); if(isRemovable(obj)) { i++; } } return i; } /** * Returns the number of editable rows. */ protected int getPartlyEditableRowCount() { int i = 0; for(Iterator<Object> it = objects.iterator(); it.hasNext(); ) { Object obj = it.next(); if(isNameEditable(obj)||isExpressionEditable(obj)) { i++; } } return i; } /** * Returns true if the object expression is invalid. */ protected boolean isInvalidExpression(Object obj) { return false; } /** * Returns true if any objects have invalid expressions. */ public boolean containsInvalidExpressions() { for(Iterator<Object> it = objects.iterator(); it.hasNext(); ) { if(isInvalidExpression(it.next())) { return true; } } return false; } /** * Copies an array of objects to the clipboard. * * @param array the array */ private void copy(Object[] array) { if((array!=null)&&(array.length>0)) { XMLControl control = new XMLControlElement(this); control.setValue("selected", array); //$NON-NLS-1$ StringSelection ss = new StringSelection(control.toXML()); Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(ss, ss); pasteButton.setEnabled(true); firePropertyChange("clipboard", null, null); //$NON-NLS-1$ } } /** * Pastes the clipboard contents. */ protected void paste() { XMLControl[] controls = getClipboardContents(); if(controls==null) { return; } for(int i = 0; i<controls.length; i++) { // create a new object Object obj = controls[i].loadObject(null); addObject(obj, true); } evaluateAll(); } /** * Gets the clipboard contents. */ protected XMLControl[] getClipboardContents() { try { Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); Transferable tran = clipboard.getContents(null); String dataString = (String) tran.getTransferData(DataFlavor.stringFlavor); if(dataString!=null) { XMLControlElement control = new XMLControlElement(); control.readXML(dataString); if(control.getObjectClass()==this.getClass()) { java.util.List<Object> list = control.getPropertyContent(); for(int i = 0; i<list.size(); i++) { Object next = list.get(i); if(next instanceof XMLProperty) { XMLProperty prop = (XMLProperty) next; if(prop.getPropertyName().equals("selected")) { //$NON-NLS-1$ return prop.getChildControls(); } } } return null; } } } catch(Exception ex) { /** empty block */ } return null; } /** * Returns the currently selected object, if any. */ protected Object getSelectedObject() { int row = table.getSelectedRow(); if(row==-1) { return null; } return objects.get(row); } /** * Returns the currently selected objects, if any. */ protected Object[] getSelectedObjects() { int[] rows = table.getSelectedRows(); Object[] selected = new Object[rows.length]; for(int i = 0; i<rows.length; i++) { selected[i] = objects.get(rows[i]); } return selected; } /** * Creates an object with specified name and expression. An existing object * may be passed in for modification or cloning, but there is no guarantee * the same object will be returned. * * @param name the name * @param expression the expression * @param obj an object to assign values (may be null) * @return the object */ protected Object createObject(String name, String expression, Object obj) { return null; } /** * Returns true if a name is forbidden or in use. * * @param obj the object (may be null) * @param name the proposed name for the object * @return true if disallowed */ protected boolean isDisallowedName(Object obj, String name) { if(forbiddenNames.contains(name)) { return true; } Iterator<Object> it = objects.iterator(); while(it.hasNext()) { Object next = it.next(); if(next==obj) { continue; } if(name.equals(getName(next))) { return true; } } if((paramEditor!=null)&&(paramEditor!=this)) { // check for Parameter[] params = paramEditor.getParameters(); for(int i = 0; i<params.length; i++) { if((params[i]!=obj)&&name.equals(params[i].getName())) { return true; } } } try { Double.parseDouble(name); return true; } catch (NumberFormatException e) { } return false; } /** * Returns a valid function name by removing spaces and symbols. * * @param proposed the proposed name * @return a valid name */ private String getValidName(String proposedName) { if(proposedName==null || proposedName.trim().equals("")) { //$NON-NLS-1$ return ""; //$NON-NLS-1$ } String name = proposedName; ArrayList<String> invalid = getInvalidTokens(name); while(!invalid.isEmpty()) { for(Iterator<String> it = invalid.iterator(); it.hasNext(); ) { String next = it.next(); int n = name.indexOf(next); while(n>-1) { name = (n==0) ? name.substring(next.length()) : name.substring(0, n)+name.substring(n+next.length()); n = name.indexOf(next); } } Object input = JOptionPane.showInputDialog(FunctionEditor.this, ToolsRes.getString("FunctionEditor.Dialog.InvalidName.Message"), //$NON-NLS-1$ ToolsRes.getString("FunctionEditor.Dialog.InvalidName.Title"), //$NON-NLS-1$ JOptionPane.WARNING_MESSAGE, null, null, name); if(input==null) { return null; } if(input.equals(name)) { break; } name = input.toString(); invalid = getInvalidTokens(name); } // warn users and don't accept names that start with a number try { String test = name.substring(0, Math.min(1, name.length())); Double.parseDouble(test); JOptionPane.showMessageDialog(FunctionEditor.this, ToolsRes.getString("FunctionEditor.Dialog.InvalidNumberInName.Text"), //$NON-NLS-1$ ToolsRes.getString("FunctionEditor.Dialog.InvalidName.Title"), //$NON-NLS-1$ JOptionPane.WARNING_MESSAGE); return ""; //$NON-NLS-1$ } catch (NumberFormatException e) { } return name; } /** * Returns a list of invalid tokens found in the name. * Invalid tokens include spaces and mathematical symbols. * * @param name the name * @return a list of invalid tokens */ private ArrayList<String> getInvalidTokens(String name) { ArrayList<String> invalid = new ArrayList<String>(); if(name.indexOf(" ")>-1) { //$NON-NLS-1$ invalid.add(" "); //$NON-NLS-1$ } String[] suspects = FunctionTool.parserOperators; for(int i = 0; i<suspects.length; i++) { if(name.indexOf(suspects[i])>-1) { invalid.add(suspects[i]); } } return invalid; } /** * Creates an object with a unique name. * * @param obj the object (may be null) * @param proposedName the proposed name * @param confirmChanges true to require user to confirm changes * @return the object */ protected Object createUniqueObject(Object obj, String proposedName, boolean confirmChanges) { // construct a unique name from that proposed if nec proposedName = getValidName(proposedName); if(proposedName==null || proposedName.trim().equals("")) { //$NON-NLS-1$ return null; } String name = proposedName; while(isDisallowedName(obj, proposedName)) { int i = 0; while(isDisallowedName(obj, name)) { i++; name = proposedName+i; } if(!confirmChanges) { break; } Object input = JOptionPane.showInputDialog(this, "\""+proposedName+"\" "+ //$NON-NLS-1$ //$NON-NLS-2$ ToolsRes.getString("FunctionEditor.Dialog.DuplicateName.Message"), //$NON-NLS-1$ ToolsRes.getString("FunctionEditor.Dialog.DuplicateName.Title"), //$NON-NLS-1$ JOptionPane.WARNING_MESSAGE, null, null, name); if(input==null) { return null; } if(input.equals("")||input.equals(name)) { //$NON-NLS-1$ break; } name = proposedName = input.toString(); } String expression = (obj==null) ? "0" : getExpression(obj); //$NON-NLS-1$ return createObject(name, expression, obj); } /** * Class description * */ public class Table extends JTable { public boolean selectOnFocus = true; int rowToSelect, columnToSelect; // constructor Table(TableModel model) { setModel(model); setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); setColumnSelectionAllowed(false); getTableHeader().setReorderingAllowed(false); setGridColor(Color.BLACK); addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { int row = rowAtPoint(e.getPoint()); int col = columnAtPoint(e.getPoint()); table.rowToSelect = row; table.columnToSelect = col; if(!tableModel.isCellEditable(row, col)) { functionPanel.clearSelection(); selectOnFocus = false; } else if(e.getClickCount()==1) { functionPanel.refreshInstructions(FunctionEditor.this, false, col); selectOnFocus = table.hasFocus(); } } public void mouseClicked(MouseEvent e) { if (OSPRuntime.isPopupTrigger(e)) { int col = columnAtPoint(e.getPoint()); if (col!=0) return; int row = rowAtPoint(e.getPoint()); if (tableModel.isCellEditable(row, col)) { String name = (String)table.getValueAt(row, col); Object obj = getObject(name); String desc = getDescription(obj); String message = ToolsRes.getString("FunctionEditor.Dialog.SetDescription.Message"); //$NON-NLS-1$ message += " \""+name+"\""; //$NON-NLS-1$ //$NON-NLS-2$ Object input = JOptionPane.showInputDialog(FunctionEditor.this, message, ToolsRes.getString("FunctionEditor.Dialog.SetDescription.Title"), //$NON-NLS-1$ JOptionPane.PLAIN_MESSAGE, null, null, desc); if (input==null || input.equals(desc)) { return; } desc = input.toString(); setDescription(obj, desc); } } } }); addFocusListener(new FocusAdapter() { public void focusGained(FocusEvent e) { firePropertyChange("focus", null, null); //$NON-NLS-1$ if(getRowCount()==0) { functionPanel.tabToNext(FunctionEditor.this); return; } if(selectOnFocus&&(getRowCount()>0)) { selectCell(rowToSelect, columnToSelect); int col = table.getSelectedColumn(); functionPanel.refreshInstructions(FunctionEditor.this, false, col); } selectOnFocus = true; } public void focusLost(FocusEvent e) { rowToSelect = Math.max(0, getSelectedRow()); columnToSelect = Math.max(0, getSelectedColumn()); } }); // enter key action starts editing InputMap im = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); 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); FunctionEditor.this.tableCellEditor.field.requestFocus(); FunctionEditor.this.tableCellEditor.field.selectAll(); } }; KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); getActionMap().put(im.get(enter), enterAction); // tab key tabs to next editable cell or component KeyStroke tab = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0); Action tabAction = new AbstractAction() { public void actionPerformed(ActionEvent e) { int rowCount = table.getRowCount(); int row = table.rowToSelect; int col = table.columnToSelect; boolean atEnd = ((col==1)&&(row==rowCount-1)); // determine next cell row and column col = (col==0) ? 1 : 0; row = (col==0) ? ((row==getRowCount()-1) ? 0 : row+1) : row; if(table.isEditing()) { table.rowToSelect = row; table.columnToSelect = col; tableCellEditor.stopCellEditing(); } if(atEnd) { functionPanel.tabToNext(FunctionEditor.this); table.clearSelection(); } else { table.requestFocusInWindow(); selectCell(row, col); functionPanel.refreshInstructions(FunctionEditor.this, false, col); } } }; getActionMap().put(im.get(tab), tabAction); } public void selectCell(int row, int col) { // trap for high row numbers if(row==getRowCount()) { row = getRowCount()-1; col = 0; } if(row==-1) { return; } while(!isCellEditable(row, col)) { if(col==0) { col = 1; } else { col = 0; row += 1; } if(row==getRowCount()) { row = 0; } if((row==getSelectedRow())&&(col==getSelectedColumn())) { break; } } table.rowToSelect = row; table.columnToSelect = col; table.changeSelection(row, col, false, false); } // gets the cell editor public TableCellEditor getCellEditor(int row, int column) { return tableCellEditor; } // gets the cell renderer public TableCellRenderer getCellRenderer(int row, int column) { return tableCellRenderer; } public void setFont(Font font) { super.setFont(font); getTableHeader().setFont(font); tableCellRenderer.font = font; tableCellEditor.field.setFont(font); int size = Math.max(font.getSize(), 24); tableCellEditor.popupField.setFont(font.deriveFont((float)size)); setRowHeight(font.getSize()+4); } } /** * The table model. */ protected class TableModel extends AbstractTableModel { boolean settingValue = false; // returns the number of columns in the table public int getColumnCount() { return 2; } // returns the number of rows in the table public int getRowCount() { return objects.size(); } // gets the name of the column public String getColumnName(int col) { return(col==0) ? ToolsRes.getString("FunctionEditor.Table.Column.Name") : //$NON-NLS-1$ ToolsRes.getString("FunctionEditor.Table.Column.Value"); //$NON-NLS-1$ } // gets the value in a cell public Object getValueAt(int row, int col) { Object obj = objects.get(row); String name = getName(obj); if (col==0) return name; String expression = getExpression(obj); try { double value = Double.parseDouble(expression); if (anglesInDegrees && (name.indexOf(THETA)>-1 || name.indexOf(OMEGA)>-1)) { String s = format(value*180/Math.PI, 0.0001); if (name.indexOf(THETA)>-1) s += DEGREES; return s; } return format(value, 0); } catch (NumberFormatException e) { } return expression; } // changes the value of a cell public void setValueAt(Object value, int row, int col) { if(settingValue) { return; } if(value instanceof String) { String val = (String) value; int n = val.indexOf(DEGREES); if (n>-1) val = val.substring(0, n); // get previous state for undoable edit String prev = null; int type = 0; Object obj = objects.get(row); if(col==0) { // name prev = getName(obj); type = NAME_EDIT; settingValue = true; if(!val.equals(prev)) { obj = createUniqueObject(obj, val, true); // name may have changed val = getName(obj); } settingValue = false; if (obj==null || val.equals(prev)) { functionPanel.refreshInstructions(FunctionEditor.this, false, 0); return; } objects.remove(row); objects.add(row, obj); } else { // expression prev = getValueAt(row, col).toString(); type = EXPRESSION_EDIT; if(val.equals(prev)) { functionPanel.refreshInstructions(FunctionEditor.this, false, 1); return; } if(val.equals("")) { //$NON-NLS-1$ val = "0"; //$NON-NLS-1$ } String name = getName(obj); if (anglesInDegrees && (name.indexOf(THETA)>-1 || name.indexOf(OMEGA)>-1)) { try { double d = Double.parseDouble(val); val = String.valueOf(d*Math.PI/180); } catch (NumberFormatException e) { } } obj = createObject(getName(obj), val, obj); objects.remove(row); objects.add(row, obj); } evaluateAll(); table.repaint(); UndoableEdit edit = null; if (undoEditsEnabled) { // create undoable edit edit = getUndoableEdit(type, val, row, col, prev, row, col, getName(obj)); } // inform listeners firePropertyChange("edit", getName(obj), edit); //$NON-NLS-1$ functionPanel.refreshInstructions(FunctionEditor.this, false, col); } } // determines if a cell is editable public boolean isCellEditable(int row, int col) { Object obj = objects.get(row); return((col==0)&&isNameEditable(obj))||((col==1)&&isExpressionEditable(obj)); } } private class CellEditor extends AbstractCellEditor implements TableCellEditor { JPanel panel = new JPanel(new BorderLayout()); JTextField field = new JTextField(); boolean keyPressed = false; boolean mouseClicked = false; JDialog popupEditor; JPanel editorPane; JPanel dragPane; JTextField popupField = new JTextField(); JTextPane variablesPane; JButton revertButton; ValueMouseControl valueMouseController; int minPopupWidth, varBegin, varEnd; Object prevObject; String prevName, prevExpression; // Constructor. CellEditor() { panel.add(field, BorderLayout.CENTER); panel.setOpaque(false); panel.setBorder(BorderFactory.createEmptyBorder(0, 1, 1, 2)); field.setBorder(null); field.setEditable(true); field.setFont(field.getFont().deriveFont(18f)); field.addKeyListener(new KeyAdapter() { public void keyPressed(KeyEvent e) { if(e.getKeyCode()==KeyEvent.VK_ENTER) { keyPressed = true; stopCellEditing(); } else { field.setBackground(Color.yellow); } } }); field.addFocusListener(new FocusAdapter() { public void focusGained(FocusEvent e) { if (usePopupEditor) { stopCellEditing(); undoEditsEnabled = true; } mouseClicked = false; table.clearSelection(); } public void focusLost(FocusEvent e) { if(!mouseClicked) { stopCellEditing(); } if(keyPressed) { keyPressed = false; table.requestFocusInWindow(); } } }); } // Gets the component to be displayed while editing. public Component getTableCellEditorComponent(JTable atable, Object value, boolean isSelected, int row, int column) { table.rowToSelect = row; table.columnToSelect = column; if (usePopupEditor) { undoEditsEnabled = false; JDialog popup = getPopupEditor(); if (functionPanel.functionTool!=null) { // set font level of popup editor int level = functionPanel.functionTool.getFontLevel(); FontSizer.setFonts(popup, level); } dragLabel.setText(ToolsRes.getString("FunctionEditor.DragLabel.Text")); //$NON-NLS-1$ popupField.setText(value.toString()); popupField.requestFocusInWindow(); try { String s = popupField.getText(); setInitialValue(s); } catch (NumberFormatException ex) { } prevObject = objects.get(row); if (prevObject!=null) { prevName = getName(prevObject); prevExpression = getExpression(prevObject); } popupField.selectAll(); if (column==1) { variablesPane.setText(getVariablesString(":\n")); //$NON-NLS-1$ StyledDocument doc = variablesPane.getStyledDocument(); Style blue = doc.getStyle("blue"); //$NON-NLS-1$ doc.setCharacterAttributes(0, variablesPane.getText().length(), blue, false); popup.getContentPane().add(variablesPane, BorderLayout.CENTER); } else { popup.getContentPane().remove(variablesPane); } Rectangle cell = table.getCellRect(row, column, true); minPopupWidth = cell.width+2; Dimension dim = resizePopupEditor(); Point p = table.getLocationOnScreen(); popup.setLocation(p.x+cell.x+cell.width/2-dim.width/2, p.y+cell.y+cell.height/2-dim.height/2); popup.setVisible(true); } else { field.setText(value.toString()); functionPanel.refreshInstructions(FunctionEditor.this, true, column); functionPanel.tableEditorField = field; } return panel; } void setInitialValue(final String stringValue) { Runnable runner = new Runnable() { public void run() { JDialog editor = getPopupEditor(); try { String val=stringValue.replaceAll(",","."); //$NON-NLS-1$ //$NON-NLS-2$ if ("".equals(val)) val = "0"; //$NON-NLS-1$ //$NON-NLS-2$ double value = Double.parseDouble(val); valueMouseController.prevValue = value; popupField.setToolTipText(ToolsRes.getString("FunctionEditor.PopupField.Tooltip")); //$NON-NLS-1$ revertButton.setToolTipText(ToolsRes.getString("FunctionEditor.Button.Revert.Tooltip")); //$NON-NLS-1$ variablesPane.setToolTipText(ToolsRes.getString("FunctionEditor.VariablesPane.Tooltip")); //$NON-NLS-1$ int row = table.rowToSelect; String tooltip = ToolsRes.getString("FunctionEditor.DragLabel.Tooltip"); //$NON-NLS-1$ dragLabel.setToolTipText(tooltip); String name = (String)table.getValueAt(row, 0); if (!name.equals("t")) { //$NON-NLS-1$ editor.getContentPane().add(dragPane, BorderLayout.SOUTH); } else { editor.getContentPane().remove(dragPane); } } catch (NumberFormatException e) { editor.getContentPane().remove(dragPane); } editor.pack(); } }; if(SwingUtilities.isEventDispatchThread()) { runner.run(); } else { SwingUtilities.invokeLater(runner); } } // Determines when editing starts. public boolean isCellEditable(EventObject e) { if(e instanceof MouseEvent) { firePropertyChange("focus", null, null); //$NON-NLS-1$ MouseEvent me = (MouseEvent) e; if(me.getClickCount()==2) { mouseClicked = true; Runnable runner = new Runnable() { public synchronized void run() { field.selectAll(); } }; SwingUtilities.invokeLater(runner); return true; } } else if((e==null)||(e instanceof ActionEvent)) { return true; } return false; } // Called when editing is completed. public Object getCellEditorValue() { if (usePopupEditor) { popupField.setBackground(Color.WHITE); } field.setBackground(Color.WHITE); // revalidate table to keep cell widths correct (workaround) Runnable runner = new Runnable() { public synchronized void run() { table.revalidate(); } }; SwingUtilities.invokeLater(runner); return field.getText(); } // Resizes the popup editor and returns the new size private Dimension resizePopupEditor() { String s = popupField.getText(); Font font = popupField.getFont(); Rectangle rect = font.getStringBounds(s, frc).getBounds(); int h = rect.height; int w = Math.max(minPopupWidth, rect.width+32); if (table.columnToSelect==1) { s = variablesPane.getText(); int n = s.indexOf("\n"); //$NON-NLS-1$ s = s.substring(n+1); font = variablesPane.getFont().deriveFont(Font.BOLD); rect = font.getStringBounds(s, frc).getBounds(); w = Math.max(w, rect.width); } Dimension dim = new Dimension(w, h); editorPane.setPreferredSize(dim); popupEditor.pack(); dim.width = popupEditor.getWidth(); return dim; } // Gets the popup editor private JDialog getPopupEditor() { if (popupEditor==null) { popupField.setEditable(true); Font font = popupField.getFont().deriveFont(24f); int level = functionPanel.functionTool.getFontLevel(); font = FontSizer.getResizedFont(font, level); popupField.setFont(font); popupField.addKeyListener(new KeyAdapter() { public void keyPressed(KeyEvent e) { if(e.getKeyCode()==KeyEvent.VK_ENTER) { String text = popupField.getText(); // restore previous name and expression for undoable edit // in case they were changed with mouse int row = table.rowToSelect; objects.remove(row); objects.add(row, prevObject); if (table.columnToSelect==1) { // setExpression(prevName, prevExpression, false); table.setValueAt(prevExpression, row, 1); } // be sure editor field has correct text field.setText(text); undoEditsEnabled = true; keyPressed = true; popupEditor.setVisible(false); if (table.columnToSelect==1) { // explicitly set value in case focus listener not triggered! table.setValueAt(text, row, 1); } field.requestFocusInWindow(); // triggers call to stopCellEditing() field.selectAll(); } else { popupField.setBackground(Color.yellow); resizePopupEditor(); } } public void keyReleased(KeyEvent e) { if(e.getKeyCode()==KeyEvent.VK_ENTER) return; setInitialValue(popupField.getText()); } }); // create variables pane variablesPane = new JTextPane() { public void paintComponent(Graphics g) { if(OSPRuntime.antiAliasText) { Graphics2D g2 = (Graphics2D) g; RenderingHints rh = g2.getRenderingHints(); rh.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); rh.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } super.paintComponent(g); } }; variablesPane.setEditable(false); variablesPane.setFocusable(false); variablesPane.setBorder(popupField.getBorder()); font = popupField.getFont().deriveFont(14f); font = FontSizer.getResizedFont(font, level); variablesPane.setFont(font); StyledDocument doc = variablesPane.getStyledDocument(); Style def = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE); StyleConstants.setFontFamily(def, "SansSerif"); //$NON-NLS-1$ Style blue = doc.addStyle("blue", def); //$NON-NLS-1$ StyleConstants.setBold(blue, false); StyleConstants.setForeground(blue, Color.blue); Style red = doc.addStyle("red", blue); //$NON-NLS-1$ StyleConstants.setBold(red, true); StyleConstants.setForeground(red, Color.red); varBegin = varEnd = 0; variablesPane.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { if(varEnd==0) { return; } variablesPane.setCaretPosition(varBegin); variablesPane.moveCaretPosition(varEnd); popupField.replaceSelection(variablesPane.getSelectedText()); popupField.setBackground(Color.yellow); setInitialValue(popupField.getText()); } public void mouseExited(MouseEvent e) { StyledDocument doc = variablesPane.getStyledDocument(); Style blue = doc.getStyle("blue"); //$NON-NLS-1$ doc.setCharacterAttributes(0, variablesPane.getText().length(), blue, false); varBegin = varEnd = 0; } }); variablesPane.addMouseMotionListener(new MouseMotionAdapter() { public void mouseMoved(MouseEvent e) { varBegin = varEnd = 0; // select and highlight the variable under mouse String text = variablesPane.getText(); // first separate the instructions from the variables int startVars = text.indexOf(":\n"); //$NON-NLS-1$ if(startVars==-1) { return; } startVars += 2; String vars = text.substring(startVars); StyledDocument doc = variablesPane.getStyledDocument(); Style blue = doc.getStyle("blue"); //$NON-NLS-1$ Style red = doc.getStyle("red"); //$NON-NLS-1$ int beginVar = variablesPane.viewToModel(e.getPoint())-startVars; if(beginVar<0) { // mouse is over instructions doc.setCharacterAttributes(0, text.length(), blue, false); return; } while (beginVar>0) { // back up to preceding space String s = vars.substring(0, beginVar); if (s.endsWith(" ")) //$NON-NLS-1$ break; beginVar--; } varBegin = beginVar+startVars; // find following comma, space or end String s = vars.substring(beginVar); int len = s.indexOf(","); //$NON-NLS-1$ if(len==-1) len = s.indexOf(" "); //$NON-NLS-1$ if(len==-1) len = s.length(); // set variable bounds and character style varEnd = varBegin+len; doc.setCharacterAttributes(0, varBegin, blue, false); doc.setCharacterAttributes(varBegin, len, red, false); doc.setCharacterAttributes(varEnd, text.length()-varEnd, blue, false); } }); // create drag pane dragPane = new JPanel(new BorderLayout()); dragPane.setBackground(new Color(240, 255, 240)); // very light green dragLabel = new JLabel(); dragLabel.setHorizontalAlignment(JLabel.CENTER); Border line = BorderFactory.createLineBorder(LIGHT_BLUE); Border space = BorderFactory.createEmptyBorder(2, 3, 2, 3); dragLabel.setBorder(BorderFactory.createCompoundBorder(line, space)); font = popupField.getFont().deriveFont(12f); font = FontSizer.getResizedFont(font, level); dragLabel.setFont(font); dragLabel.setForeground(Color.green.darker().darker()); dragPane.add(dragLabel, BorderLayout.CENTER); valueMouseController = new ValueMouseControl(tableCellEditor); dragLabel.addMouseListener(valueMouseController); dragLabel.addMouseMotionListener(valueMouseController); // make revert button String imageFile = "/org/opensourcephysics/resources/tools/images/close.gif"; //$NON-NLS-1$ Icon icon = ResourceLoader.getIcon(imageFile); revertButton = new JButton(icon); line = BorderFactory.createLineBorder(Color.LIGHT_GRAY); space = BorderFactory.createEmptyBorder(0,2,0,2); revertButton.setBorder(BorderFactory.createCompoundBorder(line, space)); revertButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (prevObject!=null) { // restore original value if (table.columnToSelect==1) { table.setValueAt(prevExpression, table.rowToSelect, 1); field.setText(prevExpression); } else { table.setValueAt(prevName, table.rowToSelect, 0); field.setText(prevName); } // objects.remove(row); // objects.add(row, prevObject); // if (table.getSelectedColumn()==1) { // setExpression(prevName, prevExpression, false); // field.setText(prevExpression); // } stopCellEditing(); undoEditsEnabled = true; } popupField.setBackground(Color.WHITE); popupEditor.setVisible(false); } }); // assemble components Frame frame = JOptionPane.getFrameForComponent(FunctionEditor.this); popupEditor = new JDialog(frame, true); popupEditor.setUndecorated(true); popupEditor.getRootPane().setWindowDecorationStyle(JRootPane.NONE); JPanel contentPane = new JPanel(new BorderLayout()); popupEditor.setContentPane(contentPane); editorPane = new JPanel(new BorderLayout()); editorPane.setBackground(Color.WHITE); editorPane.add(popupField, BorderLayout.CENTER); editorPane.add(revertButton, BorderLayout.EAST); contentPane.add(editorPane, BorderLayout.NORTH); // variablesPane and dragPane are added/removed from contentPane as needed } return popupEditor; } } private class CellRenderer extends DefaultTableCellRenderer { Font font = new JTextField().getFont(); /** * Constructor CellRenderer */ public CellRenderer() { super(); setOpaque(true); // make background visible setFont(font); setHorizontalAlignment(SwingConstants.LEFT); setBorder(BorderFactory.createEmptyBorder(2, 1, 2, 2)); } // Returns a label for the specified cell. public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col) { String val = value.toString(); if (col==0 && (FunctionEditor.this instanceof UserFunctionEditor)) { val = FitBuilder.localize(val); } setText(val); Object obj = objects.get(row); String tooltip = getTooltip(obj); String tooltipText = (col==0 && tooltip!=null)? tooltip: (col==0)? ToolsRes.getString("FunctionEditor.Table.Cell.Name.Tooltip"): //$NON-NLS-1$ ToolsRes.getString("FunctionEditor.Table.Cell.Value.Tooltip"); //$NON-NLS-1$ if (tooltip==null && col==0) { tooltipText += " ("+ToolsRes.getString("FunctionEditor.Tooltip.HowToEdit")+")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } setToolTipText(tooltipText); if((col==1)&&circularErrors.contains(obj)) { setToolTipText(ToolsRes.getString("FunctionEditor.Table.Cell.CircularErrors.Tooltip")); //$NON-NLS-1$ setForeground(DARK_RED); if(isSelected) { setBackground(MEDIUM_RED); } else { setBackground(LIGHT_RED); } } else if((col==1)&&isInvalidExpression(obj)) { // error condition setToolTipText(ToolsRes.getString("FunctionEditor.Table.Cell.Invalid.Tooltip")); //$NON-NLS-1$ setForeground(DARK_RED); if(isSelected) { setBackground(MEDIUM_RED); } else { setBackground(LIGHT_RED); } } else if(((col==0)&&!isNameEditable(obj))||((col==1)&&!isExpressionEditable(obj))) { // uneditable cell setForeground(Color.BLACK); setBackground(LIGHT_GRAY); } else { if(isSelected) { // selected editable cell setForeground(hasFocus ? Color.BLUE : Color.BLACK); setBackground(LIGHT_BLUE); } else { // unselected editable cell setForeground(Color.BLACK); setBackground(Color.WHITE); } } setFont(((col==0)&&isImportant(obj)) ? font.deriveFont(Font.BOLD) : font); refreshButtons(); return this; } } /** * A MouseAdapter to change numerical values in a CellEditor by dragging a mouse. */ private class ValueMouseControl extends MouseAdapter { CellEditor cellEditor; double prevValue, newValue; Point startingPoint; int logDelta; // log (base 10) of step size delta /** * Constructor. * @param editor the CellEditor */ private ValueMouseControl(CellEditor editor) { cellEditor = editor; } /** * Determines logDelta from a String value and relative level. * Relative level is a number from 0 to 1 (eg relative mouse position within a component). * Strings may be in decimal (eg 0.00) or scientific (eg 0.000E0) format. * * @param val the value string * @param relativeX number from 0-1 (higher relativeX ==> farther right in string digits) * @return the log base 10 of the step size delta */ int getLogDelta(String val, double relativeX) { if ("".equals(val)) return 0; //$NON-NLS-1$ int digits = val.length(); int powerOfTen = 0; // handle decimal point int decimal = val.replaceAll(",",".").indexOf("."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ if (decimal>-1) { digits--; } // handle minus sign int minus = val.indexOf("-"); //$NON-NLS-1$ if (minus>-1) { digits--; decimal--; } // handle sci format int exp = val.indexOf("E"); //$NON-NLS-1$ if (exp>-1) { String exponent = val.substring(exp+1); digits -= exponent.length()+1; powerOfTen = Integer.parseInt(exponent); } // determine power of ten int selectableDigits = exp>-1? digits: digits+1; int selectedDigit = (int)Math.floor(relativeX*selectableDigits); int integerDigits = decimal>-1? decimal: digits; powerOfTen += integerDigits-selectedDigit-1; return powerOfTen; } /** * Determines the text selection start index for a String. * Strings may be in decimal (eg 0.00) or scientific (eg 0.000E0) format. * * @param val the value string * @return the text selection start index */ int getSelectionIndex(String val) { int powerOfTen = logDelta; int digits = val.length(); int offset = 0; // handle decimal point int decimal = val.replaceAll(",",".").indexOf("."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ if (decimal>-1) { digits--; } // handle minus sign int minus = val.indexOf("-"); //$NON-NLS-1$ if (minus>-1) { digits--; offset = 1; decimal--; } // handle sci format int exp = val.indexOf("E"); //$NON-NLS-1$ if (exp>-1) { String exponent = val.substring(exp+1); digits -= exponent.length()+1; powerOfTen -= Integer.parseInt(exponent); } int integerDigits = decimal>-1? decimal: digits; int selectedDigit = integerDigits-powerOfTen-1; offset += decimal>-1 && selectedDigit>=decimal? 1: 0; int selectionIndex = selectedDigit+offset; return selectionIndex; } @Override public void mousePressed(MouseEvent e) { int row = table.rowToSelect; String name = (String)table.getValueAt(row, 0); // return if this control is not suitable for a variable (eg time) if (name.equals("t")) { //$NON-NLS-1$ startingPoint = null; return; } startingPoint = e.getPoint(); newValue = prevValue; double level = 1.0*startingPoint.x/cellEditor.dragPane.getWidth(); logDelta = getLogDelta(cellEditor.popupField.getText(), level); } @Override public void mouseReleased(MouseEvent e) { if (startingPoint==null) return; prevValue = newValue; startingPoint = null; // deselect text String val = cellEditor.popupField.getText(); cellEditor.popupField.select(val.length(), val.length()); } @Override public void mouseDragged(MouseEvent e) { if (startingPoint==null || Double.isNaN(prevValue)) return; // use shift key accelerator int pixelsPerStep = e.isShiftDown()? 1: 10; int d = (e.getPoint().x-startingPoint.x)/pixelsPerStep; // change value in steps of delta double delta = Math.pow(10, logDelta); newValue = prevValue+d*delta; String s = format(newValue, 0); int row = table.rowToSelect; table.setValueAt(s, row, 1); cellEditor.popupField.setText(s); cellEditor.popupField.setBackground(Color.yellow); cellEditor.popupField.requestFocusInWindow(); String val = cellEditor.popupField.getText(); int index = getSelectionIndex(val); cellEditor.popupField.select(index, index+1); } @Override public void mouseMoved(MouseEvent e) { double level = 1.0*e.getPoint().x/cellEditor.dragPane.getWidth(); String val = cellEditor.popupField.getText(); logDelta = getLogDelta(val, level); int index = getSelectionIndex(val); cellEditor.popupField.select(index, index+1); } @Override public void mouseEntered(MouseEvent e) { cellEditor.dragPane.setCursor(Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR)); } @Override public void mouseExited(MouseEvent e) { cellEditor.dragPane.setCursor(Cursor.getDefaultCursor()); } } /** * A class to undo/redo edits. */ protected class DefaultEdit extends AbstractUndoableEdit { Object redoObj, undoObj; int redoRow, redoCol, undoRow, undoCol, editType; String name; /** * A class to undo/redo edits. * * @param type may be ADD_EDIT, REMOVE_EDIT, NAME_EDIT, or EXPRESSION_EDIT * @param newVal the new object, name or expression * @param newRow the row selected * @param newCol the col selected * @param prevVal the previous object, name or expression * @param prevRow the previous row selected * @param prevCol the previous col selected * @param name the name of the edited object */ public DefaultEdit(int type, Object newVal, int newRow, int newCol, Object prevVal, int prevRow, int prevCol, String name) { editType = type; redoObj = newVal; undoObj = prevVal; redoRow = newRow; redoCol = newCol; undoRow = prevRow; undoCol = prevCol; this.name = name; OSPLog.finer(editTypes[type]+": \""+name+"\"" //$NON-NLS-1$ //$NON-NLS-2$ +"\nold value: "+prevVal //$NON-NLS-1$ +"\nnew value: "+newVal); //$NON-NLS-1$ } // undoes the change public void undo() throws CannotUndoException { super.undo(); undoEditsEnabled = false; switch(editType) { case ADD_EDIT : { removeObject(undoObj, false); break; } case REMOVE_EDIT : { addObject(undoObj, undoRow, false, true); break; } case NAME_EDIT : { Object obj = objects.get(undoRow); String expression = getExpression(obj); name = undoObj.toString(); String prevName = redoObj.toString(); obj = createObject(name, expression, obj); objects.remove(undoRow); objects.add(undoRow, obj); evaluateAll(); firePropertyChange("edit", name, prevName); //$NON-NLS-1$ break; } case EXPRESSION_EDIT : { Object obj = objects.get(undoRow); Object[] undoArray = (Object[]) undoObj; // array is {expression, buttons} obj = createObject(name, undoArray[0].toString(), obj); objects.remove(undoRow); objects.add(undoRow, obj); evaluateAll(); firePropertyChange("edit", name, null); //$NON-NLS-1$ } } // select cell and request keyboard focus table.rowToSelect = undoRow; table.columnToSelect = undoCol; getTable().selectOnFocus = true; getTable().requestFocusInWindow(); refreshGUI(); undoEditsEnabled = true; } // redoes the change public void redo() throws CannotUndoException { super.redo(); undoEditsEnabled = false; switch(editType) { case ADD_EDIT : { addObject(redoObj, redoRow, false, true); break; } case REMOVE_EDIT : { removeObject(redoObj, false); break; } case NAME_EDIT : { Object obj = objects.get(redoRow); String expression = getExpression(obj); name = redoObj.toString(); String prevName = undoObj.toString(); obj = createObject(name, expression, obj); objects.remove(redoRow); objects.add(redoRow, obj); evaluateAll(); firePropertyChange("edit", name, prevName); //$NON-NLS-1$ break; } case EXPRESSION_EDIT : { Object obj = objects.get(redoRow); Object[] redoArray = (Object[]) redoObj; // array is {expression, buttons} ArrayList<?> buttons = (ArrayList<?>) redoArray[1]; for(Object next : buttons) { AbstractButton b = (AbstractButton) next; if(!b.isSelected()) { b.doClick(0); } } obj = createObject(name, redoArray[0].toString(), obj); objects.remove(redoRow); objects.add(redoRow, obj); evaluateAll(); firePropertyChange("edit", name, null); //$NON-NLS-1$ } } // select cell and request keyboard focus table.rowToSelect = redoRow; table.columnToSelect = redoCol; getTable().selectOnFocus = true; getTable().requestFocusInWindow(); refreshGUI(); undoEditsEnabled = true; } // returns the presentation name public String getPresentationName() { if(editType==REMOVE_EDIT) { return "Deletion"; //$NON-NLS-1$ } return "Edit"; //$NON-NLS-1$ } } /** * Formats a number. * * @param value the number * @param zeroLevel the level below which the value is considered zero * @return the formatted string */ public static String format(double value, double zeroLevel) { if(Math.abs(value)<zeroLevel) { value = 0; } int rounded = (int) Math.round(value); if(Math.abs(value-rounded)<zeroLevel) { value = rounded; } double absVal = Math.abs(value); boolean scientific = ((absVal<0.01)&&(value!=0))||(absVal>=1000); String s = scientific ? sciFormat.format(value) : decimalFormat.format(value); // eliminate trailing "0000x" and "9999x" int n = s.indexOf("E"); // exponential symbol //$NON-NLS-1$ String tail = (n>-1) ? s.substring(n) : ""; //$NON-NLS-1$ s = (n>-1) ? s.substring(0, n) : s; n = s.indexOf("0000"); //$NON-NLS-1$ if(n>1) { s = s.substring(0, n+1); // leave one trailing zero } n = s.indexOf("9999"); //$NON-NLS-1$ if(n>1) { s = s.substring(0, n); DecimalFormatSymbols symbols = sciFormat.getDecimalFormatSymbols(); char separator = symbols.getDecimalSeparator(); int m = s.indexOf(separator); if(m==s.length()-1) { int i = Integer.parseInt(s.substring(0, m))+1; s = Integer.toString(i)+separator+"0"; //$NON-NLS-1$ } else { int i = Integer.parseInt(s.substring(n-1))+1; s = s.substring(0, n-1)+i; } } return s+tail; } /** * Rounds a number. * * @param value the number * @param sigfigs the number of significant figures in the rounded value * @return the rounded value */ public static double round(double value, int sigfigs) { if (value==0) return value; int multiplier = value<0? -1: +1; value = Math.abs(value); // increase or decrease value by factors of 10 to get sigfigs double limit = Math.pow(10, sigfigs-1); int power = 0; while (value<limit) { value *= 10; power++; } while (value>10*limit) { value /= 10; power--; } value = Math.round(value); return multiplier*value/Math.pow(10, power); } } /* * 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 */