| /* |
| * 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.designer.propertyTable; |
| |
| import com.intellij.designer.model.ErrorInfo; |
| import com.intellij.designer.model.PropertiesContainer; |
| import com.intellij.designer.model.Property; |
| import com.intellij.designer.model.PropertyContext; |
| import com.intellij.designer.propertyTable.renderers.LabelPropertyRenderer; |
| import com.intellij.ide.ui.search.SearchUtil; |
| import com.intellij.lang.annotation.HighlightSeverity; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.colors.EditorColorsManager; |
| import com.intellij.openapi.editor.colors.TextAttributesKey; |
| import com.intellij.openapi.ui.ComboBox; |
| import com.intellij.openapi.ui.Messages; |
| import com.intellij.openapi.util.Comparing; |
| import com.intellij.openapi.util.Couple; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vcs.FileStatus; |
| import com.intellij.openapi.wm.IdeFocusManager; |
| import com.intellij.openapi.wm.ex.IdeFocusTraversalPolicy; |
| import com.intellij.ui.*; |
| import com.intellij.ui.table.JBTable; |
| import com.intellij.util.PairFunction; |
| import com.intellij.util.ThrowableRunnable; |
| import com.intellij.util.ui.UIUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import javax.swing.event.ChangeEvent; |
| import javax.swing.plaf.TableUI; |
| import javax.swing.table.AbstractTableModel; |
| import javax.swing.table.TableCellEditor; |
| import javax.swing.table.TableCellRenderer; |
| import javax.swing.table.TableColumnModel; |
| 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.text.MessageFormat; |
| import java.util.*; |
| import java.util.List; |
| |
| /** |
| * @author Alexander Lobas |
| */ |
| public abstract class PropertyTable extends JBTable { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.designer.propertyTable.PropertyTable"); |
| private static final Comparator<String> GROUP_COMPARATOR = new Comparator<String>() { |
| @Override |
| public int compare(String o1, String o2) { |
| return StringUtil.compare(o1, o2, true); |
| } |
| }; |
| private static final Comparator<Property> PROPERTY_COMPARATOR = new Comparator<Property>() { |
| @Override |
| public int compare(Property o1, Property o2) { |
| return StringUtil.compare(o1.getName(), o2.getName(), true); |
| } |
| }; |
| |
| private boolean mySorted; |
| private boolean myShowGroups; |
| private boolean myShowExpertProperties; |
| |
| private String[] myColumnNames = new String[]{"Property", "Value"}; |
| |
| private final TableSpeedSearch mySpeedSearch; |
| |
| private final AbstractTableModel myModel = new PropertyTableModel(); |
| protected List<PropertiesContainer> myContainers = Collections.emptyList(); |
| protected List<Property> myProperties = Collections.emptyList(); |
| protected final Set<String> myExpandedProperties = new HashSet<String>(); |
| |
| private boolean mySkipUpdate; |
| private boolean myStoppingEditing; |
| |
| private final TableCellRenderer myCellRenderer = new PropertyCellRenderer(); |
| private final PropertyCellEditor myCellEditor = new PropertyCellEditor(); |
| |
| private final PropertyEditorListener myPropertyEditorListener = new PropertyCellEditorListener(); |
| |
| public PropertyTable() { |
| setModel(myModel); |
| setSelectionMode(ListSelectionModel.SINGLE_SELECTION); |
| |
| setShowColumns(false); |
| setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); |
| |
| setShowVerticalLines(false); |
| setIntercellSpacing(new Dimension(0, 1)); |
| setGridColor(UIUtil.getSlightlyDarkerColor(getBackground())); |
| |
| setColumnSelectionAllowed(false); |
| setCellSelectionEnabled(false); |
| setRowSelectionAllowed(true); |
| |
| addMouseListener(new MouseTableListener()); |
| |
| mySpeedSearch = new TableSpeedSearch(this, new PairFunction<Object, Cell, String>() { |
| @Override |
| public String fun(Object object, Cell cell) { |
| if (cell.column != 0) return null; |
| if (object instanceof GroupProperty) return null; |
| return ((Property)object).getName(); |
| } |
| }) { |
| @Override |
| protected void selectElement(Object element, String selectedText) { |
| super.selectElement(element, selectedText); |
| repaint(PropertyTable.this.getVisibleRect()); |
| } |
| }; |
| mySpeedSearch.setComparator(new SpeedSearchComparator(false, false)); |
| |
| // TODO: Updates UI after LAF updated |
| } |
| |
| public void setColumnNames(String... columnNames) { |
| if (columnNames.length != 2) throw new IllegalArgumentException("Invalid number of columns. Expected 2, got " + columnNames.length); |
| myColumnNames = columnNames; |
| |
| TableColumnModel mmodel = getColumnModel(); |
| for (int i = 0; i < columnNames.length; i++) { |
| mmodel.getColumn(i).setHeaderValue(columnNames[i]); |
| } |
| } |
| |
| public void setSorted(boolean sorted) { |
| mySorted = sorted; |
| update(); |
| } |
| |
| public boolean isSorted() { |
| return mySorted; |
| } |
| |
| public void setShowGroups(boolean showGroups) { |
| myShowGroups = showGroups; |
| update(); |
| } |
| |
| public boolean isShowGroups() { |
| return myShowGroups; |
| } |
| |
| public void showExpert(boolean showExpert) { |
| myShowExpertProperties = showExpert; |
| update(); |
| } |
| |
| public boolean isShowExpertProperties() { |
| return myShowExpertProperties; |
| } |
| |
| public void setUI(TableUI ui) { |
| super.setUI(ui); |
| |
| // Customize action and input maps |
| ActionMap actionMap = getActionMap(); |
| |
| setFocusTraversalKeys( |
| KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, |
| KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)); |
| setFocusTraversalKeys( |
| KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, |
| KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)); |
| |
| InputMap focusedInputMap = getInputMap(JComponent.WHEN_FOCUSED); |
| InputMap ancestorInputMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); |
| |
| actionMap.put("selectPreviousRow", new MySelectNextPreviousRowAction(false)); |
| actionMap.put("selectNextRow", new MySelectNextPreviousRowAction(true)); |
| |
| actionMap.put("startEditing", new MyStartEditingAction()); |
| focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0), "startEditing"); |
| ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0)); |
| |
| actionMap.put("smartEnter", new MyEnterAction()); |
| focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "smartEnter"); |
| ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)); |
| |
| focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel"); |
| ancestorInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel"); |
| |
| actionMap.put("restoreDefault", new MyRestoreDefaultAction()); |
| focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "restoreDefault"); |
| ancestorInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "restoreDefault"); |
| focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), "restoreDefault"); |
| ancestorInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), "restoreDefault"); |
| |
| actionMap.put("expandCurrent", new MyExpandCurrentAction(true, false)); |
| focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0), "expandCurrent"); |
| ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0)); |
| |
| actionMap.put("expandCurrentRight", new MyExpandCurrentAction(true, true)); |
| focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "expandCurrentRight"); |
| ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0)); |
| focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_KP_RIGHT, 0), "expandCurrentRight"); |
| ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_KP_RIGHT, 0)); |
| |
| actionMap.put("collapseCurrent", new MyExpandCurrentAction(false, false)); |
| focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0), "collapseCurrent"); |
| ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0)); |
| |
| actionMap.put("collapseCurrentLeft", new MyExpandCurrentAction(false, true)); |
| focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "collapseCurrentLeft"); |
| ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0)); |
| focusedInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_KP_LEFT, 0), "collapseCurrentLeft"); |
| ancestorInputMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_KP_LEFT, 0)); |
| } |
| |
| public TableCellRenderer getCellRenderer(int row, int column) { |
| return myCellRenderer; |
| } |
| |
| public void restoreDefaultValue() { |
| final Property property = getSelectionProperty(); |
| if (property != null) { |
| if (isEditing()) { |
| cellEditor.stopCellEditing(); |
| } |
| |
| doRestoreDefault(new ThrowableRunnable<Exception>() { |
| @Override |
| public void run() throws Exception { |
| for (PropertiesContainer component : myContainers) { |
| if (!property.isDefaultRecursively(component)) { |
| property.setDefaultValue(component); |
| } |
| } |
| } |
| }); |
| |
| repaint(); |
| } |
| } |
| |
| protected abstract boolean doRestoreDefault(ThrowableRunnable<Exception> runnable); |
| |
| @Nullable |
| public ErrorInfo getErrorInfoForRow(int row) { |
| if (myContainers.size() != 1) { |
| return null; |
| } |
| |
| Property property = myProperties.get(row); |
| if (property.getParent() != null) { |
| return null; |
| } |
| |
| for (ErrorInfo errorInfo : getErrors(myContainers.get(0))) { |
| if (property.getName().equals(errorInfo.getPropertyName())) { |
| return errorInfo; |
| } |
| } |
| return null; |
| } |
| |
| protected abstract List<ErrorInfo> getErrors(@NotNull PropertiesContainer container); |
| |
| @Override |
| public String getToolTipText(MouseEvent event) { |
| int row = rowAtPoint(event.getPoint()); |
| if (row != -1 && !myProperties.isEmpty()) { |
| ErrorInfo errorInfo = getErrorInfoForRow(row); |
| if (errorInfo != null) { |
| return errorInfo.getName(); |
| } |
| if (columnAtPoint(event.getPoint()) == 0) { |
| String tooltip = myProperties.get(row).getTooltip(); |
| if (tooltip != null) { |
| return tooltip; |
| } |
| } |
| } |
| return super.getToolTipText(event); |
| } |
| |
| ////////////////////////////////////////////////////////////////////////////////////////// |
| // |
| // |
| // |
| ////////////////////////////////////////////////////////////////////////////////////////// |
| |
| @Nullable |
| protected PropertyContext getPropertyContext() { |
| return null; |
| } |
| |
| public void update() { |
| update(myContainers, null); |
| } |
| |
| public void update(@NotNull List<? extends PropertiesContainer> containers, @Nullable Property initialSelection) { |
| update(containers, initialSelection, true); |
| } |
| |
| private void update(@NotNull List<? extends PropertiesContainer> containers, @Nullable Property initialSelection, boolean finishEditing) { |
| if (finishEditing) { |
| finishEditing(); |
| } |
| |
| if (mySkipUpdate) { |
| return; |
| } |
| mySkipUpdate = true; |
| |
| try { |
| if (finishEditing && isEditing()) { |
| cellEditor.stopCellEditing(); |
| } |
| |
| Property selection = initialSelection != null ? initialSelection : getSelectionProperty(); |
| myContainers = new ArrayList<PropertiesContainer>(containers); |
| fillProperties(); |
| myModel.fireTableDataChanged(); |
| |
| restoreSelection(selection); |
| } |
| finally { |
| mySkipUpdate = false; |
| } |
| } |
| |
| private void sortPropertiesAndCreateGroups(List<Property> rootProperties) { |
| if (!mySorted && !myShowGroups) return; |
| |
| Collections.sort(rootProperties, new Comparator<Property>() { |
| @Override |
| public int compare(Property o1, Property o2) { |
| if (o1.getParent() != null || o2.getParent() != null) { |
| if (o1.getParent() == o2) return -1; |
| if (o2.getParent() == o1) return 1; |
| return 0; |
| } |
| |
| if (myShowGroups) { |
| int result = getGroupComparator().compare(o1.getGroup(), o2.getGroup()); |
| if (result != 0) return result; |
| } |
| return mySorted ? getPropertyComparator().compare(o1, o2) : 0; |
| } |
| }); |
| |
| if (myShowGroups) { |
| for (int i = 0; i < rootProperties.size() - 1; i++) { |
| Property prev = i == 0 ? null : rootProperties.get(i - 1); |
| Property each = rootProperties.get(i); |
| |
| String eachGroup = each.getGroup(); |
| String prevGroup = prev == null ? null : prev.getGroup(); |
| |
| if (prevGroup != null || eachGroup != null) { |
| if (!StringUtil.equalsIgnoreCase(eachGroup, prevGroup)) { |
| rootProperties.add(i, new GroupProperty(each.getGroup())); |
| i++; |
| } |
| } |
| } |
| } |
| } |
| |
| @NotNull |
| protected Comparator<String> getGroupComparator() { |
| return GROUP_COMPARATOR; |
| } |
| |
| @NotNull |
| protected Comparator<Property> getPropertyComparator() { |
| return PROPERTY_COMPARATOR; |
| } |
| |
| protected List<Property> getProperties(PropertiesContainer component) { |
| return component.getProperties(); |
| } |
| |
| private void restoreSelection(Property selection) { |
| List<Property> propertyPath = new ArrayList<Property>(2); |
| while (selection != null) { |
| propertyPath.add(0, selection); |
| selection = selection.getParent(); |
| } |
| |
| int indexToSelect = -1; |
| int size = propertyPath.size(); |
| for (int i = 0; i < size; i++) { |
| int index = findFullPathProperty(myProperties, propertyPath.get(i)); |
| if (index == -1) { |
| break; |
| } |
| if (i == size - 1) { |
| indexToSelect = index; |
| } |
| else { |
| expand(index); |
| } |
| } |
| |
| if (indexToSelect != -1) { |
| getSelectionModel().setSelectionInterval(indexToSelect, indexToSelect); |
| } |
| else if (getRowCount() > 0) { |
| indexToSelect = 0; |
| for (int i = 0; i < myProperties.size(); i++) { |
| if (!(myProperties.get(i) instanceof GroupProperty)) { |
| indexToSelect = i; |
| break; |
| } |
| } |
| getSelectionModel().setSelectionInterval(indexToSelect, indexToSelect); |
| } |
| TableUtil.scrollSelectionToVisible(this); |
| } |
| |
| private void fillProperties() { |
| myProperties = new ArrayList<Property>(); |
| int size = myContainers.size(); |
| |
| if (size > 0) { |
| List<Property> rootProperties = new ArrayList<Property>(); |
| for (Property each : (Iterable<? extends Property>)getProperties(myContainers.get(0))) { |
| addIfNeeded(getCurrentComponent(), each, rootProperties); |
| } |
| sortPropertiesAndCreateGroups(rootProperties); |
| |
| for (Property property : rootProperties) { |
| myProperties.add(property); |
| addExpandedChildren(getCurrentComponent(), property, myProperties); |
| } |
| |
| if (size > 1) { |
| for (Iterator<Property> I = myProperties.iterator(); I.hasNext(); ) { |
| if (!I.next().availableFor(myContainers)) { |
| I.remove(); |
| } |
| } |
| |
| for (int i = 1; i < size; i++) { |
| List<Property> otherProperties = new ArrayList<Property>(); |
| fillProperties(myContainers.get(i), otherProperties); |
| |
| for (Iterator<Property> I = myProperties.iterator(); I.hasNext(); ) { |
| Property addedProperty = I.next(); |
| |
| int index = findFullPathProperty(otherProperties, addedProperty); |
| if (index == -1) { |
| I.remove(); |
| continue; |
| } |
| |
| Property testProperty = otherProperties.get(index); |
| if (!addedProperty.getClass().equals(testProperty.getClass())) { |
| I.remove(); |
| continue; |
| } |
| |
| List<Property> addedChildren = getChildren(addedProperty); |
| List<Property> testChildren = getChildren(testProperty); |
| int addedChildrenSize = addedChildren.size(); |
| |
| if (addedChildrenSize != testChildren.size()) { |
| I.remove(); |
| continue; |
| } |
| |
| for (int j = 0; j < addedChildrenSize; j++) { |
| if (!addedChildren.get(j).getName().equals(testChildren.get(j).getName())) { |
| I.remove(); |
| break; |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| private void fillProperties(PropertiesContainer<?> component, List<Property> properties) { |
| for (Property each : getProperties(component)) { |
| if (addIfNeeded(component, each, properties)) { |
| addExpandedChildren(component, each, properties); |
| } |
| } |
| } |
| |
| private void addExpandedChildren(PropertiesContainer<?> component, Property property, List<Property> properties) { |
| if (isExpanded(property)) { |
| for (Property child : getChildren(property)) { |
| if (addIfNeeded(component, child, properties)) { |
| addExpandedChildren(component, child, properties); |
| } |
| } |
| } |
| } |
| |
| private boolean addIfNeeded(PropertiesContainer<?> component, Property property, List<Property> properties) { |
| if (property.isExpert() && !myShowExpertProperties) { |
| try { |
| if (property.isDefaultRecursively(component)) { |
| return false; |
| } |
| } |
| catch (Throwable ignore) { |
| } |
| } |
| properties.add(property); |
| return true; |
| } |
| |
| @Nullable |
| public static Property findProperty(List<Property> properties, String name) { |
| for (Property property : properties) { |
| if (name.equals(property.getName())) { |
| return property; |
| } |
| } |
| return null; |
| } |
| |
| public static int findProperty(List<Property> properties, Property property) { |
| String name = property.getName(); |
| int size = properties.size(); |
| |
| for (int i = 0; i < size; i++) { |
| Property nextProperty = properties.get(i); |
| if (Comparing.equal(nextProperty.getGroup(), property.getGroup()) && name.equals(nextProperty.getName())) { |
| return i; |
| } |
| } |
| |
| return -1; |
| } |
| |
| private static int findFullPathProperty(List<Property> properties, Property property) { |
| Property parent = property.getParent(); |
| if (parent == null) { |
| return findProperty(properties, property); |
| } |
| |
| String name = getFullPathName(property); |
| int size = properties.size(); |
| |
| for (int i = 0; i < size; i++) { |
| if (name.equals(getFullPathName(properties.get(i)))) { |
| return i; |
| } |
| } |
| |
| return -1; |
| } |
| |
| private static String getFullPathName(Property property) { |
| StringBuilder builder = new StringBuilder(); |
| for (; property != null; property = property.getParent()) { |
| builder.insert(0, ".").insert(0, property.getName()); |
| } |
| return builder.toString(); |
| } |
| |
| public static void moveProperty(List<Property> source, String name, List<Property> destination, int index) { |
| Property property = extractProperty(source, name); |
| if (property != null) { |
| if (index == -1) { |
| destination.add(property); |
| } |
| else { |
| destination.add(index, property); |
| } |
| } |
| } |
| |
| @Nullable |
| public static Property extractProperty(List<Property> properties, String name) { |
| int size = properties.size(); |
| for (int i = 0; i < size; i++) { |
| if (name.equals(properties.get(i).getName())) { |
| return properties.remove(i); |
| } |
| } |
| return null; |
| } |
| |
| @Nullable |
| public Property getSelectionProperty() { |
| int selectedRow = getSelectedRow(); |
| if (selectedRow >= 0 && selectedRow < myProperties.size()) { |
| return myProperties.get(selectedRow); |
| } |
| return null; |
| } |
| |
| @Nullable |
| private PropertiesContainer getCurrentComponent() { |
| return myContainers.size() == 1 ? myContainers.get(0) : null; |
| } |
| |
| private List<Property> getChildren(Property property) { |
| return property.getChildren(getCurrentComponent()); |
| } |
| |
| private List<Property> getFilterChildren(Property property) { |
| List<Property> properties = new ArrayList<Property>(getChildren(property)); |
| for (Iterator<Property> I = properties.iterator(); I.hasNext(); ) { |
| Property child = I.next(); |
| if (child.isExpert() && !myShowExpertProperties) { |
| I.remove(); |
| } |
| } |
| return properties; |
| } |
| |
| public boolean isDefault(Property property) throws Exception { |
| for (PropertiesContainer component : myContainers) { |
| if (!property.isDefaultRecursively(component)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| @Nullable |
| protected final Object getValue(Property property) throws Exception { |
| int size = myContainers.size(); |
| if (size == 0) { |
| return null; |
| } |
| |
| Object value = property.getValue(myContainers.get(0)); |
| for (int i = 1; i < size; i++) { |
| if (!Comparing.equal(value, property.getValue(myContainers.get(i)))) { |
| return null; |
| } |
| } |
| |
| return value; |
| } |
| |
| private boolean isExpanded(Property property) { |
| return myExpandedProperties.contains(property.getPath()); |
| } |
| |
| private void collapse(int rowIndex) { |
| int selectedRow = getSelectedRow(); |
| Property property = myProperties.get(rowIndex); |
| |
| int size = collapse(property, rowIndex + 1); |
| LOG.assertTrue(size > 0); |
| myModel.fireTableDataChanged(); |
| |
| if (selectedRow != -1) { |
| if (selectedRow > rowIndex) { |
| selectedRow -= size; |
| } |
| |
| getSelectionModel().setSelectionInterval(selectedRow, selectedRow); |
| } |
| } |
| |
| private int collapse(Property property, int startIndex) { |
| int totalSize = 0; |
| if (myExpandedProperties.remove(property.getPath())) { |
| int size = getFilterChildren(property).size(); |
| totalSize += size; |
| for (int i = 0; i < size; i++) { |
| totalSize += collapse(myProperties.remove(startIndex), startIndex); |
| } |
| } |
| return totalSize; |
| } |
| |
| private void expand(int rowIndex) { |
| int selectedRow = getSelectedRow(); |
| Property property = myProperties.get(rowIndex); |
| String path = property.getPath(); |
| |
| if (myExpandedProperties.contains(path)) { |
| return; |
| } |
| myExpandedProperties.add(path); |
| |
| List<Property> properties = getFilterChildren(property); |
| myProperties.addAll(rowIndex + 1, properties); |
| |
| myModel.fireTableDataChanged(); |
| |
| if (selectedRow != -1) { |
| if (selectedRow > rowIndex) { |
| selectedRow += properties.size(); |
| } |
| |
| getSelectionModel().setSelectionInterval(selectedRow, selectedRow); |
| } |
| |
| Rectangle rectStart = getCellRect(selectedRow, 0, true); |
| Rectangle rectEnd = getCellRect(selectedRow + properties.size(), 0, true); |
| scrollRectToVisible( |
| new Rectangle(rectStart.x, rectStart.y, rectEnd.x + rectEnd.width - rectStart.x, rectEnd.y + rectEnd.height - rectStart.y)); |
| } |
| |
| ////////////////////////////////////////////////////////////////////////////////////////// |
| // |
| // |
| // |
| ////////////////////////////////////////////////////////////////////////////////////////// |
| |
| @Override |
| public void setValueAt(Object aValue, int row, int column) { |
| Property property = myProperties.get(row); |
| super.setValueAt(aValue, row, column); |
| |
| if (property.needRefreshPropertyList()) { |
| update(); |
| } |
| |
| repaint(); |
| } |
| |
| @Override |
| public TableCellEditor getCellEditor(int row, int column) { |
| PropertyEditor editor = myProperties.get(row).getEditor(); |
| editor.removePropertyEditorListener(myPropertyEditorListener); // reorder listener (first) |
| editor.addPropertyEditorListener(myPropertyEditorListener); |
| myCellEditor.setEditor(editor); |
| return myCellEditor; |
| } |
| |
| /* |
| * This method is overriden due to bug in the JTree. The problem is that |
| * JTree does not properly repaint edited cell if the editor is opaque or |
| * has opaque child components. |
| */ |
| public boolean editCellAt(int row, int column, EventObject e) { |
| boolean result = super.editCellAt(row, column, e); |
| repaint(getCellRect(row, column, true)); |
| return result; |
| } |
| |
| private void startEditing(int index) { |
| startEditing(index, false); |
| } |
| |
| private void startEditing(int index, boolean startedWithKeyboard) { |
| final PropertyEditor editor = myProperties.get(index).getEditor(); |
| if (editor == null) { |
| return; |
| } |
| |
| editCellAt(index, convertColumnIndexToView(1)); |
| LOG.assertTrue(editorComp != null); |
| |
| JComponent preferredComponent = editor.getPreferredFocusedComponent(); |
| if (preferredComponent == null) { |
| preferredComponent = IdeFocusTraversalPolicy.getPreferredFocusedComponent((JComponent)editorComp); |
| } |
| if (preferredComponent != null) { |
| preferredComponent.requestFocusInWindow(); |
| } |
| |
| if (startedWithKeyboard) { |
| // waiting for focus is necessary in case, if 'activate' opens dialog. If we don't wait for focus, after the dialog is shown we'll |
| // end up with the table focused instead of the dialog |
| IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(new Runnable() { |
| @Override |
| public void run() { |
| editor.activate(); |
| } |
| }); |
| } |
| } |
| |
| private void finishEditing() { |
| if (editingRow != -1) { |
| editingStopped(null); |
| } |
| } |
| |
| public void editingStopped(@Nullable ChangeEvent event) { |
| if (myStoppingEditing) { |
| return; |
| } |
| myStoppingEditing = true; |
| |
| LOG.assertTrue(isEditing()); |
| LOG.assertTrue(editingRow != -1); |
| |
| PropertyEditor editor = myProperties.get(editingRow).getEditor(); |
| editor.removePropertyEditorListener(myPropertyEditorListener); |
| |
| try { |
| setValueAt(editor.getValue(), editingRow, editingColumn); |
| } |
| catch (Exception e) { |
| showInvalidInput(e); |
| } |
| finally { |
| removeEditor(); |
| myStoppingEditing = false; |
| } |
| } |
| |
| @Override |
| public void removeEditor() { |
| super.removeEditor(); |
| updateEditActions(); |
| } |
| |
| protected void updateEditActions() { |
| } |
| |
| private boolean setValueAtRow(int row, final Object newValue) { |
| final Property property = myProperties.get(row); |
| |
| final Object[] oldValue = new Object[1]; |
| boolean isNewValue; |
| try { |
| oldValue[0] = getValue(property); |
| isNewValue = !Comparing.equal(oldValue[0], newValue); |
| if (newValue == null && oldValue[0] instanceof String && ((String)oldValue[0]).length() == 0) { |
| isNewValue = false; |
| } |
| } |
| catch (Throwable e) { |
| isNewValue = true; |
| } |
| |
| boolean isSetValue = true; |
| final boolean[] needRefresh = new boolean[1]; |
| |
| if (isNewValue) { |
| isSetValue = doSetValue(new ThrowableRunnable<Exception>() { |
| @Override |
| public void run() throws Exception { |
| for (PropertiesContainer component : myContainers) { |
| property.setValue(component, newValue); |
| needRefresh[0] |= property.needRefreshPropertyList(component, oldValue[0], newValue); |
| } |
| } |
| }); |
| } |
| |
| if (isSetValue) { |
| if (property.needRefreshPropertyList() || needRefresh[0]) { |
| update(myContainers, null, property.closeEditorDuringRefresh()); |
| } |
| else { |
| myModel.fireTableRowsUpdated(row, row); |
| } |
| } |
| |
| return isSetValue; |
| } |
| |
| protected abstract boolean doSetValue(ThrowableRunnable<Exception> runnable); |
| |
| private static void showInvalidInput(Exception e) { |
| Throwable cause = e.getCause(); |
| String message = cause == null ? e.getMessage() : cause.getMessage(); |
| |
| if (message == null || message.length() == 0) { |
| message = "No message"; |
| } |
| |
| Messages.showMessageDialog(MessageFormat.format("Error setting value: {0}", message), |
| "Invalid Input", |
| Messages.getErrorIcon()); |
| } |
| |
| ////////////////////////////////////////////////////////////////////////////////////////// |
| // |
| // |
| // |
| ////////////////////////////////////////////////////////////////////////////////////////// |
| |
| /** |
| * Reimplementation of LookAndFeel's SelectNextRowAction action. |
| * Standard implementation isn't smart enough. |
| * |
| * @see javax.swing.plaf.basic.BasicTableUI |
| */ |
| private class MySelectNextPreviousRowAction extends AbstractAction { |
| private boolean selectNext; |
| |
| private MySelectNextPreviousRowAction(boolean selectNext) { |
| this.selectNext = selectNext; |
| } |
| |
| public void actionPerformed(ActionEvent e) { |
| int rowCount = getRowCount(); |
| LOG.assertTrue(rowCount > 0); |
| |
| int selectedRow = getSelectedRow(); |
| if (selectedRow == -1) { |
| selectedRow = 0; |
| } |
| else { |
| if (selectNext) { |
| selectedRow = Math.min(rowCount - 1, getSelectedRow() + 1); |
| } |
| else { |
| selectedRow = Math.max(0, selectedRow - 1); |
| } |
| } |
| |
| if (isEditing()) { |
| finishEditing(); |
| getSelectionModel().setSelectionInterval(selectedRow, selectedRow); |
| scrollRectToVisible(getCellRect(selectedRow, 0, true)); |
| startEditing(selectedRow); |
| } |
| else { |
| getSelectionModel().setSelectionInterval(selectedRow, selectedRow); |
| scrollRectToVisible(getCellRect(selectedRow, 0, true)); |
| } |
| } |
| } |
| |
| /** |
| * Reimplementation of LookAndFeel's StartEditingAction action. |
| * Standard implementation isn't smart enough. |
| * |
| * @see javax.swing.plaf.basic.BasicTableUI |
| */ |
| private class MyStartEditingAction extends AbstractAction { |
| public void actionPerformed(ActionEvent e) { |
| int selectedRow = getSelectedRow(); |
| if (selectedRow == -1 || isEditing()) { |
| return; |
| } |
| |
| startEditing(selectedRow, true); |
| } |
| } |
| |
| private class MyEnterAction extends AbstractAction { |
| public void actionPerformed(ActionEvent e) { |
| int selectedRow = getSelectedRow(); |
| if (isEditing() || selectedRow == -1) { |
| return; |
| } |
| |
| Property property = myProperties.get(selectedRow); |
| if (!getChildren(property).isEmpty()) { |
| if (isExpanded(property)) { |
| collapse(selectedRow); |
| } |
| else { |
| expand(selectedRow); |
| } |
| } |
| else { |
| startEditing(selectedRow, true); |
| } |
| } |
| } |
| |
| private class MyExpandCurrentAction extends AbstractAction { |
| private final boolean myExpand; |
| private final boolean mySelect; |
| |
| public MyExpandCurrentAction(boolean expand, boolean select) { |
| myExpand = expand; |
| mySelect = select; |
| } |
| |
| public void actionPerformed(ActionEvent e) { |
| int selectedRow = getSelectedRow(); |
| if (isEditing() || selectedRow == -1) { |
| return; |
| } |
| |
| Property property = myProperties.get(selectedRow); |
| List<Property> children = getChildren(property); |
| if (!children.isEmpty()) { |
| if (myExpand) { |
| if (!isExpanded(property)) { |
| expand(selectedRow); |
| } |
| else if (mySelect) { |
| restoreSelection(children.get(0)); |
| } |
| } |
| else if (isExpanded(property)) { |
| collapse(selectedRow); |
| } |
| else if (mySelect) { |
| Property parent = property.getParent(); |
| if (parent != null) { |
| restoreSelection(parent); |
| } |
| } |
| } |
| else if (!myExpand && mySelect) { |
| Property parent = property.getParent(); |
| if (parent != null) { |
| restoreSelection(parent); |
| } |
| } |
| } |
| } |
| |
| private class MyRestoreDefaultAction extends AbstractAction { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| restoreDefaultValue(); |
| } |
| } |
| |
| private class MouseTableListener extends MouseAdapter { |
| @Override |
| public void mousePressed(MouseEvent e) { |
| int row = rowAtPoint(e.getPoint()); |
| if (row == -1) { |
| return; |
| } |
| |
| Property property = myProperties.get(row); |
| if (getChildren(property).isEmpty()) return; |
| |
| Icon icon = UIUtil.getTreeNodeIcon(false, true, true); |
| |
| Rectangle rect = getCellRect(row, convertColumnIndexToView(0), false); |
| int indent = getBeforeIconAndAfterIndents(property, icon).first; |
| if (e.getX() < rect.x + indent || |
| e.getX() > rect.x + indent + icon.getIconWidth() || |
| e.getY() < rect.y || |
| e.getY() > rect.y + rect.height) { |
| return; |
| } |
| |
| // TODO: disallow selection for this row |
| if (isExpanded(property)) { |
| collapse(row); |
| } |
| else { |
| expand(row); |
| } |
| } |
| } |
| |
| private class PropertyTableModel extends AbstractTableModel { |
| @Override |
| public int getColumnCount() { |
| return myColumnNames.length; |
| } |
| |
| @Override |
| public String getColumnName(int column) { |
| return myColumnNames[column]; |
| } |
| |
| public boolean isCellEditable(int row, int column) { |
| return column == 1 && myProperties.get(row).isEditable(getCurrentComponent()); |
| } |
| |
| @Override |
| public int getRowCount() { |
| return myProperties.size(); |
| } |
| |
| @Override |
| public Object getValueAt(int rowIndex, int columnIndex) { |
| return myProperties.get(rowIndex); |
| } |
| |
| @Override |
| public void setValueAt(Object aValue, int rowIndex, int columnIndex) { |
| setValueAtRow(rowIndex, aValue); |
| } |
| } |
| |
| private static int getDepth(@NotNull Property property) { |
| int result = 0; |
| for (Property each = property.getParent(); each != null; each = each.getParent(), result++) { |
| // empty |
| } |
| return result; |
| } |
| |
| @NotNull |
| private static Couple<Integer> getBeforeIconAndAfterIndents(@NotNull Property property, @NotNull Icon icon) { |
| int nodeIndent = UIUtil.getTreeLeftChildIndent() + UIUtil.getTreeRightChildIndent(); |
| int beforeIcon = nodeIndent * getDepth(property); |
| |
| int leftIconOffset = Math.max(0, UIUtil.getTreeLeftChildIndent() - (icon.getIconWidth() / 2)); |
| beforeIcon += leftIconOffset; |
| |
| int afterIcon = Math.max(0, nodeIndent - leftIconOffset - icon.getIconWidth()); |
| |
| return Couple.of(beforeIcon, afterIcon); |
| } |
| |
| private class PropertyCellEditorListener implements PropertyEditorListener { |
| @Override |
| public void valueCommitted(PropertyEditor source, boolean continueEditing, boolean closeEditorOnError) { |
| if (isEditing()) { |
| Object value; |
| TableCellEditor tableCellEditor = cellEditor; |
| |
| try { |
| value = tableCellEditor.getCellEditorValue(); |
| } |
| catch (Exception e) { |
| showInvalidInput(e); |
| return; |
| } |
| |
| if (setValueAtRow(editingRow, value)) { |
| if (!continueEditing && editingRow != -1) { |
| PropertyEditor editor = myProperties.get(editingRow).getEditor(); |
| editor.removePropertyEditorListener(myPropertyEditorListener); |
| removeEditor(); |
| } |
| } |
| else if (closeEditorOnError) { |
| tableCellEditor.cancelCellEditing(); |
| } |
| } |
| } |
| |
| @Override |
| public void editingCanceled(PropertyEditor source) { |
| if (isEditing()) { |
| cellEditor.cancelCellEditing(); |
| } |
| } |
| |
| @Override |
| public void preferredSizeChanged(PropertyEditor source) { |
| } |
| } |
| |
| private class PropertyCellEditor extends AbstractCellEditor implements TableCellEditor { |
| private PropertyEditor myEditor; |
| |
| public void setEditor(PropertyEditor editor) { |
| myEditor = editor; |
| } |
| |
| @Override |
| public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { |
| try { |
| JComponent component = myEditor.getComponent(getCurrentComponent(), getPropertyContext(), getValue((Property)value), null); |
| |
| if (component instanceof JComboBox) { |
| ComboBox.registerTableCellEditor((JComboBox)component, this); |
| } |
| else if (component instanceof JCheckBox) { |
| component.putClientProperty("JComponent.sizeVariant", UIUtil.isUnderAquaLookAndFeel() ? "small" : null); |
| } |
| |
| return component; |
| } |
| catch (Throwable e) { |
| LOG.debug(e); |
| SimpleColoredComponent errComponent = new SimpleColoredComponent(); |
| errComponent |
| .append(MessageFormat.format("Error getting value: {0}", e.getMessage()), SimpleTextAttributes.ERROR_ATTRIBUTES); |
| return errComponent; |
| } |
| finally { |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| public void run() { |
| updateEditActions(); |
| } |
| }); |
| } |
| } |
| |
| @Override |
| public Object getCellEditorValue() { |
| try { |
| return myEditor.getValue(); |
| } |
| catch (Exception e) { |
| throw new RuntimeException(e); |
| } |
| } |
| } |
| |
| public static void updateRenderer(JComponent component, boolean selected) { |
| if (selected) { |
| component.setForeground(UIUtil.getTableSelectionForeground()); |
| component.setBackground(UIUtil.getTableSelectionBackground()); |
| } |
| else { |
| component.setForeground(UIUtil.getTableForeground()); |
| component.setBackground(UIUtil.getTableBackground()); |
| } |
| } |
| |
| @NotNull |
| protected abstract TextAttributesKey getErrorAttributes(@NotNull HighlightSeverity severity); |
| |
| private class PropertyCellRenderer implements TableCellRenderer { |
| private final ColoredTableCellRenderer myCellRenderer; |
| private final ColoredTableCellRenderer myGroupRenderer; |
| |
| private PropertyCellRenderer() { |
| myCellRenderer = new MyCellRenderer(); |
| myGroupRenderer = new MyCellRenderer() { |
| private boolean mySelected; |
| public boolean myDrawTopLine; |
| |
| |
| @Override |
| protected void customizeCellRenderer(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) { |
| super.customizeCellRenderer(table, value, selected, hasFocus, row, column); |
| mySelected = selected; |
| myDrawTopLine = row > 0; |
| } |
| |
| @Override |
| protected void paintBackground(Graphics2D g, int x, int width, int height) { |
| if (mySelected) { |
| super.paintBackground(g, x, width, height); |
| } |
| else { |
| UIUtil.drawHeader(g, x, width, height, true, myDrawTopLine); |
| } |
| } |
| }; |
| } |
| |
| @Override |
| public Component getTableCellRendererComponent(JTable table, |
| Object value, |
| boolean selected, |
| boolean cellHasFocus, |
| int row, |
| int column) { |
| column = table.convertColumnIndexToModel(column); |
| Property property = (Property)value; |
| Color background = table.getBackground(); |
| |
| Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); |
| boolean tableHasFocus = focusOwner != null && SwingUtilities.isDescendingFrom(focusOwner, table); |
| |
| ColoredTableCellRenderer renderer = property instanceof GroupProperty ? myGroupRenderer : myCellRenderer; |
| |
| renderer.getTableCellRendererComponent(table, value, selected, cellHasFocus, row, column); |
| renderer.setBackground(selected ? UIUtil.getTreeSelectionBackground(tableHasFocus) : background); |
| |
| if (property instanceof GroupProperty) { |
| renderer.setIpad(new Insets(0, 5, 0, 0)); |
| if (column == 0) { |
| renderer.append(property.getName()); |
| } |
| return renderer; |
| } |
| |
| boolean isDefault = true; |
| try { |
| for (PropertiesContainer container : myContainers) { |
| if (!property.showAsDefault(container)) { |
| isDefault = false; |
| break; |
| } |
| } |
| } |
| catch (Exception e) { |
| LOG.debug(e); |
| } |
| |
| renderer.clear(); |
| |
| if (column == 0) { |
| SimpleTextAttributes attr = SimpleTextAttributes.REGULAR_ATTRIBUTES; |
| |
| if (!selected && !isDefault) { |
| attr = attr.derive(-1, FileStatus.MODIFIED.getColor(), null, null); |
| } |
| if (property.isImportant()) { |
| attr = attr.derive(attr.getStyle() | SimpleTextAttributes.STYLE_BOLD, null, null, null); |
| } |
| if (property.isExpert()) { |
| attr = attr.derive(attr.getStyle() | SimpleTextAttributes.STYLE_ITALIC, null, null, null); |
| } |
| if (property.isDeprecated()) { |
| attr = attr.derive(attr.getStyle() | SimpleTextAttributes.STYLE_STRIKEOUT, null, null, null); |
| } |
| |
| ErrorInfo errorInfo = getErrorInfoForRow(row); |
| if (errorInfo != null) { |
| SimpleTextAttributes template = SimpleTextAttributes.fromTextAttributes( |
| EditorColorsManager.getInstance().getGlobalScheme().getAttributes(getErrorAttributes(errorInfo.getLevel().getSeverity()))); |
| |
| int style = ((template.getStyle() & SimpleTextAttributes.STYLE_WAVED) != 0 ? SimpleTextAttributes.STYLE_WAVED : 0) |
| | ((template.getStyle() & SimpleTextAttributes.STYLE_UNDERLINE) != 0 ? SimpleTextAttributes.STYLE_UNDERLINE : 0); |
| attr = attr.derive(attr.getStyle() | style, template.getFgColor(), null, template.getWaveColor()); |
| } |
| |
| SearchUtil.appendFragments(mySpeedSearch.getEnteredPrefix(), property.getName(), |
| attr.getStyle(), attr.getFgColor(), attr.getBgColor(), renderer); |
| |
| Icon icon = UIUtil.getTreeNodeIcon(isExpanded(property), selected, tableHasFocus); |
| boolean hasChildren = !getChildren(property).isEmpty(); |
| |
| renderer.setIcon(hasChildren ? icon : null); |
| |
| Couple<Integer> indents = getBeforeIconAndAfterIndents(property, icon); |
| int indent = indents.first; |
| |
| if (hasChildren) { |
| renderer.setIconTextGap(indents.second); |
| } |
| else { |
| indent += icon.getIconWidth() + indents.second; |
| } |
| renderer.setIpad(new Insets(0, indent, 0, 0)); |
| |
| return renderer; |
| } |
| else { |
| try { |
| PropertyRenderer valueRenderer = property.getRenderer(); |
| JComponent component = |
| valueRenderer.getComponent(getCurrentComponent(), getPropertyContext(), getValue(property), selected, tableHasFocus); |
| |
| component.setBackground(selected ? UIUtil.getTreeSelectionBackground(tableHasFocus) : background); |
| component.setFont(table.getFont()); |
| |
| if (component instanceof JCheckBox) { |
| component.putClientProperty("JComponent.sizeVariant", UIUtil.isUnderAquaLookAndFeel() ? "small" : null); |
| } |
| |
| return component; |
| } |
| catch (Exception e) { |
| LOG.debug(e); |
| renderer.append(MessageFormat.format("Error getting value: {0}", e.getMessage()), SimpleTextAttributes.ERROR_ATTRIBUTES); |
| return renderer; |
| } |
| } |
| } |
| |
| private class MyCellRenderer extends ColoredTableCellRenderer { |
| protected void customizeCellRenderer(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) { |
| setPaintFocusBorder(false); |
| setFocusBorderAroundIcon(true); |
| } |
| } |
| } |
| |
| private static class GroupProperty extends Property { |
| public GroupProperty(@Nullable String name) { |
| super(null, StringUtil.notNullize(name)); |
| } |
| |
| @NotNull |
| @Override |
| public PropertyRenderer getRenderer() { |
| return new LabelPropertyRenderer(null); |
| } |
| |
| @Override |
| public PropertyEditor getEditor() { |
| return null; |
| } |
| } |
| } |