blob: 8eb3c15c63a505bf361829b1e6f88797e797f925 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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.android.tools.idea.uibuilder.property.ptable;
import com.android.tools.idea.uibuilder.property.ptable.renderers.PNameRenderer;
import com.intellij.designer.model.Property;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.openapi.wm.ex.IdeFocusTraversalPolicy;
import com.intellij.ui.Cell;
import com.intellij.ui.TableSpeedSearch;
import com.intellij.ui.TableUtil;
import com.intellij.ui.table.JBTable;
import com.intellij.util.PairFunction;
import com.intellij.util.containers.Convertor;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.plaf.TableUI;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableModel;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.*;
public class PTable extends JBTable {
private final PNameRenderer myNameRenderer = new PNameRenderer();
private final TableSpeedSearch mySpeedSearch;
private PTableModel myModel;
private int myMouseHoverRow;
private int myMouseHoverCol;
public PTable(@NotNull PTableModel model) {
super(model);
myModel = model;
// since the row heights are uniform, there is no need to look at more than a few items
setMaxItemsForSizeCalculation(5);
// When a label cannot be fully displayed, hovering over it results in a popup that extends beyond the
// cell bounds to show the full value. We don't need this feature as it'll end up covering parts of the
// cell we don't want covered.
setExpandableItemsEnabled(false);
setShowColumns(false);
setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
setShowVerticalLines(true);
setIntercellSpacing(new Dimension(0, 1));
setGridColor(UIUtil.getSlightlyDarkerColor(getBackground()));
setColumnSelectionAllowed(false);
setCellSelectionEnabled(false);
setRowSelectionAllowed(true);
addMouseListener(new MouseTableListener());
HoverListener hoverListener = new HoverListener();
addMouseMotionListener(hoverListener);
addMouseListener(hoverListener);
mySpeedSearch = new TableSpeedSearch(this, new PairFunction<Object, Cell, String>() {
@Override
public String fun(Object object, Cell cell) {
if (cell.column != 0) return null; // only match property names, not values
return object instanceof PTableItem ? ((PTableItem)object).getName() : null;
}
});
}
@Override
public void setModel(@NotNull TableModel model) {
myModel = (PTableModel)model;
super.setModel(model);
}
@Override
public TableCellRenderer getCellRenderer(int row, int column) {
if (column == 0) {
return myNameRenderer;
}
PTableItem value = (PTableItem)getValueAt(row, column);
return value.getCellRenderer();
}
@Override
public TableCellEditor getCellEditor(int row, int column) {
PTableItem value = (PTableItem)getValueAt(row, column);
return value.getCellEditor();
}
public TableSpeedSearch getSpeedSearch() {
return mySpeedSearch;
}
public boolean isHover(int row, int col) {
return row == myMouseHoverRow && col == myMouseHoverCol;
}
@Override
public void setUI(TableUI ui) {
super.setUI(ui);
// Setup focus traversal keys such that tab takes focus out of the table
setFocusTraversalKeys(
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS));
setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS));
// Customize keymaps. See https://docs.oracle.com/javase/tutorial/uiswing/misc/keybinding.html for info on how this works, but the
// summary is that we set an input map mapping key bindings to a string, and an action map that maps those strings to specific actions.
ActionMap actionMap = getActionMap();
InputMap focusedInputMap = getInputMap(JComponent.WHEN_FOCUSED);
InputMap ancestorInputMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "smartEnter");
ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0));
actionMap.put("smartEnter", new MyEnterAction(false));
focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), "toggleEditor");
ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0));
actionMap.put("toggleEditor", new MyEnterAction(true));
ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0));
ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_KP_RIGHT, 0));
focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "expandCurrentRight");
focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_KP_RIGHT, 0), "expandCurrentRight");
actionMap.put("expandCurrentRight", new MyExpandCurrentAction(true));
ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0));
ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_KP_LEFT, 0));
focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "collapseCurrentLeft");
focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_KP_LEFT, 0), "collapseCurrentLeft");
actionMap.put("collapseCurrentLeft", new MyExpandCurrentAction(false));
}
private void toggleTreeNode(int row) {
PTableItem item = (PTableItem)myModel.getValueAt(row, 0);
if (item.isExpanded()) {
myModel.collapse(row);
}
else {
myModel.expand(row);
}
}
private void selectRow(int row) {
getSelectionModel().setSelectionInterval(row, row);
TableUtil.scrollSelectionToVisible(this);
}
private void quickEdit(int row) {
final PTableCellEditor editor = ((PTableItem)myModel.getValueAt(row, 0)).getCellEditor();
if (editor == null) {
return;
}
// only perform edit if we know the editor is capable of a quick toggle action.
// We know that boolean editors switch their state and finish editing right away
if (editor.isBooleanEditor()) {
startEditing(row);
}
}
private void startEditing(int row) {
final PTableCellEditor editor = ((PTableItem)myModel.getValueAt(row, 0)).getCellEditor();
if (editor == null) {
return;
}
editCellAt(row, 1);
final JComponent preferredComponent = getComponentToFocus(editor);
if (preferredComponent == null) return;
IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(new Runnable() {
@Override
public void run() {
preferredComponent.requestFocusInWindow();
editor.activate();
}
});
}
@Nullable
private JComponent getComponentToFocus(PTableCellEditor editor) {
JComponent preferredComponent = editor.getPreferredFocusComponent();
if (preferredComponent == null) {
preferredComponent = IdeFocusTraversalPolicy.getPreferredFocusedComponent((JComponent)editorComp);
}
if (preferredComponent == null) {
return null;
}
return preferredComponent;
}
// Expand/Collapse if it is a group property, start editing otherwise
private class MyEnterAction extends AbstractAction {
// don't launch a full editor, just perform a quick toggle
private final boolean myToggleOnly;
public MyEnterAction(boolean toggleOnly) {
myToggleOnly = toggleOnly;
}
@Override
public void actionPerformed(ActionEvent e) {
int selectedRow = getSelectedRow();
if (isEditing() || selectedRow == -1) {
return;
}
PTableItem item = (PTableItem)myModel.getValueAt(selectedRow, 0);
if (item.hasChildren()) {
toggleTreeNode(selectedRow);
selectRow(selectedRow);
}
else if (myToggleOnly) {
quickEdit(selectedRow);
}
else {
startEditing(selectedRow);
}
}
}
// Expand/Collapse items on right/left key press
private class MyExpandCurrentAction extends AbstractAction {
private final boolean myExpand;
public MyExpandCurrentAction(boolean expand) {
myExpand = expand;
}
@Override
public void actionPerformed(ActionEvent e) {
int selectedRow = getSelectedRow();
if (isEditing() || selectedRow == -1) {
return;
}
PTableItem item = (PTableItem)myModel.getValueAt(selectedRow, 0);
if (myExpand) {
if (item.hasChildren() && !item.isExpanded()) {
myModel.expand(selectedRow);
selectRow(selectedRow);
}
}
else {
if (item.isExpanded()) { // if it is a compound node, collapse it
myModel.collapse(selectedRow);
selectRow(selectedRow);
}
else if (item.getParent() != null) { // if it is a child node, move selection to the parent
selectRow(myModel.getParent(selectedRow));
}
}
}
}
// Expand/Collapse group items on mouse click
private class MouseTableListener extends MouseAdapter {
@Override
public void mousePressed(MouseEvent e) {
int row = rowAtPoint(e.getPoint());
if (row == -1) {
return;
}
PTableItem item = (PTableItem)myModel.getValueAt(row, 0);
if (!item.hasChildren()) {
return;
}
Rectangle rect = getCellRect(row, convertColumnIndexToView(0), false);
if (!rect.contains(e.getX(), e.getY())) {
return;
}
if (!PNameRenderer.hitTestTreeNodeIcon(item, e.getX() - rect.x)) {
return;
}
toggleTreeNode(row);
}
}
// Repaint cells on mouse hover
private class HoverListener extends MouseAdapter {
private int myPreviousHoverRow = -1;
private int myPreviousHoverCol = -1;
@Override
public void mouseMoved(MouseEvent e) {
myMouseHoverRow = rowAtPoint(e.getPoint());
if (myMouseHoverRow >= 0) {
myMouseHoverCol = columnAtPoint(e.getPoint());
}
// remove hover from the previous cell
if (myPreviousHoverRow != -1 && (myPreviousHoverRow != myMouseHoverRow || myPreviousHoverCol != myMouseHoverCol)) {
repaint(getCellRect(myPreviousHoverRow, myPreviousHoverCol, true));
myPreviousHoverRow = -1;
}
if (myMouseHoverCol < 0) {
return;
}
// repaint cell that has the hover
repaint(getCellRect(myMouseHoverRow, myMouseHoverCol, true));
myPreviousHoverRow = myMouseHoverRow;
myPreviousHoverCol = myMouseHoverCol;
}
@Override
public void mouseExited(MouseEvent e) {
if (myMouseHoverRow != -1 && myMouseHoverCol != -1) {
Rectangle cellRect = getCellRect(myMouseHoverRow, 1, true);
myMouseHoverRow = myMouseHoverCol = -1;
repaint(cellRect);
}
}
}
}