/* * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.util.ui.table; import com.intellij.openapi.Disposable; import com.intellij.openapi.editor.colors.EditorColorsManager; import com.intellij.openapi.editor.colors.EditorFontType; import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.wm.IdeFocusManager; import com.intellij.ui.DottedBorder; import com.intellij.ui.EditorSettingsProvider; import com.intellij.ui.EditorTextField; import com.intellij.ui.TableUtil; import com.intellij.ui.table.JBTable; import com.intellij.util.ui.AbstractTableCellEditor; import com.intellij.util.ui.MouseEventHandler; import com.intellij.util.ui.UIUtil; import gnu.trove.TIntArrayList; import gnu.trove.TIntObjectHashMap; import gnu.trove.TIntObjectProcedure; import org.jetbrains.annotations.NotNull; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableModel; import java.awt.*; import java.awt.event.*; import java.util.EventObject; import java.util.List; import static java.awt.event.KeyEvent.*; /** * @author Konstantin Bulenkov */ public abstract class JBListTable { protected final JTable myInternalTable; private final JBTable myMainTable; private final RowResizeAnimator myRowResizeAnimator; protected MouseEvent myMouseEvent; private MyCellEditor myCellEditor; private int myLastFocusedEditorComponentIdx = -1; public JBListTable(@NotNull final JTable t, @NotNull Disposable parent) { myInternalTable = t; myMainTable = new MyTable(); myMainTable.setTableHeader(null); myMainTable.setStriped(true); myRowResizeAnimator = new RowResizeAnimator(myMainTable); Disposer.register(parent, myRowResizeAnimator); } public void stopEditing() { TableUtil.stopEditing(myMainTable); } private static void installPaddingAndBordersForEditors(JBTableRowEditor editor) { final List<EditorTextField> editors = UIUtil.findComponentsOfType(editor, EditorTextField.class); for (EditorTextField textField : editors) { textField.putClientProperty("JComboBox.isTableCellEditor", Boolean.FALSE); textField.putClientProperty("JBListTable.isTableCellEditor", Boolean.TRUE); } } public final JBTable getTable() { return myMainTable; } protected abstract JBTableRowRenderer getRowRenderer(int row); protected abstract JBTableRowEditor getRowEditor(int row); protected JBTableRow getRowAt(final int row) { return new JBTableRow() { @Override public Object getValueAt(int column) { return myInternalTable.getValueAt(row, column); } }; } protected boolean isRowEditable(int row) { return true; } protected boolean isRowEmpty(int row) { return false; } public static JComponent createEditorTextFieldPresentation(final Project project, final FileType type, final String text, boolean selected, boolean focused) { final JPanel panel = new JPanel(new BorderLayout()); final EditorTextField field = new EditorTextField(text, project, type) { @Override protected boolean shouldHaveBorder() { return false; } }; Font font = EditorColorsManager.getInstance().getGlobalScheme().getFont(EditorFontType.PLAIN); font = new Font(font.getFontName(), font.getStyle(), 12); field.setFont(font); field.addSettingsProvider(EditorSettingsProvider.NO_WHITESPACE); if (selected && focused) { panel.setBackground(UIUtil.getTableSelectionBackground()); field.setAsRendererWithSelection(UIUtil.getTableSelectionBackground(), UIUtil.getTableSelectionForeground()); } else { panel.setBackground(UIUtil.getTableBackground()); if (selected) { panel.setBorder(new DottedBorder(UIUtil.getTableForeground())); } } panel.add(field, BorderLayout.WEST); return panel; } private class MyCellEditor extends AbstractTableCellEditor { private final JBTableRowEditor myEditor; public MyCellEditor(JBTableRowEditor editor) { myEditor = editor; } @Override public boolean isCellEditable(EventObject e) { if (e instanceof MouseEvent && UIUtil.isSelectionButtonDown((MouseEvent)e)) { return false; } return super.isCellEditable(e); } @Override public Component getTableCellEditorComponent(final JTable table, Object value, boolean isSelected, final int row, int column) { final JPanel p = new JPanel(new BorderLayout()) { @Override public void addNotify() { super.addNotify(); int height = getPreferredSize().height; if (height > table.getRowHeight(row)) { myRowResizeAnimator.resize(row, height); } } public void removeNotify() { if (myCellEditor != null) myCellEditor.saveFocusIndex(); super.removeNotify(); myRowResizeAnimator.resize(row, table.getRowHeight()); } }; p.addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { IdeFocusManager focusManager = IdeFocusManager.findInstanceByComponent(p); focusManager.requestFocus(getComponentToFocus(), true); } private Component getComponentToFocus() { if (myLastFocusedEditorComponentIdx >= 0) { JComponent[] focusableComponents = myEditor.getFocusableComponents(); if (myLastFocusedEditorComponentIdx < focusableComponents.length) { return focusableComponents[myLastFocusedEditorComponentIdx]; } } return myEditor.getPreferredFocusedComponent(); } }); p.add(myEditor, BorderLayout.CENTER); return p; } @Override public Object getCellEditorValue() { return myEditor.getValue(); } @Override public boolean stopCellEditing() { saveFocusIndex(); return super.stopCellEditing(); } @Override public void cancelCellEditing() { saveFocusIndex(); super.cancelCellEditing(); } private void saveFocusIndex() { JComponent[] components = myEditor.getFocusableComponents(); for (int i = 0; i < components.length; i++) { if (components[i].hasFocus()) { myLastFocusedEditorComponentIdx = i; break; } } } } private static class RowResizeAnimator implements ActionListener, Disposable { private static final int ANIMATION_STEP_MILLIS = 15; private static final int RESIZE_AMOUNT_PER_STEP = 5; private final TIntObjectHashMap<RowAnimationState> myRowAnimationStates = new TIntObjectHashMap<>(); private final Timer myAnimationTimer = UIUtil.createNamedTimer("JBListTableTimer",ANIMATION_STEP_MILLIS, this); private final JTable myTable; public RowResizeAnimator(JTable table) { myTable = table; } public void resize(int row, int targetHeight) { myRowAnimationStates.put(row, new RowAnimationState(row, targetHeight)); startAnimation(); } @Override public void actionPerformed(final ActionEvent e) { doAnimationStep(e.getWhen()); } @Override public void dispose() { stopAnimation(); // enforce all animations are completed doAnimationStep(Long.MAX_VALUE); } private void startAnimation() { if (!myAnimationTimer.isRunning()) { myAnimationTimer.start(); } } private void stopAnimation() { myAnimationTimer.stop(); } private void doAnimationStep(final long updateTime) { final TIntArrayList completeRows = new TIntArrayList(myRowAnimationStates.size()); myRowAnimationStates.forEachEntry(new TIntObjectProcedure<RowAnimationState>() { @Override public boolean execute(int row, RowAnimationState animationState) { if (animationState.doAnimationStep(updateTime)) { completeRows.add(row); } return true; } }); completeRows.forEach(row -> { myRowAnimationStates.remove(row); return true; }); if (myRowAnimationStates.isEmpty()) { stopAnimation(); } } private class RowAnimationState { private final int myRow; private final int myTargetHeight; private long myLastUpdateTime; public RowAnimationState(int row, int targetHeight) { myRow = row; myTargetHeight = targetHeight; myLastUpdateTime = System.currentTimeMillis(); } /** * @return whether this row animation is complete */ public boolean doAnimationStep(long currentTime) { if (myRow >= myTable.getRowCount()) return true; int currentRowHeight = myTable.getRowHeight(myRow); int resizeAbs = (int) (RESIZE_AMOUNT_PER_STEP * ((currentTime - myLastUpdateTime) / (double)ANIMATION_STEP_MILLIS)); int leftToAnimate = myTargetHeight - currentRowHeight; int newHeight = Math.abs(leftToAnimate) <= resizeAbs ? myTargetHeight : currentRowHeight + (leftToAnimate < 0 ? -resizeAbs : resizeAbs); myTable.setRowHeight(myRow, newHeight); myLastUpdateTime = currentTime; return myTargetHeight == newHeight; } } } private class MyTable extends JBTable { public MyTable() { super(new MyTableModel(myInternalTable.getModel())); } @Override public MyTableModel getModel() { return (MyTableModel)super.getModel(); } @Override public void editingStopped(ChangeEvent e) { super.editingStopped(e); } @Override public void editingCanceled(ChangeEvent e) { super.editingCanceled(e); } @Override protected void processKeyEvent(KeyEvent e) { myMouseEvent = null; //Mnemonics if (e.isAltDown()) { super.processKeyEvent(e); return; } if (e.getKeyCode() == VK_TAB) { if (e.getID() == KEY_PRESSED) { final KeyboardFocusManager keyboardFocusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager(); if (e.isShiftDown()) { keyboardFocusManager.focusPreviousComponent(this); } else { keyboardFocusManager.focusNextComponent(this); } } e.consume(); return; } super.processKeyEvent(e); } @Override protected void processMouseEvent(MouseEvent e) { myMouseEvent = e; super.processMouseEvent(e); } @Override public TableCellRenderer getCellRenderer(int row, int column) { final JBTableRowRenderer rowRenderer = getRowRenderer(row); return new TableCellRenderer() { @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean selected, boolean focused, int row, int col) { return rowRenderer.getRowRendererComponent(myInternalTable, row, selected, focused); } }; } @Override protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) { //Mnemonics and actions if (e.isAltDown() || e.isMetaDown() || e.isControlDown()) { return false; } if (e.getKeyCode() == VK_ESCAPE && pressed) { final int row = getSelectedRow(); if (row != -1 && isRowEmpty(row)) { MyTableModel model = getModel(); final int count = model.getRowCount(); model.removeRow(row); int newRow = count == row + 1 ? row - 1 : row; if (0 <= newRow && newRow < model.getRowCount()) { setRowSelectionInterval(newRow, newRow); } } } if (e.getKeyCode() == VK_ENTER) { if (e.getID() == KEY_PRESSED) { if (!isEditing() && e.getModifiers() == 0) { editCellAt(getSelectedRow(), getSelectedColumn()); } else if (isEditing()) { TableUtil.stopEditing(this); if (e.isControlDown() || e.isMetaDown()) { return false; } else { final int row = getSelectedRow() + 1; if (row < getRowCount()) { getSelectionModel().setSelectionInterval(row, row); } } } else { if (e.isControlDown() || e.isMetaDown()) { return false; } } } e.consume(); return true; } if (isEditing() && e.getKeyCode() == VK_TAB) { if (pressed) { final KeyboardFocusManager mgr = KeyboardFocusManager.getCurrentKeyboardFocusManager(); if (e.isShiftDown()) { mgr.focusPreviousComponent(); } else { mgr.focusNextComponent(); } } return true; } final boolean isUp = e.getKeyCode() == VK_UP; final boolean isDown = e.getKeyCode() == VK_DOWN; if (isEditing() && (isUp || isDown) && e.getModifiers() == 0 && e.getID() == KEY_PRESSED) { int row = getSelectedRow(); super.processKeyBinding(ks, e, condition, pressed); if (!isEditing() && row != getSelectedRow()) { TableUtil.editCellAt(this, getSelectedRow(), 0); e.consume(); return true; } } return super.processKeyBinding(ks, e, condition, pressed); } @Override public void columnMarginChanged(ChangeEvent e) { // we don't stop editing (it prevents editor removal when scrollbar is added) TableColumn resizingColumn = tableHeader != null ? tableHeader.getResizingColumn() : null; if (resizingColumn != null && autoResizeMode == AUTO_RESIZE_OFF) { resizingColumn.setPreferredWidth(resizingColumn.getWidth()); } resizeAndRepaint(); } @Override public TableCellEditor getCellEditor(final int row, int column) { final JBTableRowEditor editor = getRowEditor(row); if (editor != null) { editor.setMouseEvent(myMouseEvent); editor.prepareEditor(myInternalTable, row); installPaddingAndBordersForEditors(editor); editor.setFocusCycleRoot(true); editor.setFocusTraversalPolicy(new JBListTableFocusTraversalPolicy(editor)); editor.addMouseListener(MouseEventHandler.CONSUMER); myCellEditor = new MyCellEditor(editor); return myCellEditor; } myCellEditor = null; return myCellEditor; } @Override public Component prepareEditor(TableCellEditor editor, int row, int column) { Object value = getValueAt(row, column); boolean isSelected = isCellSelected(row, column); return editor.getTableCellEditorComponent(this, value, isSelected, row, column); } @Override public void addNotify() { super.addNotify(); } @Override public void removeNotify() { super.removeNotify(); Disposer.dispose(myRowResizeAnimator); } } private class MyTableModel extends JBListTableModel { MyTableModel(TableModel model) { super(model); } @Override public JBTableRow getRow(int index) { return getRowAt(index); } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { return isRowEditable(rowIndex); } @Override public void addRow() { myLastFocusedEditorComponentIdx = -1; super.addRow(); } } }