/*
* Copyright 2000-2014 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.application.options.codeStyle.arrangement.match;
import com.intellij.application.options.codeStyle.arrangement.color.ArrangementColorsProvider;
import com.intellij.application.options.codeStyle.arrangement.ui.ArrangementEditorAware;
import com.intellij.application.options.codeStyle.arrangement.ui.ArrangementRepresentationAware;
import com.intellij.application.options.codeStyle.arrangement.util.ArrangementListRowDecorator;
import com.intellij.application.options.codeStyle.arrangement.util.IntObjectMap;
import com.intellij.openapi.application.ApplicationBundle;
import com.intellij.psi.codeStyle.arrangement.match.StdArrangementEntryMatcher;
import com.intellij.psi.codeStyle.arrangement.match.StdArrangementMatchRule;
import com.intellij.psi.codeStyle.arrangement.model.ArrangementAtomMatchCondition;
import com.intellij.psi.codeStyle.arrangement.std.ArrangementStandardSettingsManager;
import com.intellij.psi.codeStyle.arrangement.std.ArrangementUiComponent;
import com.intellij.psi.codeStyle.arrangement.std.StdArrangementTokens;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.table.JBTable;
import com.intellij.util.ui.AbstractTableCellEditor;
import gnu.trove.TIntArrayList;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
/**
* @author Denis Zhdanov
* @since 10/31/12 1:23 PM
*/
public class ArrangementMatchingRulesControl extends JBTable {
@NotNull private static final JLabel EMPTY_RENDERER = new JLabel(ApplicationBundle.message("arrangement.text.empty.rule"));
@NotNull protected final IntObjectMap<ArrangementListRowDecorator> myComponents = new IntObjectMap<ArrangementListRowDecorator>();
@NotNull private final TIntArrayList mySelectedRows = new TIntArrayList();
@NotNull private final ArrangementMatchNodeComponentFactory myFactory;
@NotNull protected ArrangementMatchingRuleEditor myEditor;
@NotNull private final RepresentationCallback myRepresentationCallback;
@NotNull private final MatchingRulesRendererBase myRenderer;
private ArrangementMatchingRulesValidator myValidator;
private final int myMinRowHeight;
private int myRowUnderMouse = -1;
private int myEditorRow = -1;
private boolean mySkipSelectionChange;
public ArrangementMatchingRulesControl(@NotNull ArrangementStandardSettingsManager settingsManager,
@NotNull ArrangementColorsProvider colorsProvider,
@NotNull RepresentationCallback callback)
{
super(new ArrangementMatchingRulesModel());
myRepresentationCallback = callback;
myFactory = new ArrangementMatchNodeComponentFactory(settingsManager, colorsProvider, this);
myRenderer = createRender();
setDefaultRenderer(Object.class, myRenderer);
getColumnModel().getColumn(0).setCellEditor(new MyEditor());
setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
setShowColumns(false);
setShowGrid(false);
setBorder(IdeBorderFactory.createBorder());
setSurrendersFocusOnKeystroke(true);
putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
ArrangementAtomMatchCondition condition = new ArrangementAtomMatchCondition(StdArrangementTokens.EntryType.CLASS);
StdArrangementMatchRule rule = new StdArrangementMatchRule(new StdArrangementEntryMatcher(condition));
ArrangementUiComponent component = myFactory.getComponent(condition, rule, true);
myMinRowHeight = new ArrangementListRowDecorator(component, this).getPreferredSize().height;
myEditor = new ArrangementMatchingRuleEditor(settingsManager, colorsProvider, this);
addMouseMotionListener(new MouseAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
onMouseMoved(e);
}
});
getSelectionModel().addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
onSelectionChange(e);
}
});
getModel().addTableModelListener(new TableModelListener() {
@Override
public void tableChanged(TableModelEvent e) { onTableChange(e); }
});
}
protected MatchingRulesRendererBase createRender() {
return new MatchingRulesRendererBase();
}
@NotNull
protected ArrangementMatchingRulesValidator createValidator() {
return new ArrangementMatchingRulesValidator(getModel());
}
private ArrangementMatchingRulesValidator getValidator() {
if (myValidator == null) {
myValidator = createValidator();
}
return myValidator;
}
@NotNull
@Override
public ArrangementMatchingRulesModel getModel() {
return (ArrangementMatchingRulesModel)super.getModel();
}
@Override
protected void processMouseEvent(MouseEvent e) {
int id = e.getID();
switch (id) {
case MouseEvent.MOUSE_ENTERED: onMouseEntered(e); break;
case MouseEvent.MOUSE_EXITED: onMouseExited(); break;
case MouseEvent.MOUSE_RELEASED: onMouseReleased(e); break;
case MouseEvent.MOUSE_CLICKED: onMouseClicked(e); break;
}
if (!e.isConsumed()) {
super.processMouseEvent(e);
}
}
private void onMouseClicked(@NotNull MouseEvent e) {
final int count = e.getClickCount();
if (count != 2) {
return;
}
final TIntArrayList rows = getSelectedModelRows();
if (rows.size() != 1) {
return;
}
final int row = rows.get(0);
showEditor(row);
scrollRowToVisible(row);
}
private void onMouseMoved(@NotNull MouseEvent e) {
int i = rowAtPoint(e.getPoint());
if (i != myRowUnderMouse) {
onMouseExited();
}
if (i < 0) {
return;
}
if (i != myRowUnderMouse) {
onMouseEntered(e);
}
ArrangementListRowDecorator decorator = myComponents.get(i);
if (decorator == null) {
return;
}
Rectangle rectangle = decorator.onMouseMove(e);
if (rectangle != null) {
repaintScreenBounds(rectangle);
}
}
private void repaintScreenBounds(@NotNull Rectangle bounds) {
Point location = bounds.getLocation();
SwingUtilities.convertPointFromScreen(location, this);
int x = location.x;
int width = bounds.width;
repaint(x, location.y, width, bounds.height);
}
private void onMouseReleased(@NotNull MouseEvent e) {
int i = rowAtPoint(e.getPoint());
if (i < 0) {
return;
}
ArrangementListRowDecorator decorator = myComponents.get(i);
if (decorator != null) {
decorator.onMouseRelease(e);
}
if (!e.isConsumed() && myEditorRow > 0 && myEditorRow == i + 1) {
hideEditor();
}
}
private void onMouseExited() {
if (myRowUnderMouse < 0) {
return;
}
ArrangementListRowDecorator decorator = myComponents.get(myRowUnderMouse);
if (decorator != null) {
decorator.onMouseExited();
repaintRows(myRowUnderMouse, myRowUnderMouse, false);
}
myRowUnderMouse = -1;
}
private void onMouseEntered(@NotNull MouseEvent e) {
myRowUnderMouse = rowAtPoint(e.getPoint());
ArrangementListRowDecorator decorator = myComponents.get(myRowUnderMouse);
if (decorator != null) {
decorator.onMouseEntered(e);
repaintRows(myRowUnderMouse, myRowUnderMouse, false);
}
}
public void runOperationIgnoreSelectionChange(@NotNull Runnable task) {
mySkipSelectionChange = true;
try {
if (isEditing()) {
getCellEditor().stopCellEditing();
}
task.run();
}
finally {
mySkipSelectionChange = false;
refreshEditor();
}
}
private void onSelectionChange(@NotNull ListSelectionEvent e) {
if (mySkipSelectionChange || e.getValueIsAdjusting()) {
return;
}
refreshEditor();
}
public void removeRow(int rowIndex) {
if (rowIndex < myEditorRow) {
hideEditor();
}
getModel().removeRow(rowIndex);
}
public void refreshEditor() {
ArrangementMatchingRulesModel model = getModel();
if (myEditorRow >= model.getSize()) {
myEditorRow = -1;
for (int i = 0, max = model.getSize(); i < max; i++) {
if (model.getElementAt(i) instanceof ArrangementEditorComponent) {
myEditorRow = i;
break;
}
}
}
if (myEditorRow < 0) {
return;
}
ListSelectionModel selectionModel = getSelectionModel();
if (selectionModel.isSelectionEmpty()) {
hideEditor();
return;
}
int selectedRow = selectionModel.getMinSelectionIndex();
if (selectedRow != selectionModel.getMaxSelectionIndex()) {
// More than one row is selected.
hideEditor();
return;
}
if (selectedRow != myEditorRow && selectedRow != myEditorRow - 1) {
hideEditor();
}
}
public void hideEditor() {
if (myEditorRow < 0) {
return;
}
if (isEditing()) {
TableCellEditor editor = getCellEditor();
if (editor != null) {
editor.stopCellEditing();
}
}
mySkipSelectionChange = true;
try {
ArrangementMatchingRulesModel model = getModel();
model.removeRow(myEditorRow);
if (myEditorRow > 0 && model.getElementAt(myEditorRow - 1) instanceof EmptyArrangementRuleComponent) {
model.removeRow(myEditorRow - 1);
}
}
finally {
mySkipSelectionChange = false;
}
myEditorRow = -1;
}
private void onTableChange(@NotNull TableModelEvent e) {
final int signum;
switch (e.getType()) {
case TableModelEvent.INSERT:
signum = 1;
break;
case TableModelEvent.DELETE:
signum = -1;
for (int i = e.getLastRow(); i >= e.getFirstRow(); i--) {
myComponents.remove(i);
}
break;
default:
return;
}
int shift = Math.abs(e.getFirstRow() - e.getLastRow() + 1) * signum;
myComponents.shiftKeys(e.getFirstRow(), shift);
if (myRowUnderMouse >= e.getFirstRow()) {
myRowUnderMouse = -1;
}
if (getModel().getSize() > 0) {
repaintRows(0, getModel().getSize() - 1, false);
}
}
public void showEditor(int rowToEdit) {
showEditor(myEditor, rowToEdit);
}
public void scrollRowToVisible(int row) {
final Rectangle rect = getCellRect(row, 0, false);
if (row != getEditingRow() - 1) {
scrollRectToVisible(rect);
}
else {
final Rectangle editorRect = getCellRect(row + 1, 0, false);
if(!rect.isEmpty() && !editorRect.isEmpty()) {
final int height = (int)(rect.getHeight() + editorRect.getHeight());
final Rectangle visibleRect = new Rectangle((int)rect.getX(), (int)rect.getY(), (int)rect.getWidth(), height);
scrollRectToVisible(visibleRect);
}
}
}
public void showEditor(@NotNull ArrangementMatchingRuleEditor editor, int rowToEdit) {
if (myEditorRow == rowToEdit + 1) {
return;
}
if (myEditorRow >= 0) {
if (myEditorRow < rowToEdit) {
rowToEdit--;
}
hideEditor();
}
myEditorRow = rowToEdit + 1;
ArrangementEditorComponent editorComponent = new ArrangementEditorComponent(this, myEditorRow, editor);
int width = getBounds().width;
JScrollPane scrollPane = JBScrollPane.findScrollPane(getParent());
if (scrollPane != null) {
width -= scrollPane.getVerticalScrollBar().getWidth();
}
editorComponent.applyAvailableWidth(width);
editor.reset(rowToEdit);
mySkipSelectionChange = true;
try {
getModel().insertRow(myEditorRow, new Object[]{editorComponent});
}
finally {
mySkipSelectionChange = false;
}
Rectangle bounds = getRowsBounds(rowToEdit, myEditorRow);
if (bounds != null) {
myRepresentationCallback.ensureVisible(bounds);
}
// We can't just subscribe to the model modification events and update cached renderers automatically because we need to use
// the cached renderer on atom condition removal (via click on 'close' button). The model is modified immediately then but
// corresponding cached renderer is used for animation.
editorComponent.expand();
repaintRows(rowToEdit, getModel().getRowCount() - 1, false);
editCellAt(myEditorRow, 0);
}
public void repaintRows(int first, int last, boolean rowStructureChanged) {
for (int i = first; i <= last; i++) {
if (rowStructureChanged) {
myComponents.remove(i);
}
else {
setRowHeight(i, myRenderer.getRendererComponent(i).getPreferredSize().height);
}
}
getModel().fireTableRowsUpdated(first, last);
}
private Rectangle getRowsBounds(int first, int last) {
Rectangle firstRect = getCellRect(first, 0, true);
Rectangle lastRect = getCellRect(last, 0, true);
return new Rectangle(firstRect.x, firstRect.y, lastRect.width, lastRect.y + lastRect.height - firstRect.y);
}
/**
* @return selected model rows sorted in descending order
*/
@NotNull
public TIntArrayList getSelectedModelRows() {
mySelectedRows.clear();
int min = selectionModel.getMinSelectionIndex();
if (min >= 0) {
for (int i = selectionModel.getMaxSelectionIndex(); i >= min; i--) {
if ((myEditorRow >= 0 && i == myEditorRow - 1)
|| (i != myEditorRow && selectionModel.isSelectedIndex(i)))
{
mySelectedRows.add(i);
}
}
}
else if (myEditorRow > 0) {
mySelectedRows.add(myEditorRow - 1);
}
return mySelectedRows;
}
public int getRowByRenderer(@NotNull ArrangementListRowDecorator renderer) {
for (int i = 0, max = getModel().getSize(); i < max; i++) {
if (myComponents.get(i) == renderer) {
return i;
}
}
return -1;
}
public int getEmptyRowHeight() {
return myMinRowHeight;
}
@NotNull
private JComponent adjustHeight(@NotNull JComponent component, int row) {
int height = component.getPreferredSize().height;
if (height < myMinRowHeight) {
height = myMinRowHeight;
}
setRowHeight(row, height);
return component;
}
protected class MatchingRulesRendererBase implements TableCellRenderer {
public Component getRendererComponent(int row) {
return getTableCellRendererComponent(ArrangementMatchingRulesControl.this, getModel().getElementAt(row), false, false, row, 0);
}
public boolean allowModifications(StdArrangementMatchRule rule) {
return true;
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
if (isEditing() && getEditingRow() == row) {
return EMPTY_RENDERER;
}
if (value instanceof ArrangementRepresentationAware) {
return adjustHeight(((ArrangementRepresentationAware)value).getComponent(), row);
}
ArrangementListRowDecorator component = myComponents.get(row);
if (component == null) {
if (!(value instanceof StdArrangementMatchRule)) {
return EMPTY_RENDERER;
}
StdArrangementMatchRule rule = (StdArrangementMatchRule)value;
final boolean allowModifications = allowModifications(rule);
ArrangementUiComponent ruleComponent = myFactory.getComponent(rule.getMatcher().getCondition(), rule, allowModifications);
component = new ArrangementListRowDecorator(ruleComponent, ArrangementMatchingRulesControl.this);
component.setError(getValidator().validate(row));
myComponents.set(row, component);
}
component.setUnderMouse(myRowUnderMouse == row);
component.setRowIndex((myEditorRow >= 0 && row > myEditorRow) ? row : row + 1);
component.setSelected(getSelectionModel().isSelectedIndex(row) || (myEditorRow >= 0 && row == myEditorRow - 1));
component.setBeingEdited(myEditorRow >= 0 && myEditorRow == row + 1);
boolean showSortIcon = value instanceof StdArrangementMatchRule
&& StdArrangementTokens.Order.BY_NAME.equals(((StdArrangementMatchRule)value).getOrderType());
component.setShowSortIcon(showSortIcon);
return component.getUiComponent();
}
}
@SuppressWarnings("ConstantConditions")
private class MyEditor extends AbstractTableCellEditor {
private int myRow;
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
if (value instanceof ArrangementEditorAware) {
myRow = row;
return ((ArrangementEditorAware)value).getComponent();
}
return null;
}
@Override
public Object getCellEditorValue() {
return myRow < getModel().getSize() ? getModel().getElementAt(myRow) : null;
}
}
public interface RepresentationCallback {
void ensureVisible(@NotNull Rectangle r);
}
}