IDEA-19061 Integrate the Rearranger-plugin into core-IDE

Add ability to add new arrangement rules
This commit is contained in:
Denis.Zhdanov
2012-08-25 01:22:21 +04:00
parent fd6044b133
commit 8eb9deaf3a
14 changed files with 407 additions and 164 deletions
@@ -15,6 +15,7 @@
*/
package com.intellij.application.options.codeStyle.arrangement;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.codeStyle.arrangement.JavaRearranger;
import com.intellij.psi.codeStyle.arrangement.match.ArrangementEntryType;
import com.intellij.psi.codeStyle.arrangement.match.ArrangementModifier;
@@ -22,6 +23,7 @@ import com.intellij.psi.codeStyle.arrangement.model.ArrangementAtomMatchConditio
import com.intellij.psi.codeStyle.arrangement.model.ArrangementMatchCondition;
import com.intellij.psi.codeStyle.arrangement.model.ArrangementSettingType;
import com.intellij.ui.treeStructure.Tree;
import gnu.trove.TIntIntHashMap;
import gnu.trove.TIntObjectHashMap;
import org.jetbrains.annotations.NotNull;
import org.junit.Before;
@@ -55,7 +57,8 @@ public abstract class AbstractArrangementRuleEditingModelTest {
}
protected void configure(@NotNull ArrangementMatchCondition matchCondition) {
myBuilder.build(matchCondition, myTree, myRoot, myGrouper, myRowMappings);
Pair<ArrangementRuleEditingModelImpl,TIntIntHashMap> pair = myBuilder.build(matchCondition, myTree, myRoot, null, myGrouper);
myRowMappings.put(pair.first.getRow(), pair.first);
}
protected static ArrangementAtomMatchCondition atom(@NotNull Object condition) {
@@ -29,6 +29,7 @@ import static org.junit.Assert.assertEquals
* @author Denis Zhdanov
* @since 8/17/12 1:12 PM
*/
@SuppressWarnings(["GroovyAssignabilityCheck", "GroovyVariableNotAssigned"])
class ArrangementConfigUtilTest {
@Test
@@ -412,6 +413,53 @@ to = '4'()}
checkRowMappings([ 6 : 3 ], rowMappings)
}
@Test
void removeSingleNodeWithMerge() {
def node;
def initial = new TreeNodeBuilder().
'0' {
'1' {
'2'()}
node = '3'()
'1' {
'4'()}
}
def expected = new TreeNodeBuilder().
'0' {
'1' {
'2'()
'4'()}
}
def rowMappings = doRemove(node, node)
assertNodesEqual(expected, initial)
checkRowMappings([ 5 : 3 ], rowMappings)
}
@Test
void removeWithSingleNodeBelowMerge() {
def node;
def initial = new TreeNodeBuilder().
'0' {
'1' {
'2'()}
node = '3'()
'1'()
}
def expected = new TreeNodeBuilder().
'0' {
'1' {
'2'()}
'1'()
}
def rowMappings = doRemove(node, node)
assertNodesEqual(expected, initial)
checkRowMappings([ 4 : 3 ], rowMappings)
}
private static def doReplace(from, to, replacement) {
ArrangementConfigUtil.replace(from, to, replacement, new DefaultTreeModel(ArrangementConfigUtil.getRoot(from)), true)
}
@@ -15,7 +15,6 @@
*/
package com.intellij.application.options.codeStyle.arrangement;
import com.intellij.psi.codeStyle.arrangement.model.ArrangementMatchCondition;
import org.junit.Test;
import static com.intellij.psi.codeStyle.arrangement.ArrangementUtil.and;
@@ -31,21 +30,19 @@ public class ArrangementRuleEditingModelBuilderTest extends AbstractArrangementR
@Test
public void mapToTheSameLayer() {
ArrangementMatchCondition matchCondition = and(atom(PUBLIC), atom(STATIC));
myBuilder.build(matchCondition, myTree, myRoot, myGrouper, myRowMappings);
configure(and(atom(PUBLIC), atom(STATIC)));
checkRows(1);
ArrangementRuleEditingModel model = myRowMappings.get(1);
assertTrue(model.hasCondition(PUBLIC));
assertTrue(model.hasCondition(STATIC));
assertFalse(model.hasCondition(PRIVATE));
assertEquals(1, myRoot.getChildCount());
assertEquals(matchCondition, myRoot.getFirstChild().getBackingCondition());
assertEquals(and(atom(PUBLIC), atom(STATIC)), myRoot.getFirstChild().getBackingCondition());
}
@Test
public void splitIntoTwoLayers() {
ArrangementMatchCondition matchCondition = and(atom(FIELD), atom(PUBLIC), atom(STATIC));
myBuilder.build(matchCondition, myTree, myRoot, myGrouper, myRowMappings);
configure(and(atom(FIELD), atom(PUBLIC), atom(STATIC)));
checkRows(2);
@@ -60,8 +57,8 @@ public class ArrangementRuleEditingModelBuilderTest extends AbstractArrangementR
@Test
public void addToExistingLayer() {
myBuilder.build(and(atom(PUBLIC), atom(STATIC), atom(FIELD)), myTree, myRoot, myGrouper, myRowMappings);
myBuilder.build(and(atom(PRIVATE), atom(FIELD)), myTree, myRoot, myGrouper, myRowMappings);
configure(and(atom(PUBLIC), atom(STATIC), atom(FIELD)));
configure(and(atom(PRIVATE), atom(FIELD)));
checkRows(2, 3);
@@ -137,7 +137,7 @@ public class ArrangementUtil {
for (ArrangementMatchCondition node : nodes) {
node.invite(visitor);
}
return result;
return result.getOperands().size() == 1 ? result.getOperands().iterator().next() : result;
}
// TODO den remove
@@ -135,6 +135,7 @@ public class ArrangementConfigUtil {
return result;
}
@SuppressWarnings("ConstantConditions")
@NotNull
public static ArrangementTreeNode getLastBefore(@NotNull ArrangementTreeNode start, @NotNull ArrangementTreeNode stop)
throws IllegalArgumentException
@@ -223,7 +224,7 @@ public class ArrangementConfigUtil {
* @param replacement root of the node sub-hierarchy which should replace the one identified by the given 'start' and 'end' nodes
* @param treeModel model which should hold ui nodes
* @param rootVisible determines if the root should be count during rows calculations
* @return collection of row changes at the form {@code 'old row -> new row'}
* @return collection of row changes at the form {@code 'old row -> new row'} (all rows are zero-based)
*/
public static TIntIntHashMap replace(@NotNull ArrangementTreeNode from,
@NotNull ArrangementTreeNode to,
@@ -244,18 +245,6 @@ public class ArrangementConfigUtil {
@NotNull DefaultTreeModel treeModel,
boolean rootVisible)
{
markRows(from, rootVisible);
if (from == to) {
ArrangementTreeNode parent = from.getParent();
assert parent != null;
int index = parent.getIndex(from);
treeModel.removeNodeFromParent(from);
if (replacement != null) {
insert(parent, index, replacement, treeModel);
}
return collectRowChangesAndUnmark(parent, rootVisible);
}
// The algorithm looks as follows:
// 1. Cut sub-hierarchy which belongs to the given 'from' root and is located below the 'to -> from' path;
// 2. Remove 'to -> from' sub-hierarchy' by going bottom-up and stopping as soon as a current node has a child over than one
@@ -311,12 +300,49 @@ public class ArrangementConfigUtil {
//
// Note: we need to have a notion of 'equal nodes' for node re-usage. It's provided by comparing node user objects.
markRows(from, rootVisible);
final ArrangementTreeNode root = from.getParent();
assert root != null;
//region Cut bottom sub-hierarchy
@Nullable ArrangementTreeNode cutHierarchy = cutSubHierarchy(root, treeModel, to);
int childCountBefore = root.getChildCount();
for (ArrangementTreeNode current = to; current != root;) {
ArrangementTreeNode parent = current.getParent();
treeModel.removeNodeFromParent(current);
current = parent;
if (parent == null || parent.getChildCount() > 0) {
break;
}
}
int insertionIndex = root.getChildCount() < childCountBefore ? childCountBefore - 1 : childCountBefore;
if (replacement != null) {
insert(root, insertionIndex, replacement, treeModel);
}
if (cutHierarchy != null) {
insert(root, root.getChildCount(), cutHierarchy, treeModel);
}
return collectRowChangesAndUnmark(root, rootVisible);
}
/**
* Removes all nodes which lay below the path identified by the given 'top' and 'bottom' nodes from the tree structure and returns them.
*
* @param topMost top most node marker, i.e. all cut nodes descend from this node
* @param treeModel tree model which manages the nodes
* @param bottomMost bottom most node after which all other nodes should be cut
* @return removed nodes below the target path if any; <code>null</code> otherwise
*/
@Nullable
public static ArrangementTreeNode cutSubHierarchy(@NotNull ArrangementTreeNode topMost,
@NotNull DefaultTreeModel treeModel,
@NotNull ArrangementTreeNode bottomMost)
{
ArrangementTreeNode cutHierarchy = null;
for (ArrangementTreeNode current = to; current != root; current = current.getParent()) {
for (ArrangementTreeNode current = bottomMost; current != null && current != topMost; current = current.getParent()) {
ArrangementTreeNode parent = current.getParent();
assert parent != null;
int i = parent.getIndex(current);
@@ -338,43 +364,7 @@ public class ArrangementConfigUtil {
}
cutHierarchy = parentCopy;
}
//endregion
int childCountBefore = root.getChildCount();
//region Remove target sub-hierarchy
for (ArrangementTreeNode current = to; current != root;) {
ArrangementTreeNode parent = current.getParent();
treeModel.removeNodeFromParent(current);
current = parent;
if (parent == null || parent.getChildCount() > 0) {
break;
}
}
//endregion
//region Insert nodes.
int insertionIndex = root.getChildCount() < childCountBefore ? childCountBefore - 1 : childCountBefore;
if (replacement != null) {
insert(root, insertionIndex, replacement, treeModel);
}
if (cutHierarchy != null) {
List<ArrangementTreeNode> toInsert = new ArrayList<ArrangementTreeNode>();
if (hasEqualSetting(root, cutHierarchy)) {
for (int i = 0; i < cutHierarchy.getChildCount(); i++) {
toInsert.add(cutHierarchy.getChildAt(i));
}
}
else {
toInsert.add(cutHierarchy);
}
for (ArrangementTreeNode node : toInsert) {
insert(root, root.getChildCount(), node, treeModel);
}
}
//endregion
return collectRowChangesAndUnmark(root, rootVisible);
return cutHierarchy;
}
/**
@@ -383,7 +373,7 @@ public class ArrangementConfigUtil {
* @param node reference to the target hierarchy
* @param rootVisible determines if the root should be count during rows calculations
*/
private static void markRows(@NotNull ArrangementTreeNode node, boolean rootVisible) {
public static void markRows(@NotNull ArrangementTreeNode node, boolean rootVisible) {
ArrangementTreeNode root = getRoot(node);
int row = rootVisible ? 0 : -1;
Stack<ArrangementTreeNode> nodes = new Stack<ArrangementTreeNode>();
@@ -413,10 +403,10 @@ public class ArrangementConfigUtil {
* Collects all row changes and returns them. All row information is dropped from the nodes during the current method processing.
*
* @param node reference to the target nodes hierarchy
* @return collection of row changes at the form {@code 'old row -> new row'}
* @return collection of row changes at the form {@code 'old row -> new row'} (all rows are zero-based)
*/
@NotNull
private static TIntIntHashMap collectRowChangesAndUnmark(@NotNull ArrangementTreeNode node, boolean rootVisible) {
public static TIntIntHashMap collectRowChangesAndUnmark(@NotNull ArrangementTreeNode node, boolean rootVisible) {
@NotNull TIntIntHashMap changes = new TIntIntHashMap();
ArrangementTreeNode root = getRoot(node);
int row = rootVisible ? 0 : -1;
@@ -467,7 +457,7 @@ public class ArrangementConfigUtil {
buffer.setLength(buffer.length() - separator.length());
throw new RuntimeException("Invalid ArrangementTreeNode detected: " + buffer.toString());
}
/**
* Inserts given child to the given parent re-using existing nodes under the parent sub-hierarchy if possible.
* <p/>
@@ -493,14 +483,36 @@ public class ArrangementConfigUtil {
* | | |_5 | | | |_6 |
* </pre>
* <p/>
*
*
* @param parent parent node to insert into
* @param index insertion index to use for the given parent node
* @param child node to insert to the given parent node at the given insertion index
* @param treeModel model which should hold UI nodes
* @return <code>true</code> if given child node has been merged to the existing node; <code>false</code> otherwise
*/
public static boolean insert(@NotNull final ArrangementTreeNode parent,
public static void insert(@NotNull final ArrangementTreeNode parent,
final int index,
@NotNull final ArrangementTreeNode child,
@NotNull DefaultTreeModel treeModel)
{
ArrangementTreeNode root = getRoot(parent);
List<ArrangementTreeNode> toInsert = new ArrayList<ArrangementTreeNode>();
if (hasEqualSetting(root, child)) {
for (int i = 0; i < child.getChildCount(); i++) {
toInsert.add(child.getChildAt(i));
}
}
else {
toInsert.add(child);
}
int i = index;
for (ArrangementTreeNode node : toInsert) {
doInsert(parent, i++, node, treeModel);
}
}
private static boolean doInsert(@NotNull final ArrangementTreeNode parent,
final int index,
@NotNull final ArrangementTreeNode child,
@NotNull DefaultTreeModel treeModel)
@@ -562,7 +574,7 @@ public class ArrangementConfigUtil {
* @param to indicates end of the node sub-hierarchy (bottom-most node) to be replaced (inclusive)
* @param treeModel model which should hold ui nodes
* @param rootVisible determines if the root should be count during rows calculations
* @return collection of row changes at the form {@code 'old row -> new row'}
* @return collection of row changes at the form {@code 'old row -> new row'} (all rows are zero-based)
*/
@NotNull
public static TIntIntHashMap remove(@NotNull final ArrangementTreeNode from,
@@ -15,6 +15,7 @@
*/
package com.intellij.application.options.codeStyle.arrangement;
import com.intellij.openapi.actionSystem.DataKey;
import org.jetbrains.annotations.NonNls;
/**
@@ -24,11 +25,16 @@ import org.jetbrains.annotations.NonNls;
public class ArrangementConstants {
@NonNls public static final String ACTION_GROUP_RULE_EDITOR_CONTEXT_MENU = "Arrangement.RuleEditor.Context.Menu";
@NonNls public static final String RULE_EDITOR_PLACE = "Arrangement.RuleEditor.Place";
@NonNls public static final String RULE_TREE_PLACE = "Arrangement.RuleTree.Place";
@NonNls public static final String ACTION_GROUP_RULE_EDITOR_TOOL_WINDOW = "Arrangement.RuleEditor.ToolWIndow";
@NonNls public static final String RULE_EDITOR_PLACE = "Arrangement.RuleEditor.Place";
@NonNls public static final String RULE_EDITOR_TOOL_WINDOW_PLACE = "Arrangement.RuleEditor.ToolWindow.Place";
@NonNls public static final String RULE_TREE_PLACE = "Arrangement.RuleTree.Place";
public static final boolean LOG_RULE_MODIFICATION = Boolean.parseBoolean(System.getProperty("log.arrangement.rule.modification"));
public static final DataKey<Runnable> NEW_RULE_FUNCTION_KEY = DataKey.create("Arrangement.Rule.Function.New");
private ArrangementConstants() {
}
}
@@ -21,13 +21,13 @@ import com.intellij.psi.codeStyle.arrangement.model.ArrangementMatchCondition;
import com.intellij.psi.codeStyle.arrangement.model.HierarchicalArrangementConditionNode;
import com.intellij.psi.codeStyle.arrangement.settings.ArrangementSettingsGrouper;
import com.intellij.psi.codeStyle.arrangement.settings.ArrangementStandardSettingsRepresentationAware;
import gnu.trove.TIntObjectHashMap;
import gnu.trove.TIntIntHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
/**
* Holds glue logic between arrangement settings and their representation -
@@ -58,45 +58,41 @@ public class ArrangementRuleEditingModelBuilder {
* </ol>
* </pre>
*
* @param matchCondition target settings to process
* @param tree UI tree which shows arrangement matcher rules
* @param root UI tree settings root to use (may be not the same as the tree root)
* @param grouper strategy that knows how to
* {@link ArrangementStandardSettingsRepresentationAware#getDisplayValue(ArrangementModifier) group} setting
* nodes for UI representation
* @param rowMappings container to hold built {@link ArrangementRuleEditingModel editing models} (UI tree row numbers are used as keys)
* @param matchCondition target settings to process
* @param tree UI tree which shows arrangement matcher rules
* @param root UI tree settings root to use (may be not the same as the tree root)
* @param anchor node after which should be previous sibling for the root node of the inserted condition;
* <code>null</code> as an indication that new condition nodes should be inserted as the last 'root' child
* @param grouper strategy that knows how to
* {@link ArrangementStandardSettingsRepresentationAware#getDisplayValue(ArrangementModifier) group} setting
* nodes for UI representation
* @return collection of row changes at the form {@code 'old row -> new row'} (all rows are zero-based)
*/
@SuppressWarnings("MethodMayBeStatic")
public void build(@NotNull ArrangementMatchCondition matchCondition,
@NotNull JTree tree,
@NotNull ArrangementTreeNode root,
@NotNull ArrangementSettingsGrouper grouper,
@NotNull TIntObjectHashMap<ArrangementRuleEditingModelImpl> rowMappings)
public Pair<ArrangementRuleEditingModelImpl, TIntIntHashMap> build(
@NotNull ArrangementMatchCondition matchCondition,
@NotNull JTree tree,
@NotNull ArrangementTreeNode root,
@Nullable ArrangementTreeNode anchor,
@NotNull ArrangementSettingsGrouper grouper)
{
int initialInsertRow = 0;
// Count rows before the settings root.
for (TreeNode n = root.getParent(); n != null; n = n.getParent()) {
for (int i = n.getChildCount() - 1; i >= 0; i--) {
TreeNode child = n.getChildAt(i);
if (child != root) {
initialInsertRow += calculateRowsCount(child);
}
}
initialInsertRow++;
}
// Count root width.
initialInsertRow += calculateRowsCount(root);
if (!tree.isRootVisible()) {
initialInsertRow--;
}
HierarchicalArrangementConditionNode grouped = grouper.group(matchCondition);
DefaultTreeModel treeModel = (DefaultTreeModel)tree.getModel();
Pair<ArrangementTreeNode, Integer> pair = ArrangementConfigUtil.map(root, grouped, treeModel);
ArrangementTreeNode topMostNode = ArrangementConfigUtil.getLastBefore(pair.first, root);
int row = initialInsertRow + pair.second - 1;
Pair<ArrangementTreeNode, Integer> pair = ArrangementConfigUtil.map(null, grouped, null);
ArrangementTreeNode topMostNode = ArrangementConfigUtil.getRoot(pair.first);
ArrangementConfigUtil.markRows(root, tree.isRootVisible());
ArrangementTreeNode bottomHierarchy = null;
if (anchor != null) {
bottomHierarchy = ArrangementConfigUtil.cutSubHierarchy(root, treeModel, anchor);
}
ArrangementConfigUtil.insert(root, root.getChildCount(), topMostNode, treeModel);
if (bottomHierarchy != null) {
ArrangementConfigUtil.insert(root, root.getChildCount(), bottomHierarchy, treeModel);
}
TIntIntHashMap rowChanges = ArrangementConfigUtil.collectRowChangesAndUnmark(root, tree.isRootVisible());
topMostNode = ArrangementConfigUtil.getLastBefore(pair.first, root);
int row = ArrangementConfigUtil.getRow(pair.first, tree.isRootVisible());
ArrangementRuleEditingModelImpl model = new ArrangementRuleEditingModelImpl(
treeModel,
matchCondition,
@@ -106,14 +102,6 @@ public class ArrangementRuleEditingModelBuilder {
row,
tree.isRootVisible()
);
rowMappings.put(row, model);
}
private static int calculateRowsCount(@NotNull TreeNode node) {
int result = 1;
for (int i = node.getChildCount() - 1; i >= 0; i--) {
result += calculateRowsCount(node.getChildAt(i));
}
return result;
return Pair.create(model, rowChanges);
}
}
@@ -16,14 +16,18 @@
package com.intellij.application.options.codeStyle.arrangement;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.codeStyle.arrangement.match.ArrangementEntryType;
import com.intellij.psi.codeStyle.arrangement.match.ArrangementModifier;
import com.intellij.psi.codeStyle.arrangement.model.*;
import com.intellij.psi.codeStyle.arrangement.model.ArrangementAtomMatchCondition;
import com.intellij.psi.codeStyle.arrangement.model.ArrangementCompositeMatchCondition;
import com.intellij.psi.codeStyle.arrangement.model.ArrangementMatchCondition;
import com.intellij.psi.codeStyle.arrangement.model.ArrangementSettingType;
import com.intellij.psi.codeStyle.arrangement.settings.ArrangementSettingsGrouper;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.ui.treeStructure.Tree;
import com.intellij.util.Alarm;
import com.intellij.util.Consumer;
import gnu.trove.*;
import org.jetbrains.annotations.NotNull;
@@ -47,13 +51,17 @@ import java.util.List;
*/
public class ArrangementRuleTree {
@NotNull private static final JLabel EMPTY_RENDERER = new JLabel("");
private static final Logger LOG = Logger.getInstance("#" + ArrangementRuleTree.class.getName());
@NotNull private static final JLabel EMPTY_RENDERER = new JLabel("");
@NotNull private static final JLabel NEW_CONDITION_RENDERER = new JLabel("<html><b>&lt;empty condition&gt;</b>");
@NotNull private static final Logger LOG = Logger.getInstance("#" + ArrangementRuleTree.class.getName());
@NotNull private final List<ArrangementRuleSelectionListener> myListeners = new ArrayList<ArrangementRuleSelectionListener>();
@NotNull private final TreeSelectionModel mySelectionModel = new MySelectionModel();
@NotNull private final MyModelChangeListener myModelChangeListener = new MyModelChangeListener();
@NotNull private final MyModelNodesRefresher myModelNodesRefresher = new MyModelNodesRefresher();
@NotNull private final ArrangementRuleEditingModelBuilder myModelBuilder = new ArrangementRuleEditingModelBuilder();
@NotNull private final Alarm myAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD);
@NotNull private final RemoveInactiveNewModelRequest myRequest = new RemoveInactiveNewModelRequest();
@NotNull private final TIntObjectHashMap<ArrangementNodeComponent> myRenderers =
new TIntObjectHashMap<ArrangementNodeComponent>();
@@ -64,11 +72,13 @@ public class ArrangementRuleTree {
@NotNull private final DefaultTreeModel myTreeModel;
@NotNull private final Tree myTree;
@NotNull private final ArrangementNodeComponentFactory myFactory;
@NotNull private final ArrangementSettingsGrouper myGrouper;
private boolean myExplicitSelectionChange;
private boolean mySkipSelectionChange;
public ArrangementRuleTree(@NotNull ArrangementSettingsGrouper grouper, @NotNull ArrangementNodeDisplayManager displayManager) {
myGrouper = grouper;
myFactory = new ArrangementNodeComponentFactory(displayManager, new Consumer<ArrangementAtomMatchCondition>() {
@Override
public void consume(@NotNull ArrangementAtomMatchCondition setting) {
@@ -78,6 +88,7 @@ public class ArrangementRuleTree {
myRoot = new ArrangementTreeNode(null);
myTreeModel = new DefaultTreeModel(myRoot);
myTree = new Tree(myTreeModel) {
@Override
protected void setExpandedState(TreePath path, boolean state) {
// Don't allow node collapse
@@ -86,16 +97,34 @@ public class ArrangementRuleTree {
}
}
@NotNull
@Override
protected boolean isAlwaysPaintRowBackground() {
return false;
protected Condition<Integer> getWideSelectionBackgroundCondition() {
return new Condition<Integer>() {
@Override
public boolean value(Integer row) {
TreePath path = getPathForRow(row);
if (path == null) {
return false;
}
return isEmptyCondition(((ArrangementTreeNode)path.getLastPathComponent()).getBackingCondition());
}
};
}
@Override
protected void processMouseEvent(MouseEvent e) {
// JTree selects a node on mouse click at the same row (even outside the node bounds). We don't want to support
// such selection because selected nodes are highlighted at the rule tree, so, it produces a 'blink' effect.
mySkipSelectionChange = e.getClickCount() > 0 && getNodeComponentAt(e.getLocationOnScreen()) == null;
if (e.getClickCount() > 0 && getNodeComponentAt(e.getLocationOnScreen()) == null) {
TreePath path = myTree.getPathForLocation(e.getX(), e.getY());
if (path == null) {
mySkipSelectionChange = true;
}
if (path == null || !isEmptyCondition(((ArrangementTreeNode)path.getLastPathComponent()).getBackingCondition())) {
mySkipSelectionChange = true;
}
}
try {
super.processMouseEvent(e);
if (mySkipSelectionChange) {
@@ -106,7 +135,7 @@ public class ArrangementRuleTree {
mySkipSelectionChange = false;
}
}
};
};
myTree.setSelectionModel(mySelectionModel);
myTree.setRootVisible(false);
mySelectionModel.addTreeSelectionListener(new TreeSelectionListener() {
@@ -166,7 +195,7 @@ public class ArrangementRuleTree {
rules.add(new ArrangementCompositeMatchCondition(ArrangementCompositeMatchCondition.Operator.AND)
.addOperand(new ArrangementAtomMatchCondition(ArrangementSettingType.TYPE, ArrangementEntryType.METHOD))
.addOperand(new ArrangementAtomMatchCondition(ArrangementSettingType.MODIFIER, ArrangementModifier.PRIVATE)));
map(myRoot, rules, grouper);
map(rules);
expandAll(myTree, new TreePath(myRoot));
myTree.setShowsRootHandles(false);
@@ -264,7 +293,7 @@ public class ArrangementRuleTree {
}
/**
* Updates renderer's {@link ArrangementNodeComponent#setSelected(boolean) 'selected'} state on tree ndoe selection change.
* Updates renderer {@link ArrangementNodeComponent#setSelected(boolean) 'selected'} state on tree node selection change.
*
* @param path changed selection path
* @param selected <code>true</code> if given path is selected now; <code>false</code> if given path was selected anymore
@@ -305,21 +334,12 @@ public class ArrangementRuleTree {
tree.expandPath(parent);
}
private void map(@NotNull ArrangementTreeNode root,
@NotNull List<ArrangementMatchCondition> matchConditions,
@NotNull ArrangementSettingsGrouper grouper)
{
ArrangementRuleEditingModelBuilder builder = new ArrangementRuleEditingModelBuilder();
private void map(@NotNull List<ArrangementMatchCondition> matchConditions) {
for (ArrangementMatchCondition matchCondition : matchConditions) {
builder.build(matchCondition, myTree, root, grouper, myModels);
Pair<ArrangementRuleEditingModelImpl, TIntIntHashMap> pair = myModelBuilder.build(matchCondition, myTree, myRoot, null, myGrouper);
myModels.put(pair.first.getRow(), pair.first);
pair.first.addListener(myModelChangeListener);
}
myModels.forEachValue(new TObjectProcedure<ArrangementRuleEditingModelImpl>() {
@Override
public boolean execute(ArrangementRuleEditingModelImpl model) {
model.addListener(myModelChangeListener);
return true;
}
});
}
public void addEditingListener(@NotNull ArrangementRuleSelectionListener listener) {
@@ -330,7 +350,7 @@ public class ArrangementRuleTree {
* @return matcher model for the selected tree row(s) if any; null otherwise
*/
@Nullable
public ArrangementRuleEditingModel getActiveModel() {
public ArrangementRuleEditingModelImpl getActiveModel() {
TreePath[] paths = mySelectionModel.getSelectionPaths();
if (paths == null) {
return null;
@@ -340,14 +360,17 @@ public class ArrangementRuleTree {
// for more than one rule. No model is registered for them then and we want just to skip them.
for (int i = paths.length - 1; i >= 0; i--) {
int row = myTree.getRowForPath(paths[i]);
ArrangementRuleEditingModel model = myModels.get(row);
ArrangementRuleEditingModelImpl model = myModels.get(row);
if (model != null) {
return model;
}
}
return null;
}
/**
* @return UI component encapsulated by the current object. Subsequent calls to this method return the same reference all the time
*/
@NotNull
public Tree getTreeComponent() {
return myTree;
@@ -387,7 +410,13 @@ public class ArrangementRuleTree {
component.handleMouseClick(e);
return;
}
// Clear selection
TreePath path = myTree.getPathForLocation(e.getX(), e.getY());
if (path != null && isEmptyCondition(((ArrangementTreeNode)path.getLastPathComponent()).getBackingCondition())) {
return;
}
// Clear selection if it was a click at the empty space
doClearSelection();
}
@@ -530,6 +559,31 @@ public class ArrangementRuleTree {
}
});
}
/**
* Asks current model to create a new rule below the currently selected (or at the last position if no one is selected at the moment).
*
* @return model for the newly created row
*/
@NotNull
public ArrangementRuleEditingModel newModel() {
ArrangementRuleEditingModelImpl activeModel = getActiveModel();
final ArrangementTreeNode anchor = activeModel == null ? null : activeModel.getBottomMost();
doClearSelection();
Pair<ArrangementRuleEditingModelImpl,TIntIntHashMap> pair = myModelBuilder.build(
new ArrangementCompositeMatchCondition(ArrangementCompositeMatchCondition.Operator.AND), myTree, myRoot, anchor, myGrouper
);
processRowChanges(pair.second);
myModels.put(pair.first.getRow(), pair.first);
pair.first.addListener(myModelChangeListener);
mySelectionModel.setSelectionPath(myTree.getPathForRow(pair.first.getRow()));
return pair.first;
}
private static boolean isEmptyCondition(@Nullable ArrangementMatchCondition condition) {
return condition instanceof ArrangementCompositeMatchCondition
&& ((ArrangementCompositeMatchCondition)condition).getOperands().isEmpty();
}
private class MyCellRenderer implements TreeCellRenderer {
@Override
@@ -545,7 +599,10 @@ public class ArrangementRuleTree {
if (node == null) {
return EMPTY_RENDERER;
}
else if (isEmptyCondition(node)) {
return NEW_CONDITION_RENDERER;
}
if (row < 0) {
return myFactory.getComponent(node).getUiComponent();
}
@@ -568,6 +625,8 @@ public class ArrangementRuleTree {
return;
}
myAlarm.cancelAllRequests();
myAlarm.addRequest(myRequest, 300);
clearSelection();
ArrangementTreeNode component = (ArrangementTreeNode)path.getLastPathComponent();
@@ -626,4 +685,27 @@ public class ArrangementRuleTree {
return true;
}
}
private class RemoveInactiveNewModelRequest implements Runnable, TObjectProcedure<ArrangementRuleEditingModelImpl> {
@Override
public void run() {
myAlarm.cancelAllRequests();
ArrangementRuleEditingModelImpl model = getActiveModel();
if (model != null && isEmptyCondition(model.getMatchCondition())) {
return;
}
myModels.forEachValue(this);
}
@Override
public boolean execute(@NotNull ArrangementRuleEditingModelImpl model) {
if (isEmptyCondition(model.getMatchCondition())) {
model.destroy();
// TODO den restore selection (e.g. from bottom)
return false;
}
return true;
}
}
}
@@ -16,18 +16,27 @@
package com.intellij.application.options.codeStyle.arrangement;
import com.intellij.application.options.CodeStyleAbstractPanel;
import com.intellij.ide.DataManager;
import com.intellij.ide.ui.customization.CustomizationUtil;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionToolbar;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.application.ApplicationBundle;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
import com.intellij.openapi.util.Ref;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.arrangement.settings.*;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.ui.SideBorder;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.treeStructure.Tree;
import com.intellij.util.ui.GridBag;
import org.jdesktop.swingx.JXTaskPane;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
@@ -44,29 +53,38 @@ public abstract class ArrangementSettingsPanel extends CodeStyleAbstractPanel {
@NotNull private final JPanel myContent = new JPanel(new GridBagLayout());
public ArrangementSettingsPanel(@NotNull CodeStyleSettings settings, @NotNull ArrangementStandardSettingsAware filter) {
public ArrangementSettingsPanel(@NotNull CodeStyleSettings settings, @NotNull final ArrangementStandardSettingsAware filter) {
super(settings);
ArrangementStandardSettingsRepresentationAware representationManager = DefaultArrangementSettingsRepresentationManager.INSTANCE;
if (filter instanceof ArrangementStandardSettingsRepresentationAware) {
representationManager = (ArrangementStandardSettingsRepresentationAware)filter;
}
ArrangementNodeDisplayManager displayManager = new ArrangementNodeDisplayManager(filter, representationManager);
final ArrangementNodeDisplayManager displayManager = new ArrangementNodeDisplayManager(filter, representationManager);
ArrangementSettingsGrouper grouper = DefaultArrangementSettingsGrouper.INSTANCE;
if (filter instanceof ArrangementSettingsGrouper) {
grouper = (ArrangementSettingsGrouper)filter;
}
final ActionManager actionManager = ActionManager.getInstance();
final ActionGroup actionGroup = (ActionGroup)actionManager.getAction(ArrangementConstants.ACTION_GROUP_RULE_EDITOR_TOOL_WINDOW);
ActionToolbar actionToolbar = actionManager.createActionToolbar(ArrangementConstants.RULE_EDITOR_TOOL_WINDOW_PLACE, actionGroup, true);
JPanel toolbarControl = new JPanel(new GridBagLayout());
toolbarControl.add(actionToolbar.getComponent(), new GridBag().anchor(GridBagConstraints.WEST).weightx(1).fillCellHorizontally());
toolbarControl.setBorder(IdeBorderFactory.createBorder(SideBorder.LEFT | SideBorder.TOP | SideBorder.RIGHT));
myContent.add(toolbarControl, new GridBag().weightx(1).fillCellHorizontally().coverLine());
final ArrangementRuleTree ruleTree = new ArrangementRuleTree(grouper, displayManager);
Tree component = ruleTree.getTreeComponent();
myContent.add(new JBScrollPane(component), new GridBag().weightx(1).weighty(1).fillCell().coverLine());
Tree treeComponent = ruleTree.getTreeComponent();
actionToolbar.setTargetComponent(treeComponent);
myContent.add(new JBScrollPane(treeComponent), new GridBag().weightx(1).weighty(1).fillCell().coverLine());
CustomizationUtil.installPopupHandler(
component, ArrangementConstants.ACTION_GROUP_RULE_EDITOR_CONTEXT_MENU, ArrangementConstants.RULE_EDITOR_PLACE
treeComponent, ArrangementConstants.ACTION_GROUP_RULE_EDITOR_CONTEXT_MENU, ArrangementConstants.RULE_EDITOR_PLACE
);
final JXTaskPane editorPane = new JXTaskPane(ApplicationBundle.message("arrangement.title.editor"));
final ArrangementRuleEditor ruleEditor = new ArrangementRuleEditor(filter, displayManager);
ruleEditor.applyBackground(component.getBackground());
editorPane.getContentPane().setBackground(component.getBackground());
ruleEditor.applyBackground(treeComponent.getBackground());
editorPane.getContentPane().setBackground(treeComponent.getBackground());
editorPane.add(ruleEditor);
editorPane.setCollapsed(true);
final Ref<Boolean> resetEditor = new Ref<Boolean>(Boolean.TRUE);
@@ -97,6 +115,30 @@ public abstract class ArrangementSettingsPanel extends CodeStyleAbstractPanel {
editorPane.setCollapsed(true);
}
});
final Runnable newRuleFunction = new Runnable() {
@Override
public void run() {
ArrangementRuleEditingModel model = ruleTree.newModel();
ruleEditor.updateState(model);
resetEditor.set(Boolean.FALSE);
try {
editorPane.setCollapsed(false);
}
finally {
resetEditor.set(Boolean.TRUE);
}
}
};
treeComponent.putClientProperty(DataManager.CLIENT_PROPERTY_DATA_PROVIDER, new DataProvider() {
@Nullable
@Override
public Object getData(@NonNls String dataId) {
if (ArrangementConstants.NEW_RULE_FUNCTION_KEY.is(dataId)) {
return newRuleFunction;
}
return null;
}
});
}
@Override
@@ -0,0 +1,43 @@
/*
* Copyright 2000-2012 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.action;
import com.intellij.application.options.codeStyle.arrangement.ArrangementConstants;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationBundle;
/**
* @author Denis Zhdanov
* @since 8/24/12 1:54 PM
*/
public class AddArrangementRuleAction extends AnAction {
public AddArrangementRuleAction() {
getTemplatePresentation().setIcon(AllIcons.General.Add);
getTemplatePresentation().setText(ApplicationBundle.message("arrangement.action.condition.add.text"));
getTemplatePresentation().setDescription(ApplicationBundle.message("arrangement.action.rule.add.description"));
}
@Override
public void actionPerformed(AnActionEvent e) {
Runnable function = ArrangementConstants.NEW_RULE_FUNCTION_KEY.getData(e.getDataContext());
if (function != null) {
function.run();
}
}
}
@@ -19,6 +19,7 @@ import com.intellij.Patches;
import com.intellij.ide.util.treeView.*;
import com.intellij.openapi.ui.GraphicsConfig;
import com.intellij.openapi.ui.Queryable;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.ui.*;
@@ -97,7 +98,7 @@ public class Tree extends JTree implements ComponentWithEmptyText, ComponentWith
TreeUI actualUI = ui;
if (!isCustomUI()) {
if (!(ui instanceof WideSelectionTreeUI) && isWideSelection() && !UIUtil.isUnderGTKLookAndFeel()) {
actualUI = new WideSelectionTreeUI(isWideSelection(), isAlwaysPaintRowBackground());
actualUI = new WideSelectionTreeUI(isWideSelection(), getWideSelectionBackgroundCondition());
}
}
super.setUI(actualUI);
@@ -140,8 +141,14 @@ public class Tree extends JTree implements ComponentWithEmptyText, ComponentWith
return true;
}
protected boolean isAlwaysPaintRowBackground() {
return !SystemInfo.isMac;
/**
* @return a strategy which determines if a wide selection should be drawn for a target row (it's number is
* {@link Condition#value(Object) given} as an argument to the strategy)
*/
@SuppressWarnings("unchecked")
@NotNull
protected Condition<Integer> getWideSelectionBackgroundCondition() {
return SystemInfo.isMac ? Condition.FALSE : Condition.TRUE;
}
public boolean isFileColorsEnabled() {
@@ -555,3 +555,5 @@ arrangement.title.editor=Edit rule
arrangement.text.type=Type
arrangement.text.modifier=Modifier
arrangement.text.negate=Negate
arrangement.action.condition.add.text=Add condition
arrangement.action.condition.add.description=Add new arrangement rule condition
@@ -811,6 +811,10 @@
<group id="Arrangement.RuleEditor.Context.Menu">
<!--TODO den implement-->
</group>
<group id="Arrangement.RuleEditor.ToolWIndow">
<action id="Arrangement.Rule.Add" class="com.intellij.application.options.codeStyle.arrangement.action.AddArrangementRuleAction"/>
</group>
</actions>
</component>
@@ -15,6 +15,7 @@
*/
package com.intellij.util.ui.tree;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.util.containers.ComparatorUtil;
import com.intellij.util.ui.UIUtil;
@@ -36,26 +37,34 @@ import java.awt.event.MouseListener;
* @author Konstantin Bulenkov
*/
public class WideSelectionTreeUI extends BasicTreeUI {
@NonNls public static final String SOURCE_LIST_CLIENT_PROPERTY = "mac.ui.source.list";
@NonNls public static final String STRIPED_CLIENT_PROPERTY = "mac.ui.striped";
private static final Border LIST_BACKGROUND_PAINTER = UIManager.getBorder("List.sourceListBackgroundPainter");
private static final Border LIST_SELECTION_BACKGROUND_PAINTER = UIManager.getBorder("List.sourceListSelectionBackgroundPainter");
private static final Border LIST_FOCUSED_SELECTION_BACKGROUND_PAINTER = UIManager.getBorder("List.sourceListFocusedSelectionBackgroundPainter");
@NotNull private final Condition<Integer> myWideSelectionCondition;
private boolean myWideSelection;
private boolean myAlwaysPaintRowBackground;
private boolean myOldRepaintAllRowValue;
private boolean invertLineColor;
@SuppressWarnings("unchecked")
public WideSelectionTreeUI() {
this(true, true);
this(true, Condition.TRUE);
}
public WideSelectionTreeUI(final boolean wideSelection, boolean alwaysPaintRowBackground) {
/**
* Creates new <code>WideSelectionTreeUI</code> object.
*
* @param wideSelection flag that determines if wide selection should be used
* @param wideSelectionCondition strategy that determine if wide selection should be used for a target row (it's zero-based index
* is given to the condition as an argument)
*/
public WideSelectionTreeUI(final boolean wideSelection, @NotNull Condition<Integer> wideSelectionCondition) {
myWideSelection = wideSelection;
myAlwaysPaintRowBackground = alwaysPaintRowBackground;
myWideSelectionCondition = wideSelectionCondition;
}
private final MouseListener mySelectionListener = new MouseAdapter() {
@@ -232,7 +241,7 @@ public class WideSelectionTreeUI extends BasicTreeUI {
@Override
protected void paintVerticalLine(Graphics g, JComponent c, int x, int top, int bottom) {
if (tree.hasFocus() && UIUtil.isUnderAlloyIDEALookAndFeel()) {
int y0 = top, y1 = top;
int y0, y1 = top;
while (y1 < bottom) {
y0 = y1;
final int row = tree.getRowForPath(tree.getClosestPathForLocation(x, y0 + 1));
@@ -297,7 +306,7 @@ public class WideSelectionTreeUI extends BasicTreeUI {
LIST_SELECTION_BACKGROUND_PAINTER.paintBorder(tree, rowGraphics, xOffset, bounds.y, containerWidth, bounds.height);
}
}
else if (myAlwaysPaintRowBackground) {
else if (myWideSelectionCondition.value(row)) {
rowGraphics.setColor(background);
rowGraphics.fillRect(xOffset, bounds.y, containerWidth, bounds.height);
}
@@ -309,7 +318,7 @@ public class WideSelectionTreeUI extends BasicTreeUI {
bg = background;
}
if (myAlwaysPaintRowBackground || selected) {
if (myWideSelectionCondition.value(row) || selected) {
rowGraphics.setColor(bg);
rowGraphics.fillRect(xOffset, bounds.y, containerWidth, bounds.height - 1);
}
@@ -330,7 +339,7 @@ public class WideSelectionTreeUI extends BasicTreeUI {
@Override
public void paint(Graphics g, JComponent c) {
if (myAlwaysPaintRowBackground && myWideSelection && !UIUtil.isUnderAquaBasedLookAndFeel()) {
if (myWideSelection && !UIUtil.isUnderAquaBasedLookAndFeel()) {
paintSelectedRows(g, ((JTree)c));
}
if (myWideSelection) {
@@ -357,7 +366,7 @@ public class WideSelectionTreeUI extends BasicTreeUI {
final int lastVisibleRow = tr.getClosestRowForLocation(rect.x, rect.y + rect.height);
for (int row = firstVisibleRow; row <= lastVisibleRow; row++) {
if (tr.getSelectionModel().isRowSelected(row)) {
if (tr.getSelectionModel().isRowSelected(row) && myWideSelectionCondition.value(row)) {
final Rectangle bounds = tr.getRowBounds(row);
Color color = UIUtil.getTreeSelectionBackground(tr.hasFocus());
if (color != null) {