package photoSpreadObjects.photoSpreadComponents; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.EOFException; import java.io.Externalizable; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.KeyStroke; import javax.swing.event.ChangeEvent; import javax.swing.table.AbstractTableModel; import javax.swing.table.TableRowSorter; import photoSpread.PhotoSpread; import photoSpread.PhotoSpreadException; import photoSpread.PhotoSpreadException.IllegalArgumentException; import photoSpread.PhotoSpreadException.KeyBindingsFileSyntaxError; import photoSpreadLoaders.XMLFileFilter; import photoSpreadTable.PhotoSpreadCell; import photoSpreadTable.PhotoSpreadDragDropManager; import photoSpreadTable.PhotoSpreadTable; import photoSpreadTable.PhotoSpreadTableModel; import photoSpreadUtilities.CellCoordinates; import photoSpreadUtilities.ChangeManager; import photoSpreadUtilities.ComputableDimension; import photoSpreadUtilities.Const; import photoSpreadUtilities.Misc; import photoSpreadUtilities.PhotoSpreadContextMenu; import photoSpreadUtilities.Misc.ShowHelpAction; import photoSpreadUtilities.Misc.WindowCloseAction; /** * @author paepcke * */ public class KeyBindEditor extends JFrame { private static final long serialVersionUID = 1L; private static final int NUM_OF_TABLE_ROWS = 25; private static final int KEY_SEQUENCE_COLUMN = 0; private static final int CELL_ADDRESS_COLUMN = 1; private static final int ACTION_BUTTONS_HGAP = 3; private static final String BINDINGS_OPEN_TAG = "<bindings>\n"; private static final String DND_BINDING_OPEN_TAG= " <dndBindingSpec>\n"; private static final String KEY_SPEC_OPEN_TAG = " <keySpec>"; private static final String CELL_SPEC_OPEN_TAG = " <cellSpec>"; private static final String BINDINGS_CLOSE_TAG = "</bindings>"; private static final String DND_BINDING_CLOSE_TAG = " </dndBindingSpec>\n"; private static final String KEY_SPEC_CLOSE_TAG = "</keySpec>\n"; private static final String CELL_SPEC_CLOSE_TAG = "</cellSpec>\n"; // The following instance var is required to // make the context menu work, even though it's // never accessed: JPopupMenu _popup; ChangeManager _changeManager; // Name of this key bindings editor instance. // Invented and used for the ChangeManager // interaction. String _editorName = "BindingsEditor"; JPanel _panel; KeyBindingsTable _table; DataTableModel _tableModel; PhotoSpreadTable _photoSpreadSheet = null; TableRowSorter<DataTableModel> _metadataSorter = null; PhotoSpreadContextMenu _contextMenu; JScrollPane _scrollPane; Dimension _workspacePanelSize; KeyBindEditor _thisWindow = null; /**************************************************** * Constructor(s) *****************************************************/ public KeyBindEditor(PhotoSpreadTable photoSpreadTable) { _photoSpreadSheet = photoSpreadTable; this.addWindowListener(new KeyBindEditorWindowListener()); this.setTitle("PhotoSpread Key Bindings Editor"); _thisWindow = this; _thisWindow.setLocationRelativeTo(PhotoSpread.getCurrentSheetWindow()); _thisWindow.setLocationRelativeTo(_photoSpreadSheet); _panel = new JPanel(); _panel.setBackground(Color.DARK_GRAY); // Safety: catch the 'close window operation' in // an event handler to warn about uncommitted changes. this.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); _scrollPane = new JScrollPane(_panel); _scrollPane .setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); _scrollPane .setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); this.add(_scrollPane); WindowCloseAction winCloseAction = new Misc().new WindowCloseAction(this); Misc.bindKey(this, "control W", winCloseAction); Misc.bindKey(this, "F1", new ShowHelpAction( "To do in Key Bindings Editor Window", "HelpFiles/keyboardShortcutsHelp.html", this)); initEditor(); pack(); setVisible(true); } public KeyBindEditor(PhotoSpreadTable photoSpreadTable, File xmlBindingsFile) throws IOException { this(photoSpreadTable); loadBindings(xmlBindingsFile); } /**************************************************** * KeyStrokeSpec Inner Class *****************************************************/ private class KeyStrokeSpec implements Externalizable { private static final long serialVersionUID = 1L; KeyStroke _keyStroke = null; String _keySpec = ""; public KeyStrokeSpec (String spec) throws IllegalArgumentException { if (spec == null) return; if (spec.isEmpty()) return; // Ensure correctness of spec, and assign a // normalized spec string to _keySpec: _keyStroke = validateKeystrokeSpec(spec); if (_keyStroke == null) throw new PhotoSpreadException.IllegalArgumentException(""); } public boolean isEmpty () { return (_keySpec.isEmpty()); } public String toString () { return _keySpec; } public KeyStroke getKeyStroke () { return _keyStroke; } /** * Given a Java KeyStroke string specification, * return a corresponding KeyStroke instance * if the spec is correct. Else throw * up a dialog to tell user; then return false. * * As a side effect: assign to instance var _keySpec * a normalized version of the passed-in spec. Most * importantly, this means to capitalize the character. * Ex: 'control a' is turned into 'control A'. Without * this change the binding will not work later on. * * @param keyStrokeSpec See getKeyStroke() of class KeyStroke. Examples: * "control shift a", "alt shift X" * @return true if the string successfully generates a KeyStroke instance. False otherwise. */ public KeyStroke validateKeystrokeSpec(String keyStrokeSpec) { String modifiers = ""; String theChar = ""; // Given something like "control shift x" we need to generate: // KeyStroke.getKeyStroke("control shift typed x"). I.e. we must // splice the word 'typed' before the letter: keyStrokeSpec.trim(); int charSpecStart = keyStrokeSpec.lastIndexOf(KeyEvent.VK_SPACE); if (charSpecStart > 0) { modifiers = keyStrokeSpec.substring(0, charSpecStart); theChar = keyStrokeSpec.substring(charSpecStart); } else theChar = keyStrokeSpec; theChar = theChar.toUpperCase(); KeyStroke testStroke = KeyStroke.getKeyStroke( modifiers + " typed " + theChar); if (testStroke == null) { Misc.showErrorMsg( "Incorrect keystroke specification '" + keyStrokeSpec + "' . Examples: " + " 'control shift a'; 'alt X'; 'b' ", _thisWindow); return null; } _keySpec = modifiers.trim() + " " + theChar; return testStroke; } /* (non-Javadoc) * @see java.io.Externalizable#readExternal(java.io.ObjectInput) */ @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { _keySpec = in.readLine(); _keySpec = Misc.trim(_keySpec, "<keySpec>"); _keySpec = Misc.trim(_keySpec, "</keySpec>"); _keyStroke = validateKeystrokeSpec(_keySpec); } /* (non-Javadoc) * @see java.io.Externalizable#writeExternal(java.io.ObjectOutput) * * Note: ObjectOutput is an interface that is implemented, * for instance, by ObjectOutputStream. */ @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeBytes("<keySpec>" + _keySpec + "</keySpec>"); out.writeBytes("\n"); } } /**************************************************** * CellSpec Inner Class *****************************************************/ private class CellSpec implements Externalizable { private static final long serialVersionUID = 1L; CellCoordinates _cellCoords = null; String _cellSpec = ""; // Don't write the PhotoSpreadCell out during serialization; // we'll reconstruct it upon reading in: transient PhotoSpreadCell _cell = null; public CellSpec (String spec) throws IllegalArgumentException { if (spec == null) return; if (spec.isEmpty()) return; _cell = validateCellSpec(spec); if (_cell == null) throw new PhotoSpreadException.IllegalArgumentException(""); } public boolean isEmpty () { return (_cellSpec.isEmpty()); } public PhotoSpreadCell getCell () { return _cell; } public CellCoordinates getCellCoordinates () { return _cellCoords; } public String toString () { return _cellSpec; } /** * Given an Excel column name specification, * return true if the spec is correct. Else throw * up a dialog to tell user; then return false. * * As a side effect, assign a normalized version * of the passed-in cell address to the instance * variable _cellSpec. The normalized version is * space-trimmed and capitalized. * * @param cellSpec Examples: "C1", "AB390" * @return true if the string successfully identifies a PhotoSpreadCell instance. False otherwise. */ public PhotoSpreadCell validateCellSpec(String cellSpec) { PhotoSpreadCell cell = null; cellSpec.trim(); cellSpec = cellSpec.toUpperCase(); CellCoordinates cellCoords = Misc.getCellAddress(cellSpec); if (cellCoords != null) { cell = _photoSpreadSheet.getCell(cellCoords); } if ((cellCoords == null) || (cell == null)) { Misc.showErrorMsg( "Incorrect destination cell specification '" + cellSpec + "' . Must specify an *existing* cell. Examples: " + " 'C3'; 'Ab243' (assuming you have that many cells)", _thisWindow); return null; } _cellSpec = cellSpec; return cell; } // Take care of serialization: /* private void writeObject (ObjectOutputStream out) throws IOException { // Nothing special to do on output: the photospread cell won't be written: out.defaultWriteObject(); } private void readObject (ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); validateCellSpec(_cellSpec); } */ /* (non-Javadoc) * @see java.io.Externalizable#readExternal(java.io.ObjectInput) */ @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { _cellSpec = in.readLine(); _cellSpec = Misc.trim(_cellSpec, "<cellSpec>"); _cellSpec = Misc.trim(_cellSpec, "</cellSpec>"); _cell = validateCellSpec(_cellSpec); } /* (non-Javadoc) * @see java.io.Externalizable#writeExternal(java.io.ObjectOutput) */ @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeBytes("<cellSpec>" + _cellSpec + "</cellSpec>"); out.writeBytes("\n"); } } /**************************************************** * DnDKeyBindSpec Inner Class *****************************************************/ private class DnDKeyBindSpec { KeyStroke _keyStroke = null; KeyStrokeSpec _keySpec = null; CellSpec _cellSpec = null; public DnDKeyBindSpec (KeyStrokeSpec keySpec, CellSpec cellSpec) { _keySpec = keySpec; _keyStroke = keySpec.getKeyStroke(); _cellSpec = cellSpec; } public DnDKeyBindSpec (String keySpec, String cellSpec) throws IllegalArgumentException { setBindingSpec(keySpec, cellSpec); } public boolean isEmpty () { return (_keySpec.isEmpty() && _cellSpec.isEmpty()); } public KeyStroke getKeyStroke () { return _keyStroke; } public CellCoordinates getDestinationCellCoordinates () { if (_cellSpec == null) return null; return _cellSpec.getCellCoordinates(); } public KeyStrokeSpec getKeySpec() { return _keySpec; } public CellSpec getCellSpec () { return _cellSpec; } public PhotoSpreadCell getDestinationCell () { return _photoSpreadSheet.getCell(getDestinationCellCoordinates()); } public void setBindingSpec (String keySpec, String cellSpec) throws IllegalArgumentException { _keySpec = new KeyStrokeSpec(keySpec); _keyStroke = _keySpec.getKeyStroke(); _cellSpec = new CellSpec(cellSpec); } public void setKeySpec (String keySpec) throws IllegalArgumentException { set (new KeyStrokeSpec(keySpec)); } public void setCellSpec (String cellSpec) throws IllegalArgumentException { set (new CellSpec(cellSpec)); } public void set (KeyStrokeSpec keySpec) { _keySpec = keySpec; _keyStroke = _keySpec.getKeyStroke(); } public void set (CellSpec cellSpec) { _cellSpec= cellSpec; } public void writeExternal(ObjectOutput out) throws IOException { out.writeBytes("<dndKeyBindSpec>\n"); _keySpec.writeExternal(out); _cellSpec.writeExternal(out); out.writeBytes("</dndKeyBindSpec>\n"); } } /**************************************************** * KeyBindEditorWindowListener Inner Class *****************************************************/ class KeyBindEditorWindowListener extends WindowAdapter { public void windowClosing(WindowEvent e) { if (JOptionPane.showConfirmDialog(_thisWindow, // make dialog appear within the editor window, // not at screen center. "Discard changes?", "Confirm", // Title JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { // Unregister from the ChangeManager: _tableModel.dispose(); dispose(); } } public void windowDeiconified(WindowEvent e) { } } /**************************************************** * Listeners: KeyBindKeyListener Inner Class *****************************************************/ class KeyBindKeyListener extends KeyAdapter { public void keyPressed(KeyEvent e) { if (e.getID() == KeyEvent.KEY_PRESSED) { // Get indices of first and last currently selected rows: int firstSelectedRow = _table.getSelectionModel() .getAnchorSelectionIndex(); int lastSelectedRow = _table.getSelectionModel() .getLeadSelectionIndex(); int currRow = getRow(); char typedChar = e.getKeyChar(); switch (typedChar) { case KeyEvent.VK_DELETE: // Main or Numpad delete key // NumPad Delete key? if (e.getKeyLocation() == KeyEvent.KEY_LOCATION_NUMPAD) _tableModel.removeRow(firstSelectedRow, lastSelectedRow); else { // Main keyboard Delete key: // Clear current row (i.e. make it empty; leave row in // place): _tableModel.clearRow(firstSelectedRow, lastSelectedRow); // Put edit focus to first of the cleared rows: int selectionUpMove = lastSelectedRow - firstSelectedRow; for (; selectionUpMove > 0; selectionUpMove--) _table.transferFocusBackward(); } // NOTE1: This next statement ensures that // repainting of the table is completed // correctly. Without it, the cell with // focus will retain its old value (Swing // bug as far as I'm concerned!): _table.editingStopped(new ChangeEvent(this)); break; case KeyEvent.VK_ENTER: // If we don't consume this key event, the default // JTable behavior interferes: e.consume(); // _table.getSelectionModel().clearSelection(); if (currRow >= _tableModel.getRowCount() - 1) { _tableModel.appendRow(); _table.editingStopped(new ChangeEvent(this)); } int selectedRowNum = _table.getSelectedRow(); int selectedColNum = _table.getSelectedColumn(); if (selectedColNum >= _table.getColumnCount() - 1) _table.changeSelection( selectedRowNum + 1, // Select next row, ... 0, // ... column 0 Const.DONT_TOGGLE_SELECTION, // Set selection there, don't toggle it Const.DONT_EXTEND_SELECTION); // Clear the previous selection else _table.changeSelection( currRow, // Select same row, ... selectedColNum + 1, // ... next column Const.DONT_TOGGLE_SELECTION, // Set selection there, don't toggle it Const.DONT_EXTEND_SELECTION); // Clear the previous selection _table.editCellAt(_table.getSelectedRow(), _table.getSelectedColumn()); break; case KeyEvent.VK_INSERT: _tableModel.addRowBefore(currRow); // see NOTE1 above on need for following statement: _table.editingStopped(new ChangeEvent(this)); default: break; } } } public void keyTyped(KeyEvent e) { if (e.getID() == KeyEvent.KEY_TYPED) { char typedChar = e.getKeyChar(); switch (typedChar) { // This branch, intended for cnt-s, unfortunately never // gets control: case KeyEvent.VK_S: if (e.getModifiersEx() == KeyEvent.VK_CONTROL) return; break; default: break; } } } private int getRow() { // Which row is in focus? int currRow = _table.getEditingRow(); if (currRow == -1) currRow = _table.getSelectedRow(); if (currRow == -1) currRow = 0; return currRow; } } // end class ObjectPanelKeyListener /**************************************************** * Action Listener: SaveButtonListener Inner Class *****************************************************/ class SaveButtonListener implements ActionListener { KeyBindEditor _theEditor = null; public SaveButtonListener(KeyBindEditor theEditor) { _theEditor = theEditor; } public void actionPerformed(ActionEvent e) { File exportFile = null; // Misc.showErrorMsg("Saving of key bindings not yet implemented.", _thisWindow); String priorWriteDir = PhotoSpread.photoSpreadPrefs.getProperty(PhotoSpread.lastDirWrittenKey); final JFileChooser fc = new JFileChooser(priorWriteDir); XMLFileFilter filter = new XMLFileFilter(); fc.setFileFilter(filter); fc.setMultiSelectionEnabled(false); fc.setFileSelectionMode(JFileChooser.FILES_ONLY); int returnVal = fc.showSaveDialog(_thisWindow); if (returnVal == JFileChooser.APPROVE_OPTION) { exportFile = fc.getSelectedFile(); PhotoSpread.photoSpreadPrefs.setProperty(PhotoSpread.lastDirWrittenKey, exportFile.getParent()); // Make sure file gets a .xml extension: try { exportFile = new File (Misc.ensureFileExtension(exportFile.getPath(), "xml")); } catch (java.text.ParseException e1) { // Exception when a directory is passed into ensureFileExtension // GUI file chooser prevents that. e1.printStackTrace(); } exportFile.setWritable(true); } else return; try { _theEditor.saveBindings(exportFile); } catch (IOException e1) { Misc.showErrorMsg("Failed to save bindings: " + e1.getMessage()); } } } /**************************************************** * Action Listener: LoadButtonListener Inner Class *****************************************************/ class LoadButtonListener implements ActionListener { KeyBindEditor _theEditor = null; public LoadButtonListener(KeyBindEditor theEditor) { _theEditor = theEditor; } public void actionPerformed(ActionEvent e) { File importFile = null; String priorReadDir = PhotoSpread.photoSpreadPrefs.getProperty(PhotoSpread.lastDirReadKey); final JFileChooser fc = new JFileChooser(priorReadDir); XMLFileFilter filter = new XMLFileFilter(); fc.setFileFilter(filter); fc.setMultiSelectionEnabled(false); fc.setFileSelectionMode(JFileChooser.FILES_ONLY); int returnVal = fc.showOpenDialog(_thisWindow); if (returnVal == JFileChooser.APPROVE_OPTION) { importFile = fc.getSelectedFile(); PhotoSpread.photoSpreadPrefs.setProperty(PhotoSpread.lastDirReadKey, importFile.getParent()); // Make sure file gets a .xml extension: try { importFile = new File (Misc.ensureFileExtension(importFile.getPath(), "xml")); } catch (java.text.ParseException e1) { // Exception when a directory is passed into ensureFileExtension // GUI file chooser prevents that. e1.printStackTrace(); } } try { _theEditor.loadBindings(importFile); } catch (IOException e1) { Misc.showErrorMsg("Failed to save bindings: " + e1.getMessage()); } } } /**************************************************** * Action Listener: SubmitButtonListener Inner Class *****************************************************/ class SubmitButtonListener implements ActionListener { public SubmitButtonListener() { } public void actionPerformed(ActionEvent e) { // Misc.showErrorMsg("Saving of key bindings not yet implemented.", _thisWindow); submitChanges(); return; } } /**************************************************** * Action Listener: CancelButtonListener Inner Class *****************************************************/ class CancelButtonListener implements ActionListener { public CancelButtonListener() { } public void actionPerformed(ActionEvent e) { // Just close the editor window: ChangeManager.unregisterClient(_editorName); dispose(); } } /**************************************************** * Methods * @throws IOException *****************************************************/ public void saveBindings (File xmlFile) throws IOException { BufferedWriter writeFD = null; try { writeFD = new BufferedWriter(new FileWriter(xmlFile)); writeFD.write(BINDINGS_OPEN_TAG); for (DnDKeyBindSpec dndSpec : _tableModel.getData()) { if (dndSpec.isEmpty()) continue; writeFD.write(DND_BINDING_OPEN_TAG); writeFD.write(KEY_SPEC_OPEN_TAG); writeFD.write(dndSpec.getKeySpec().toString()); writeFD.write(KEY_SPEC_CLOSE_TAG); writeFD.write(CELL_SPEC_OPEN_TAG); writeFD.write(dndSpec.getCellSpec().toString()); writeFD.write(CELL_SPEC_CLOSE_TAG); writeFD.write(DND_BINDING_CLOSE_TAG); } writeFD.write(BINDINGS_CLOSE_TAG); } catch (Exception e) { Misc.showErrorMsg( "Cannot save key bindings: " + e.getMessage(), _thisWindow); // window to show error msg in } finally { if (writeFD != null) writeFD.close(); } } public void loadBindings (File xmlFile) throws IOException { BufferedReader readFD = null; boolean gotBinding = false; try { _tableModel.removeAllRows(); readFD = new BufferedReader(new FileReader(xmlFile)); String bindingsTag = readFD.readLine().trim(); if (! bindingsTag.equals(BINDINGS_OPEN_TAG.trim())) throw new PhotoSpreadException.KeyBindingsFileSyntaxError( "Bindings file '" + xmlFile.getAbsolutePath() + "' does not begin with '" + BINDINGS_OPEN_TAG + "'. Begins with '" + bindingsTag + "' instead."); do { gotBinding = restoreBinding(readFD); } while (gotBinding); } catch (Exception e) { Misc.showErrorMsg( "Cannot load key bindings: " + e.getMessage(), _thisWindow); // window to show error msg in } finally { if (readFD != null) readFD.close(); } _tableModel.ensureMinRows(); } public boolean restoreBinding(BufferedReader in) throws IOException, KeyBindingsFileSyntaxError, IllegalArgumentException { try { String dndBindingTag = in.readLine().trim(); if (! dndBindingTag.equals(DND_BINDING_OPEN_TAG.trim())) { // Reached end of the <bindings> body? if (dndBindingTag.equals(BINDINGS_CLOSE_TAG.trim())) return false; throw new PhotoSpreadException.KeyBindingsFileSyntaxError( "Expected tag '" + DND_BINDING_OPEN_TAG + "'. Got '" + dndBindingTag + "' instead."); } KeyStrokeSpec keySpec = readKeyStrokeSpecObject(in); CellSpec cellSpec = readCellSpecObject(in); _tableModel.appendRow(new DnDKeyBindSpec(keySpec, cellSpec)); // Suck in the closing tag: dndBindingTag = in.readLine().trim(); if (! dndBindingTag.equals(DND_BINDING_CLOSE_TAG.trim())) throw new PhotoSpreadException.KeyBindingsFileSyntaxError ( "Expected tag '" + DND_BINDING_CLOSE_TAG + "'. Got '" + dndBindingTag + "' instead."); } catch (EOFException e) { return false; } return true; } public KeyStrokeSpec readKeyStrokeSpecObject(BufferedReader in) throws IOException, KeyBindingsFileSyntaxError, IllegalArgumentException { String keySpec = ""; try { keySpec = in.readLine().trim(); if (! keySpec.startsWith(KEY_SPEC_OPEN_TAG.trim())) throw new PhotoSpreadException.KeyBindingsFileSyntaxError( "Expected tag '" + KEY_SPEC_OPEN_TAG + "'. Got '" + keySpec+ "' instead."); if (! keySpec.endsWith(KEY_SPEC_CLOSE_TAG.trim())) throw new PhotoSpreadException.KeyBindingsFileSyntaxError ( "Expected tag '" + KEY_SPEC_CLOSE_TAG + "'. Got '" + keySpec + "' instead."); int endPos = keySpec.indexOf(KEY_SPEC_CLOSE_TAG.trim()); keySpec = keySpec.substring(KEY_SPEC_OPEN_TAG.trim().length(), endPos); } catch (EOFException e) { return null; } return new KeyStrokeSpec(keySpec); } public CellSpec readCellSpecObject(BufferedReader in) throws IOException, KeyBindingsFileSyntaxError, IllegalArgumentException { String cellSpec = ""; try { cellSpec = in.readLine().trim(); if (! cellSpec.startsWith(CELL_SPEC_OPEN_TAG.trim())) throw new PhotoSpreadException.KeyBindingsFileSyntaxError( "Expected tag '" + CELL_SPEC_OPEN_TAG + "'. Got '" + cellSpec + "' instead."); if (! cellSpec.endsWith(CELL_SPEC_CLOSE_TAG.trim())) throw new PhotoSpreadException.KeyBindingsFileSyntaxError ( "Expected tag '" + CELL_SPEC_CLOSE_TAG + "'. Got '" + cellSpec + "' instead."); int endPos = cellSpec.indexOf(CELL_SPEC_CLOSE_TAG.trim()); cellSpec = cellSpec.substring(CELL_SPEC_OPEN_TAG.trim().length(), endPos); } catch (EOFException e) { return null; } return new CellSpec(cellSpec); } public void selectAll() { if (_table != null) _table.selectAll(); } private void initEditor() { _panel.setLayout(new BorderLayout()); JButton cancelButton = new JButton("Cancel"); cancelButton.setAlignmentX(Component.CENTER_ALIGNMENT); JButton submitButton = new JButton("Apply Bindings"); submitButton.setAlignmentX(CENTER_ALIGNMENT); JButton saveButton = new JButton("Save Bindings"); saveButton.setAlignmentX(CENTER_ALIGNMENT); JButton loadButton = new JButton("Load Bindings"); cancelButton.setAlignmentX(Component.CENTER_ALIGNMENT); cancelButton.addActionListener(new CancelButtonListener()); submitButton.addActionListener(new SubmitButtonListener()); saveButton.addActionListener(new SaveButtonListener(this)); loadButton.addActionListener(new LoadButtonListener(this)); // Panel for the submit/cancel buttons: JPanel buttonsPanel = new JPanel(); // Put each button into its own horizontal box // with horizontal glue on each of its sides. // This will make the buttons expand to the // same size: JPanel submitButtonPanel = new JPanel(); BoxLayout saveAndExitButtonLayout = new BoxLayout(submitButtonPanel, BoxLayout.LINE_AXIS); submitButtonPanel.setLayout(saveAndExitButtonLayout); submitButtonPanel.add(Box.createHorizontalGlue()); submitButtonPanel.add(submitButton); submitButtonPanel.add(Box.createHorizontalGlue()); JPanel cancelButtonPanel = new JPanel(); BoxLayout cancelButtonLayout = new BoxLayout(cancelButtonPanel, BoxLayout.LINE_AXIS); cancelButtonPanel.setLayout(cancelButtonLayout); cancelButtonPanel.add(Box.createHorizontalGlue()); cancelButtonPanel.add(cancelButton); cancelButtonPanel.add(Box.createHorizontalGlue()); JPanel saveButtonPanel = new JPanel(); BoxLayout saveButtonLayout = new BoxLayout(saveButtonPanel, BoxLayout.LINE_AXIS); saveButtonPanel.setLayout(saveButtonLayout); saveButtonPanel.add(Box.createHorizontalGlue()); saveButtonPanel.add(saveButton); saveButtonPanel.add(Box.createHorizontalGlue()); JPanel loadButtonPanel = new JPanel(); BoxLayout loadButtonLayout = new BoxLayout(loadButtonPanel, BoxLayout.LINE_AXIS); loadButtonPanel.setLayout(loadButtonLayout); loadButtonPanel.add(Box.createHorizontalGlue()); loadButtonPanel.add(loadButton); loadButtonPanel.add(Box.createHorizontalGlue()); // Make all buttons as wide and high as the widest/highest among them: Dimension preferredDims[] = { saveButton.getPreferredSize(), loadButton.getPreferredSize(), submitButton.getPreferredSize(), cancelButton.getPreferredSize() }; Dimension maxPreferredDim = ComputableDimension.maxWidthHeight(preferredDims); submitButton.setPreferredSize(maxPreferredDim); cancelButton.setPreferredSize(maxPreferredDim); saveButton.setPreferredSize(maxPreferredDim); loadButton.setPreferredSize(maxPreferredDim); BoxLayout buttonLayout = new BoxLayout(buttonsPanel, BoxLayout.X_AXIS); buttonsPanel.setLayout(buttonLayout); buttonsPanel.setAlignmentX(CENTER_ALIGNMENT); buttonsPanel.setBorder(BorderFactory.createLoweredBevelBorder()); buttonsPanel.setBackground(Color.LIGHT_GRAY); // Make the buttons flush right: buttonsPanel.add(Box.createHorizontalGlue()); buttonsPanel.add(cancelButton); // Space between the buttons: buttonsPanel.add(Box.createRigidArea(new Dimension(0, ACTION_BUTTONS_HGAP))); buttonsPanel.add(saveButton); // Space between the buttons: buttonsPanel.add(Box.createRigidArea(new Dimension(0, ACTION_BUTTONS_HGAP))); buttonsPanel.add(loadButton); // Space between the buttons: buttonsPanel.add(Box.createRigidArea(new Dimension(0, ACTION_BUTTONS_HGAP))); buttonsPanel.add(submitButton); // Create TablePanel for Table and Column Header JPanel tablePanel = new JPanel(); BoxLayout tableAndHeaderLayout = new BoxLayout(tablePanel, BoxLayout.PAGE_AXIS); tablePanel.setLayout(tableAndHeaderLayout); // The table of metadata attributes and values: _tableModel = new DataTableModel(getExistingBindings()); _table = new KeyBindingsTable(_tableModel); addContextMenu(); _table.setBackground(Const.metaDataEditorBackGroundColor); _table.setForeground(Const.metaDataEditorForeGroundColor); _table.setSelectionForeground(Color.gray); _table.setFont(Const.TABLE_FONT); _table.addKeyListener(new KeyBindKeyListener()); _table.setVisible(true); _table.setMinimumSize(new Dimension(100, 100)); tablePanel.add(_table.getTableHeader()); tablePanel.add(_table); this.setBackground(Color.darkGray); // Attr/value table header and body: _panel.add(tablePanel, BorderLayout.CENTER); // Add control buttons at bottom: _panel.add(buttonsPanel, BorderLayout.SOUTH); JLabel title = new JLabel("<HTML><H1>Set Drag-Drop Key Shortcuts</H1></HTML>"); title.setBackground(Color.LIGHT_GRAY); JPanel titlePanel = new JPanel(); titlePanel.add(title); _panel.add(titlePanel, BorderLayout.NORTH); _panel.validate(); } private ArrayList<DnDKeyBindSpec> getExistingBindings () { ActionMap actMap = _photoSpreadSheet.getActionMap(); Object [] actionKeys = actMap.allKeys(); ArrayList<DnDKeyBindSpec> dndBindings = new ArrayList<DnDKeyBindSpec>(); Action theAction = null; for (Object key : actionKeys) { theAction = actMap.get(key); // Is the action one of our dnd actions? if (! (theAction instanceof AutoDnDAction)) continue; try { dndBindings.add(new DnDKeyBindSpec ((String) key, ((AutoDnDAction) theAction).getDestinationCell().getCellAddress())); } catch (IllegalArgumentException e) { Misc.showErrorMsgAndStackTrace(e, ""); // e.printStackTrace(); } } return dndBindings; } private void submitChanges() { AutoDnDAction autoDnDAction = null; int count = 0; for (DnDKeyBindSpec rowData : _tableModel.getData()) { if (rowData.getCellSpec().toString().isEmpty() || rowData.getKeySpec().toString().isEmpty()) continue; autoDnDAction = new AutoDnDAction(rowData.getCellSpec().getCell()); Misc.bindKey( _photoSpreadSheet, rowData.getKeySpec().toString(), autoDnDAction); Misc.bindKey( _photoSpreadSheet.getWorkspace(), rowData.getKeySpec().toString(), autoDnDAction); count++; } boolean quitNow = Misc.showConfirmMsg( "Bound " + count + " key(s). Exit bindings editor without saving bindings to a file?", _thisWindow); if (quitNow) { ChangeManager.unregisterClient(_editorName); dispose(); } } /**************************************************** * AutoDnDAction Action *****************************************************/ class AutoDnDAction extends AbstractAction { private static final long serialVersionUID = 1L; PhotoSpreadCell _destCell = null; public AutoDnDAction (PhotoSpreadCell destCell) { _destCell = destCell; } @Override public void actionPerformed(ActionEvent e) { PhotoSpreadCell srcCell = _photoSpreadSheet.getWorkspace().getDisplayedCell(); if (_destCell.equals(srcCell)) { Misc.showErrorMsg( "Attempt to drag from/to same cell: " + srcCell.getCellAddress() + ". Will ignore."); return; } PhotoSpreadDragDropManager.setSourceCell(srcCell); PhotoSpreadDragDropManager.setDestCell(_destCell); PhotoSpreadDragDropManager.executeDragDrop(); _photoSpreadSheet.getTableModel().fireTableCellUpdated(_destCell.getRow(), _destCell.getColumn()); if (srcCell == _photoSpreadSheet.getWorkspace().getDisplayedCell()) _photoSpreadSheet.getWorkspace().redraw(); /* //******** Misc.showConfirmMsg( "Would drag from " + srcCell.getCellAddress() + " to " + _destCell.getCellAddress() + "."); */ } public PhotoSpreadCell getDestinationCell () { return _destCell; } } /**************************************************** * Context Menu Actions *****************************************************/ private void addContextMenu(){ _contextMenu = new PhotoSpreadContextMenu(); _contextMenu.addMenuItem("Add row above", new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { _tableModel.addRowBefore(); // see NOTE1 above on need for following statement: _table.editingStopped(new ChangeEvent(this)); } } ); _contextMenu.addMenuItem("Add row below", new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { _tableModel.addRowAfter(); // see NOTE1 above on need for following statement: _table.editingStopped(new ChangeEvent(this)); } } ); _contextMenu.addMenuItem("Clear row [DELETE]", new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { _tableModel.clearRow(); // see NOTE1 above on need for following statement: _table.editingStopped(new ChangeEvent(this)); } } ); _contextMenu.addMenuItem("Delete row [numpad-DEL]", new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { _tableModel.removeRow(); // see NOTE1 above on need for following statement: _table.editingStopped(new ChangeEvent(this)); } } ); _contextMenu.addMenuItem("Clear all", new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { _tableModel.clear(); // see NOTE1 above on need for following statement: _table.editingStopped(new ChangeEvent(this)); } } ); _contextMenu.addMenuItem("Select All",new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { selectAll(); } } ); _table.addMouseListener(_contextMenu.getPopupListener()); } /**************************************************** * KeyBindingsTable Inner Class *****************************************************/ /** * We subclass JTable purely to do our own tooltips * for each column. * */ class KeyBindingsTable extends JTable { private static final long serialVersionUID = 1L; public KeyBindingsTable(DataTableModel model) { super(model); } /** * Provide examples via tooltips, depending * on which column the mouse is over. * * @see javax.swing.JTable#getToolTipText(java.awt.event.MouseEvent) */ public String getToolTipText(MouseEvent e) { String tip = null; java.awt.Point p = e.getPoint(); int colIndex = columnAtPoint(p); int realColumnIndex = convertColumnIndexToModel(colIndex); if (realColumnIndex == KEY_SEQUENCE_COLUMN) { tip = "Examples: 'control x', 'alt shift Y', ... "; } else if (realColumnIndex == CELL_ADDRESS_COLUMN) { tip = "Examples: 'C2', 'a4', ..."; } return tip; } } /**************************************************** * DataTableModel Inner Class *****************************************************/ class DataTableModel extends AbstractTableModel { public static final int ATTR_NAME_COL = 0; public static final int VALUE_COL = 1; private static final long serialVersionUID = 1L; private String[] columnNames = { "<HTML><H3>Key Sequence</H3></HTML>", "<HTML><H3>Cell Address</H3></HTML>" }; private ArrayList<DnDKeyBindSpec> data; public DataTableModel() { data = new ArrayList<DnDKeyBindSpec>(); // Add some empty rows: ensureMinRows(); } public DataTableModel (ArrayList<DnDKeyBindSpec> initialData) { data = initialData; ensureMinRows(); } public DataTableModel(HashMap<KeyStrokeSpec, CellSpec> keyStrokeToCellSpecMap) { for (KeyStrokeSpec keySpec : keyStrokeToCellSpecMap.keySet()) { data.add(new DnDKeyBindSpec(keySpec, keyStrokeToCellSpecMap.get(keySpec))); } ensureMinRows(); } /** * Make sure that at least a set minimum of * rows are available in the table. */ private void ensureMinRows () { if (data.size() >= NUM_OF_TABLE_ROWS) return; int emptyRowsToAdd = NUM_OF_TABLE_ROWS - data.size(); for (int i = 0; i < emptyRowsToAdd; i++) { data.add(makeEmptyRow()); } } public void dispose() { } public Class<?> getColumnClass(int columnIndex) { return String.class; } public Comparator<?> getComparator (int col) { return null; // _metadataComparator; } private DnDKeyBindSpec makeEmptyRow() { DnDKeyBindSpec newRow = null; try { newRow = new DnDKeyBindSpec(new KeyStrokeSpec(""), new CellSpec("")); } catch (IllegalArgumentException e) { // } return newRow; } public void addRowBefore() { // Get indices of first and last currently selected rows: int firstSelectedRow = _table.getSelectionModel() .getAnchorSelectionIndex(); addRowBefore(firstSelectedRow); } public void addRowBefore(int rowNum) { int[] rowSpecs = {rowNum}; if (!areTableCoordsLegal(rowSpecs)) return; data.add(rowNum, makeEmptyRow()); ChangeManager.markDirty(_editorName); fireTableRowsInserted(rowNum, rowNum); } public void addRowAfter () { int firstSelectedRow = _table.getSelectionModel() .getAnchorSelectionIndex(); if (firstSelectedRow == getRowCount() - 1) appendRow(); else addRowBefore(firstSelectedRow + 1); } public void appendRow() { int newRowIndex = getRowCount(); data.add(makeEmptyRow()); ChangeManager.markDirty(_editorName); fireTableRowsInserted(newRowIndex, newRowIndex); } public void appendRow (DnDKeyBindSpec rowData) { int newRowIndex = getRowCount(); data.add(rowData); ChangeManager.markDirty(_editorName); fireTableRowsInserted(newRowIndex, newRowIndex); } public void removeAllRows() { removeRow(0, data.size() - 1); } public void removeRow() { // Get indices of first and last currently selected rows: int firstSelectedRow = _table.getSelectionModel() .getAnchorSelectionIndex(); removeRow(firstSelectedRow); } public void removeRow(int rowNum) { removeRow(rowNum, rowNum); } public void removeRow(int firstRowNum, int lastRowNum) { int[] rowSpecs = { firstRowNum, lastRowNum }; if (!areTableCoordsLegal(rowSpecs)) return; // Delete rows backwards so we don't remove // them under our butt: for (int rowNum = lastRowNum; rowNum >= firstRowNum; rowNum--) data.remove(rowNum); ChangeManager.markDirty(_editorName); fireTableRowsDeleted(firstRowNum, lastRowNum); } public void clear () { clearRow(0, data.size() - 1); } public void clearRow() { // Get indices of first and last currently selected rows: int firstSelectedRow = _table.getSelectionModel() .getAnchorSelectionIndex(); clearRow(firstSelectedRow); } public void clearRow(int rowNum) { clearRow(rowNum, rowNum); } public void clearRow(int firstRowNum, int lastRowNum) { int[] rowSpecs = { firstRowNum, lastRowNum }; if (!areTableCoordsLegal(rowSpecs)) return; DnDKeyBindSpec rowToClear; for (int rowNum = firstRowNum; rowNum <= lastRowNum; rowNum++) { rowToClear = data.get(rowNum); try { rowToClear.setBindingSpec("", ""); } catch (IllegalArgumentException e) { // Two empty strings are fine. } } ChangeManager.markDirty(_editorName); fireTableRowsUpdated(firstRowNum, lastRowNum); } public void clearCell(int rowNum, int colNum) { int[] rowSpecs = { rowNum }; int[] colSpecs = { colNum }; if (!areTableCoordsLegal(rowSpecs, colSpecs)) return; // setValueAt() will fire the update event: setValueAt("", rowNum, colNum); } @Override public boolean isCellEditable(int row, int col) { return true; } @Override public String getColumnName(int col) { return columnNames[col]; } public int getColumnCount() { return columnNames.length; } public int getRowCount() { return data.size(); } public Object getValueAt(int row, int col) { DnDKeyBindSpec rowData = data.get(row); if (rowData == null) return ""; switch (col) { case 0: return rowData.getKeySpec().toString(); case 1: return rowData.getCellSpec().toString(); default: return ""; } } public void setValueAt(Object value, int rowNum, int colNum) { if ((rowNum >= getRowCount()) || (rowNum < 0)) return; if ((colNum >= getColumnCount()) || (colNum < 0)) return; DnDKeyBindSpec rowData = data.get(rowNum); try { switch (colNum) { case 0: rowData.setKeySpec((String) value); break; case 1: rowData.setCellSpec((String) value); break; default: break; } } catch (PhotoSpreadException IllegalArgumentException) { // We are not allowed to throw an error, else // the JTable contract is violated. } ChangeManager.markDirty(_editorName); fireTableCellUpdated(rowNum, colNum); } public ArrayList<DnDKeyBindSpec> getData() { return data; } private boolean areTableCoordsLegal(int[] rows) { for (int rowNum : rows) { if ((rowNum >= getRowCount()) || (rowNum < 0)) return false; } return true; } private boolean areTableCoordsLegal(int[] rows, int[] cols) { for (int rowNum : rows) { if ((rowNum >= getRowCount()) || (rowNum < 0)) return false; } for (int colNum : rows) { if ((colNum >= columnNames.length) || (colNum < 0)) return false; } return true; } } /**************************************************** * Main and/or Testing Methods *****************************************************/ // Key strings used for the preferences properties data structure: public static final String prefsFileKey = "prefsFile"; public static final String csvFieldDelimiterKey = "csvFieldDelimiter"; public static final String lastDirWrittenKey = "lastDirWritten"; public static final String lastDirReadKey = "lastDirRead"; public static final String workspaceSizeKey = "workspaceSize"; public static final String editorSizeKey = "editorSize"; public static final String sheetSizeKey = "sheetSize"; public static final String formulaEditorStripSizeKey = "formulaEditorStripSize"; public static final String dragGhostSizeKey = "dragGhostSize"; public static final String sheetRowHeightMinKey = "sheetRowHeightMin"; public static final String sheetColWidthMinKey = "sheetColWidthMin"; public static final String sheetObjsInCellKey = "sheetObjsInCell"; public static final String sheetCellObjsWidthKey = "sheetCellObjsWidth"; public static final String sheetNumColsKey = "sheetNumCols"; public static final String sheetNumRowsKey = "sheetNumRows"; public static final String workspaceNumColsKey = "workspaceNumCols"; public static final String workspaceHGapKey = "workspaceHGap"; public static final String workspaceVGapKey = "workspaceVGap"; public static final String workspaceObjWidthKey = "workspaceObjWidth"; public static final String workspaceObjHeightKey = "workspaceObjHeight"; public static final String workspaceMaxObjWidthKey = "workspaceMaxObjWidth"; public static final String workspaceMaxObjHeightKey = "workspaceMaxObjHeight"; private static void initDefaultProperties() { // Set all the defaults in the separate defaults properties. // They will be used if the program asks for some property // that's not set: PhotoSpread.photoSpreadDefaults.setProperty(csvFieldDelimiterKey, ","); PhotoSpread.photoSpreadDefaults.setProperty(lastDirWrittenKey, System.getProperty("user.dir")); PhotoSpread.photoSpreadDefaults.setProperty(lastDirReadKey, System.getProperty("user.dir")); PhotoSpread.photoSpreadDefaults.setProperty(editorSizeKey, "830 940"); PhotoSpread.photoSpreadDefaults.setProperty(sheetSizeKey, "650 500"); PhotoSpread.photoSpreadDefaults.setProperty(formulaEditorStripSizeKey, "400 30"); PhotoSpread.photoSpreadDefaults.setProperty(dragGhostSizeKey, "50 50"); PhotoSpread.photoSpreadDefaults.setProperty(sheetRowHeightMinKey, "60"); PhotoSpread.photoSpreadDefaults.setProperty(sheetColWidthMinKey, "80"); PhotoSpread.photoSpreadDefaults.setProperty(sheetObjsInCellKey, "10"); PhotoSpread.photoSpreadDefaults.setProperty(sheetCellObjsWidthKey, "50"); PhotoSpread.photoSpreadDefaults.setProperty(sheetNumColsKey, "8"); PhotoSpread.photoSpreadDefaults.setProperty(sheetNumRowsKey, "8"); PhotoSpread.photoSpreadDefaults.setProperty(workspaceNumColsKey, "1"); PhotoSpread.photoSpreadDefaults.setProperty(workspaceHGapKey, "2"); PhotoSpread.photoSpreadDefaults.setProperty(workspaceVGapKey, "2"); PhotoSpread.photoSpreadDefaults.setProperty(workspaceObjWidthKey, "500"); // For now we make all items square: PhotoSpread.photoSpreadDefaults.setProperty(workspaceObjHeightKey, PhotoSpread.photoSpreadDefaults.getProperty(workspaceObjWidthKey)); //photoSpreadDefaults.setProperty(workspaceObjHeightKey, "500"); PhotoSpread.photoSpreadDefaults.setProperty(workspaceMaxObjWidthKey, "500"); PhotoSpread.photoSpreadDefaults.setProperty(workspaceMaxObjHeightKey, "500"); PhotoSpread.photoSpreadDefaults.setProperty(workspaceSizeKey, "500 500"); } public static void main (String[] args) { initDefaultProperties(); new KeyBindEditor(new PhotoSpreadTable(new PhotoSpreadTableModel(), new JFrame())); // System.exit(0); } }