All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.intellij.designer.propertyTable.PropertyTable Maven / Gradle / Ivy

Go to download

A packaging of the IntelliJ Community Edition platform-impl library. This is release number 1 of trunk branch 142.

The newest version!
/*
 * 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 GROUP_COMPARATOR = new Comparator() {
    @Override
    public int compare(String o1, String o2) {
      return StringUtil.compare(o1, o2, true);
    }
  };
  private static final Comparator PROPERTY_COMPARATOR = new Comparator() {
    @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 myContainers = Collections.emptyList();
  protected List myProperties = Collections.emptyList();
  protected final Set myExpandedProperties = new HashSet();

  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() {
      @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() {
        @Override
        public void run() throws Exception {
          for (PropertiesContainer component : myContainers) {
            if (!property.isDefaultRecursively(component)) {
              property.setDefaultValue(component);
            }
          }
        }
      });

      repaint();
    }
  }

  protected abstract boolean doRestoreDefault(ThrowableRunnable 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 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 containers, @Nullable Property initialSelection) {
    update(containers, initialSelection, true);
  }

  private void update(@NotNull List 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(containers);
      fillProperties();
      myModel.fireTableDataChanged();

      restoreSelection(selection);
    }
    finally {
      mySkipUpdate = false;
    }
  }

  private void sortPropertiesAndCreateGroups(List rootProperties) {
    if (!mySorted && !myShowGroups) return;

    Collections.sort(rootProperties, new Comparator() {
      @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 getGroupComparator() {
    return GROUP_COMPARATOR;
  }

  @NotNull
  protected Comparator getPropertyComparator() {
    return PROPERTY_COMPARATOR;
  }

  protected List getProperties(PropertiesContainer component) {
    return component.getProperties();
  }

  private void restoreSelection(Property selection) {
    List propertyPath = new ArrayList(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();
    int size = myContainers.size();

    if (size > 0) {
      List rootProperties = new ArrayList();
      for (Property each : (Iterable)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 I = myProperties.iterator(); I.hasNext(); ) {
          if (!I.next().availableFor(myContainers)) {
            I.remove();
          }
        }

        for (int i = 1; i < size; i++) {
          List otherProperties = new ArrayList();
          fillProperties(myContainers.get(i), otherProperties);

          for (Iterator 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 addedChildren = getChildren(addedProperty);
            List 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 properties) {
    for (Property each : getProperties(component)) {
      if (addIfNeeded(component, each, properties)) {
        addExpandedChildren(component, each, properties);
      }
    }
  }

  private void addExpandedChildren(PropertiesContainer component, Property property, List 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 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 properties, String name) {
    for (Property property : properties) {
      if (name.equals(property.getName())) {
        return property;
      }
    }
    return null;
  }

  public static int findProperty(List 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 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 source, String name, List 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 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 getChildren(Property property) {
    return property.getChildren(getCurrentComponent());
  }

  private List getFilterChildren(Property property) {
    List properties = new ArrayList(getChildren(property));
    for (Iterator 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 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() {
        @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 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 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 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 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;
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy