/******************************************************************************* * Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com) * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser Public License v3 * which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt ******************************************************************************/ package com.opendoorlogistics.studio.tables.grid; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Container; import java.awt.Font; import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.InputEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.concurrent.Callable; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.JToolBar; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.TransferHandler; import javax.swing.table.DefaultTableColumnModel; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableModel; import javax.swing.text.DefaultEditorKit; import com.opendoorlogistics.api.ui.Disposable; import com.opendoorlogistics.core.scripts.execution.ExecutionReportImpl; import com.opendoorlogistics.core.tables.utils.SortColumn; import com.opendoorlogistics.core.utils.strings.Strings; import com.opendoorlogistics.core.utils.ui.ExecutionReportDialog; import com.opendoorlogistics.core.utils.ui.PopupMenuMouseAdapter; import com.opendoorlogistics.studio.dialogs.SortDialog; import com.opendoorlogistics.studio.tables.ODLTableControl; import com.opendoorlogistics.studio.tables.grid.GridEditPermissions.Permission; import com.opendoorlogistics.studio.tables.grid.adapter.SwingAdapter; import com.opendoorlogistics.utils.ui.ODLAction; import com.opendoorlogistics.utils.ui.SimpleAction; public abstract class GridTable extends ODLTableControl implements Disposable { protected class ColumnModel extends DefaultTableColumnModel { /** * */ private static final long serialVersionUID = 1L; @Override public void addColumn(TableColumn aColumn) { if (getColumnCount() == 0) { aColumn.setPreferredWidth(36); } super.addColumn(aColumn); } } /** * */ private static final long serialVersionUID = 6615355902465336058L; protected boolean showFilters = false; protected List<SimpleAction> actions; protected JScrollPane scrollPane; protected TableCellRenderer myCellRenderer; protected final TableCellRenderer firstColumnRenderer = new HeaderCellRenderer(){ @Override protected boolean getColumnIsItalics(int col) { return false; } }; // protected TableCellRenderer my = new HeaderCellRenderer(); protected final int DEFAULT_ROW_HEIGHT = 24; protected final GridEditPermissions defaultPermissions; protected final SelectionManager selectionManager = createSelectionManager(); protected GridTable(TableModel tableModel, GridEditPermissions permissions) { this.defaultPermissions = permissions; putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); // set row height before setting the model as this is overrided with // images... setRowHeight(DEFAULT_ROW_HEIGHT); ColumnModel colModel = new ColumnModel(); setColumnModel(colModel); setSurrendersFocusOnKeystroke(true); setFillsViewportHeight(true); setColumnSelectionAllowed(true); setRowSelectionAllowed(true); setRowSorter(null); setAutoResizeMode(JTable.AUTO_RESIZE_OFF); // setSelectionModel(selectionManager.getSheetSelectionModel()); // init copy / paste handler CopyPasteTransferHandler copyPaste = new CopyPasteTransferHandler(this); setTransferHandler(copyPaste); initTableHeader(); initActions(copyPaste, permissions); // right-click popup menu final JPopupMenu popup = new JPopupMenu(); for (Action action : actions) { if (action != null) { popup.add(action); } else { popup.addSeparator(); } } addMouseListener(new PopupMenuMouseAdapter() { @Override protected void launchMenu(MouseEvent me) { popup.show(me.getComponent(), me.getX(), me.getY()); } }); // start editing on a key press but not for special keys addKeyListener(new KeyAdapter() { public void keyPressed(KeyEvent e) { // ignore if control pressed if ((e.getModifiers() & ActionEvent.CTRL_MASK) == ActionEvent.CTRL_MASK) { return; } if ((e.getModifiers() & ActionEvent.ALT_MASK) == ActionEvent.ALT_MASK) { return; } if (e.isActionKey() || e.getKeyCode() == KeyEvent.VK_SHIFT || e.getKeyCode() == KeyEvent.VK_TAB || e.getKeyCode() == KeyEvent.VK_DELETE) { return; } if (selectionManager.getFocusPoint() != null) { Point point = selectionManager.getFocusPoint(); if (!isEditing() && getModel().isCellEditable(point.y, point.x)) { clearSelection(); selectionManager.setSelectedCell(point.y, point.x); // clear value first, but user the editor component to // clear the value // so the clear only enters the undo/redo buffer if the // user selects the new changed value if (editCellAt(point.y, point.x)) { Component component = getEditorComponent(); if (component != null && JTextField.class.isInstance(component)) { ((JTextField) component).setText(""); } } transferFocus(); } } } }); addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent e) { if (selectionManager != null) { selectionManager.focusLost(e); } repaint(); } @Override public void focusGained(FocusEvent e) { // TODO Auto-generated method stub } }); initMoveCursorMappings(); // do set model last as this fires a table changed event which adjust // row heights for cells containing images setModel(tableModel); } protected SelectionManager createSelectionManager(){ return new SelectionManager(this,false); } protected abstract void setHeaderRenderer(); private void initTableHeader() { final JTableHeader header = getTableHeader(); header.setReorderingAllowed(false); header.setFont(new Font("Dialog", Font.BOLD, 11)); setHeaderRenderer(); class MouseListenerImpl implements MouseListener, MouseMotionListener { // private boolean drag=false; @Override public void mouseReleased(MouseEvent e) { updateSel(e, false); } @Override public void mousePressed(MouseEvent e) { updateSel(e, false); } @Override public void mouseExited(MouseEvent e) { // System.out.println("exit : drag=" + drag); } @Override public void mouseEntered(MouseEvent e) { // if(drag){ // updateSel(e); // } // System.out.println("entered : drag=" + drag); } @Override public void mouseClicked(MouseEvent e) { int col = columnAtPoint(e.getPoint()); if (col == 0) { selectAll(); } updateSel(e, false); // System.out.println("clicked : drag=" + drag); TableCellRenderer headerRender = getTableHeader().getDefaultRenderer(); if (headerRender != null && HeaderCellRenderer.class.isInstance(headerRender)) { ((HeaderCellRenderer) headerRender).mouseClicked(e); } } private void updateSel(MouseEvent e, boolean drag) { int col = columnAtPoint(e.getPoint()); selectionManager.changeSelection(-1, col, (e.getModifiers() & Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()) != 0, e.isShiftDown() || drag); header.repaint(); } @Override public void mouseDragged(MouseEvent e) { updateSel(e, true); } @Override public void mouseMoved(MouseEvent e) { // TODO Auto-generated method stub } } MouseListenerImpl headerMouseListener = new MouseListenerImpl(); header.addMouseListener(headerMouseListener); header.addMouseMotionListener(headerMouseListener); } @Override public void changeSelection(int rowIndex, int columnIndex, boolean toggle, boolean extend) { selectionManager.changeSelection(rowIndex, columnIndex, toggle, extend); } protected boolean runTransaction(Callable<Boolean> callable) { startTransaction(); try { if (callable.call()) { endTransaction(); return true; } else { rollbackTransaction(); } } catch (Throwable e2) { rollbackTransaction(); ExecutionReportImpl report = new ExecutionReportImpl(); report.setFailed("An error occurred when editing the table data."); report.setFailed(e2); new ExecutionReportDialog((JFrame)SwingUtilities.getWindowAncestor(this), "Table editing error", report, false).setVisible(true); //JOptionPane.showMessageDialog(getRootPane(), "An error occurred." + (Strings.isEmpty(e2.getMessage()) == false ? " " + e2.getMessage() : "")); } return false; } protected void clearSelected() { runTransaction(new Callable<Boolean>() { @Override public Boolean call() throws Exception { int minRow = Integer.MAX_VALUE; int maxRow = Integer.MIN_VALUE; List<List<Point>> selected = selectionManager.getSelectedPoints(); for (Point point : PasteLogic.toSingleList(selected)) { minRow = Math.min(minRow, point.y); maxRow = Math.max(maxRow, point.y); getModel().setValueAt(null, point.y, point.x); // System.out.println("Set null: " + point); } return true; } }); } private void fillSelectedCells() { runTransaction(new Callable<Boolean>() { @Override public Boolean call() throws Exception { List<List<Point>> selected = selectionManager.getSelectedPoints(); List<Point> points = PasteLogic.toSingleList(selected); if (points.size() > 0) { String val = JOptionPane.showInputDialog(getRootPane(), "Enter value to fill all selected cells"); if (val != null) { for (Point point : points) { if (getModel().isCellEditable(point.y, point.x)) { getModel().setValueAt(val, point.y, point.x); } } } } return true; } }); } @Override public void clearSelection() { super.clearSelection(); if (selectionManager != null) { selectionManager.clearSelection(); } } /** * Creates the toolbar but doen't add it to any component as this must be done by the parent component * * @return */ public JToolBar createToolbar() { JToolBar toolBar = new JToolBar(); toolBar.setFloatable(false); for (Action action : actions) { if (action != null) { toolBar.add(action); } else { toolBar.addSeparator(); } } toolBar.addSeparator(); final JCheckBox checkBox = new JCheckBox("Show filters", showFilters); checkBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { setShowFilters(checkBox.isSelected()); } }); // checkBox.setHorizontalTextPosition(SwingConstants.LEFT); toolBar.add(checkBox); return toolBar; } protected abstract void deleteColumns(); @Override public void dispose() { getModel().removeTableModelListener(this); }; protected abstract void endTransaction(); public abstract String getSelectedAsTabbedText(); public abstract String getTableName(); @SuppressWarnings("serial") protected void initActions(TransferHandler transferHandler, GridEditPermissions permissions) { abstract class MySimpleAction extends SimpleAction { private final boolean addToActionMap; public MySimpleAction(String name, String tooltip, String png, boolean addToActionMap) { super(name, tooltip, png); this.addToActionMap = addToActionMap; } public boolean isAddToActionMap() { return addToActionMap; } } actions = new ArrayList<>(); class EventRedirector { void redirect(String longname, ActionEvent e) { // possible hack? Action original = getActionMap().get(longname.split("-")[0]); if (original != null) { ActionEvent newE = new ActionEvent(GridTable.this, e.getID(), e.getActionCommand(), e.getWhen(), e.getModifiers()); original.actionPerformed(newE); } } } final EventRedirector redirector = new EventRedirector(); actions.add(new MySimpleAction("Create copy of table", "Create a copy of the table", "table-copy.png", false) { @Override public void actionPerformed(ActionEvent e) { copyTable(); } }); actions.add(new MySimpleAction("Copy", "Copy selected", "edit-copy-7.png", false) { /** * */ private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { redirector.redirect(DefaultEditorKit.copyAction, e); } }); actions.add(new MySimpleAction("Paste", "Paste", "edit-paste-7.png", false) { /** * */ private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { redirector.redirect(DefaultEditorKit.pasteAction, e); } @Override public void updateEnabledState() { setEnabled(getPermissions().get(Permission.setValues)); } }); actions.add(new MySimpleAction("Clear selected cells", "Clear selected cell(s)", "edit-clear-2.png", true) { /** * */ private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { clearSelected(); } @Override public void updateEnabledState() { setEnabled(getPermissions().get(Permission.setValues)); } }); getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "Clear cells"); actions.add(new MySimpleAction("Fill selected cells", "Fill selected cell(s)", "tool-bucket-fill-16x16.png", false) { @Override public void actionPerformed(ActionEvent e) { fillSelectedCells(); } @Override public void updateEnabledState() { setEnabled(getPermissions().get(Permission.setValues)); } }); actions.add(new MySimpleAction("Sort", "Sort rows", "view-sort-ascending-2.png", true) { /** * */ private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { SortDialog dialog = new SortDialog(getModel(), 1); Component parent = GridTable.this; if (parent.getParent() != null) { parent = parent.getParent(); } dialog.setLocationRelativeTo(parent); dialog.setVisible(true); if (dialog.getResult() != null) { SortColumn[] sortCols = dialog.getResult(); sort(sortCols); repaint(); } } @Override public void updateEnabledState() { setEnabled(getPermissions().get(Permission.moveRows)); } }); actions.add(new MySimpleAction("Insert row", "Insert empty row(s)", "insert-table-row.png", true) { /** * */ private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { insertDeleteRows(true); } @Override public void updateEnabledState() { setEnabled(getPermissions().get(Permission.createRows)); } }); actions.add(new MySimpleAction("Delete row(s)", "Delete selected row(s)", "deleterow.png", true) { /** * */ private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { insertDeleteRows(false); } @Override public void updateEnabledState() { setEnabled(getPermissions().get(Permission.deleteRows)); } }); initSubclassActions(permissions); // put all in the action map for (SimpleAction action : actions) { if (action != null && MySimpleAction.class.isInstance(action)) { if (((MySimpleAction) action).isAddToActionMap()) { getActionMap().put(action.getValue(Action.NAME), action); } } } } protected void initMoveCursorMappings() { abstract class BaseMoveCursor extends AbstractAction { private final String name; protected final int keyCode; protected int modifiers; public BaseMoveCursor(String name, int keyCode) { super(); this.name = name; this.keyCode = keyCode; } protected boolean isValidPoint(Point point) { return point.x >= 1 && point.x < getModel().getColumnCount() && point.y >= 0; } protected void selectPoint(Point point) { if (isValidPoint(point)) { if (isEditing()) { return; } clearSelection(); selectionManager.setFocusPoint(point); } } } // class SelectAll extends BaseMoveCursor{ // // public SelectAll() { // super("CTRL_A", KeyEvent.VK_A); // modifiers =InputEvent.CTRL_DOWN_MASK; // } // // @Override // public void actionPerformed(ActionEvent e) { // // TODO Auto-generated method stub // // } // // } class MoveCursor extends BaseMoveCursor { protected final int deltaColumn; protected final int deltaRow; public MoveCursor(String name, int deltaRow, int deltaColumn, int keyCode) { super(name, keyCode); this.deltaRow = deltaRow; this.deltaColumn = deltaColumn; } @Override public void actionPerformed(ActionEvent e) { Point point = selectionManager.getFocusPoint(); if (point != null) { point.x += deltaColumn; point.y += deltaRow; selectPoint(point); } } } class ExtendMove extends MoveCursor { public ExtendMove(String name, int deltaRow, int deltaColumn, int keyCode) { super(name, deltaRow, deltaColumn, keyCode); modifiers = InputEvent.SHIFT_DOWN_MASK; } @Override public void actionPerformed(ActionEvent e) { Point point = selectionManager.getFocusPoint(); if (point != null) { point.x += deltaColumn; point.y += deltaRow; selectionManager.changeSelection(point.y, point.x, false, true); } } } class PageUpDown extends BaseMoveCursor { public PageUpDown(boolean up) { super(up ? "PAGE_UP" : "PAGE_DOWN", up ? KeyEvent.VK_PAGE_UP : KeyEvent.VK_PAGE_DOWN); } @Override public void actionPerformed(ActionEvent e) { Point point = selectionManager.getFocusPoint(); if (point != null) { Rectangle vr = getVisibleRect(); int firstVisibleRow = rowAtPoint(vr.getLocation()); vr.translate(0, vr.height); int visibleRows = rowAtPoint(vr.getLocation()) - firstVisibleRow; if (keyCode == KeyEvent.VK_PAGE_UP) { point.y -= visibleRows; point.y = Math.max(0, point.y); } else { point.y += visibleRows; } selectPoint(point); } } } class HomeEnd extends BaseMoveCursor { public HomeEnd(boolean home) { super(home ? "HOME" : "END", home ? KeyEvent.VK_HOME : KeyEvent.VK_END); } @Override public void actionPerformed(ActionEvent e) { Point point = selectionManager.getFocusPoint(); if (point != null) { if (keyCode == KeyEvent.VK_HOME) { point.x = 1; } else { point.x = getColumnCount() - 1; } selectPoint(point); } } } class ProcessTab extends BaseMoveCursor { public ProcessTab() { super("TAB", KeyEvent.VK_TAB); } @Override public void actionPerformed(ActionEvent e) { Point point = selectionManager.getFocusPoint(); if (point == null && isEditing()) { point = new Point(getEditingColumn(), getEditingRow()); } if (point != null) { if (point.x < getModel().getColumnCount() - 1) { point.x++; } else { point.y++; point.x = 1; } if (isValidPoint(point)) { if (isEditing() && !getCellEditor().stopCellEditing()) { return; } clearSelection(); selectionManager.setFocusPoint(point); editCellAt(point.y, point.x); } } } } ArrayList<BaseMoveCursor> moveActions = new ArrayList<>(); moveActions.add(new MoveCursor("LEFT", 0, -1, KeyEvent.VK_LEFT)); moveActions.add(new MoveCursor("RIGHT", 0, +1, KeyEvent.VK_RIGHT)); moveActions.add(new MoveCursor("UP", -1, 0, KeyEvent.VK_UP)); moveActions.add(new MoveCursor("DOWN", +1, 0, KeyEvent.VK_DOWN)); moveActions.add(new ExtendMove("SHIFT_LEFT", 0, -1, KeyEvent.VK_LEFT)); moveActions.add(new ExtendMove("SHIFT_RIGHT", 0, +1, KeyEvent.VK_RIGHT)); moveActions.add(new ExtendMove("SHIFT_UP", -1, 0, KeyEvent.VK_UP)); moveActions.add(new ExtendMove("SHIFT_DOWN", +1, 0, KeyEvent.VK_DOWN)); moveActions.add(new ProcessTab()); moveActions.add(new PageUpDown(true)); moveActions.add(new PageUpDown(false)); moveActions.add(new HomeEnd(true)); moveActions.add(new HomeEnd(false)); // put all in the action map for (BaseMoveCursor action : moveActions) { for (int imap : new int[] { JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, JComponent.WHEN_FOCUSED, JComponent.WHEN_IN_FOCUSED_WINDOW }) { getInputMap(imap).put(KeyStroke.getKeyStroke(action.keyCode, action.modifiers), action.name); } ActionMap map = getActionMap(); map.put(action.name, action); } } protected void initSubclassActions(GridEditPermissions permissions) { } protected abstract void insertCols(boolean toLeft); protected abstract void insertDeleteRows(boolean inserting); @Override public boolean isCellSelected(int row, int column) { return selectionManager.isCellSelected(row, column); } @Override public boolean isRowSelected(int row) { return selectionManager.isRowSelected(row); } public abstract void pasteTabbedText(final String s); @Override public Component prepareRenderer(TableCellRenderer renderer, int row, int column) { // first column gets a header rendering instead if (column == 0) { renderer = firstColumnRenderer; } else { renderer = myCellRenderer; } Object value = getValueAt(row, column); // Only indicate the selection and focused cell if not printing boolean isSelected = isCellSelected(row, column); boolean hasFocus = selectionManager.isFocused(row, column) && isFocusOwner(); return renderer.getTableCellRendererComponent(this, value, isSelected, hasFocus, row, column); } // protected abstract void redo(); protected abstract void rollbackTransaction(); protected abstract void sort(SortColumn[] sortCols); protected abstract void startTransaction(); // protected abstract void undo(); protected abstract void copyTable(); /** * Add the table together with toolbar and scrollpane to the container. This assumes the container is empty and replaces its layout. * * @param table * @param container */ public static JToolBar addToContainer(GridTable table, Container container) { JToolBar toolBar = table.createToolbar(); container.setLayout(new BorderLayout()); container.add(toolBar, BorderLayout.NORTH); table.scrollPane = new JScrollPane(table, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); container.add(table.scrollPane, BorderLayout.CENTER); return toolBar; } abstract int getLastFilledRowNumber(); @Override public void selectAll() { if (isEditing()) { removeEditor(); } int nc = getColumnCount(); LinkedList<Point> list = new LinkedList<>(); int lastFilledRow = getLastFilledRowNumber(); boolean allSel = true; for (int col = 1; col < nc; col++) { for (int row = 0; row <= lastFilledRow; row++) { list.add(new Point(col, row)); if (selectionManager.isCellSelected(row, col) == false) { allSel = false; } } } if (allSel) { selectionManager.clearSelection(); } else { selectionManager.setSelectedCells(list); } repaint(); } public abstract GridEditPermissions getPermissions() ; public boolean isShowFilters() { return showFilters; } public void setShowFilters(boolean showFilters) { if (showFilters != this.showFilters) { this.showFilters = showFilters; setHeaderRenderer(); tableHeader.invalidate(); tableHeader.revalidate(); tableHeader.updateUI(); } } public void updateActions() { if(actions!=null){ for (ODLAction action : actions) { if(action!=null){ action.updateEnabledState(); } } } } }