blob: 4f2ed17842e280028284ecbc1e6a44beba45f1aa [file] [log] [blame]
/*
* 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.match;
import com.intellij.application.options.codeStyle.arrangement.ArrangementConstants;
import com.intellij.application.options.codeStyle.arrangement.color.ArrangementColorsProvider;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.codeStyle.arrangement.ArrangementUtil;
import com.intellij.psi.codeStyle.arrangement.match.ArrangementEntryMatcher;
import com.intellij.psi.codeStyle.arrangement.match.ArrangementMatchRule;
import com.intellij.psi.codeStyle.arrangement.match.StdArrangementEntryMatcher;
import com.intellij.psi.codeStyle.arrangement.match.StdArrangementMatchRule;
import com.intellij.psi.codeStyle.arrangement.model.ArrangementMatchCondition;
import com.intellij.psi.codeStyle.arrangement.std.*;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.util.containers.ContainerUtilRt;
import com.intellij.util.ui.GridBag;
import com.intellij.util.ui.MultiRowFlowPanel;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.*;
import java.util.List;
/**
* Control for managing {@link ArrangementEntryMatcher matching rule conditions} for a single {@link ArrangementMatchRule}.
* <p/>
* Not thread-safe.
*
* @author Denis Zhdanov
* @since 8/14/12 9:54 AM
*/
public class ArrangementMatchingRuleEditor extends JPanel implements ArrangementUiComponent.Listener {
@NotNull private final Map<ArrangementSettingsToken, ArrangementUiComponent> myComponents = ContainerUtilRt.newHashMap();
@NotNull private final List<MultiRowFlowPanel> myRows = ContainerUtilRt.newArrayList();
@NotNull private final ArrangementMatchingRulesControl myControl;
@NotNull private final ArrangementStandardSettingsManager mySettingsManager;
@NotNull private final ArrangementColorsProvider myColorsProvider;
private int myRow = -1;
private int myLabelWidth;
@Nullable private JComponent myDefaultFocusRequestor;
@Nullable private JComponent myFocusRequestor;
private boolean mySkipStateChange;
public ArrangementMatchingRuleEditor(@NotNull ArrangementStandardSettingsManager settingsManager,
@NotNull ArrangementColorsProvider colorsProvider,
@NotNull ArrangementMatchingRulesControl control)
{
this(settingsManager, settingsManager.getSupportedMatchingTokens(), colorsProvider, control);
}
public ArrangementMatchingRuleEditor(@NotNull ArrangementStandardSettingsManager settingsManager,
@Nullable List<CompositeArrangementSettingsToken> tokens,
@NotNull ArrangementColorsProvider colorsProvider,
@NotNull ArrangementMatchingRulesControl control)
{
mySettingsManager = settingsManager;
myColorsProvider = colorsProvider;
myControl = control;
init(tokens);
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
onMouseClicked(e);
}
});
}
private void init(@Nullable List<CompositeArrangementSettingsToken> tokens) {
setLayout(new GridBagLayout());
setBorder(IdeBorderFactory.createEmptyBorder(5));
if (tokens != null) {
for (CompositeArrangementSettingsToken token : tokens) {
addToken(token);
}
}
applyBackground(UIUtil.getListBackground());
}
private void addToken(@NotNull CompositeArrangementSettingsToken rowToken) {
List<CompositeArrangementSettingsToken> tokens = ArrangementUtil.flatten(rowToken);
GridBag labelConstraints = new GridBag().anchor(GridBagConstraints.NORTHWEST).insets(ArrangementConstants.VERTICAL_PADDING, 0, 0, 0);
MultiRowFlowPanel panel = new MultiRowFlowPanel(
FlowLayout.LEFT, ArrangementConstants.HORIZONTAL_GAP, ArrangementConstants.VERTICAL_GAP
);
List<ArrangementSettingsToken> prevTokens = ContainerUtilRt.newArrayList();
StdArrangementTokenUiRole prevRole = null;
ArrangementUiComponent component;
JComponent uiComponent;
for (CompositeArrangementSettingsToken token : tokens) {
StdArrangementTokenUiRole role = token.getRole();
if (role != prevRole && !prevTokens.isEmpty()) {
component = ArrangementUtil.buildUiComponent(
role, prevTokens, myColorsProvider, mySettingsManager
);
component.setListener(this);
for (ArrangementSettingsToken prevToken : prevTokens) {
myComponents.put(prevToken, component);
}
panel.add(component.getUiComponent());
panel = addRowIfNecessary(panel);
prevRole = null;
prevTokens.clear();
}
component = ArrangementUtil.buildUiComponent(
role, Collections.singletonList(token.getToken()), myColorsProvider, mySettingsManager
);
component.setListener(this);
uiComponent = component.getUiComponent();
switch (role) {
case LABEL:
panel = addRowIfNecessary(panel);
add(uiComponent, labelConstraints);
myLabelWidth = Math.max(myLabelWidth, uiComponent.getPreferredSize().width);
prevRole = null;
break;
case TEXT_FIELD:
panel = addRowIfNecessary(panel);
ArrangementUiComponent textLabel = ArrangementUtil.buildUiComponent(
StdArrangementTokenUiRole.LABEL, Collections.singletonList(token.getToken()), myColorsProvider, mySettingsManager
);
JComponent textLabelComponent = textLabel.getUiComponent();
add(textLabelComponent, labelConstraints);
myLabelWidth = Math.max(myLabelWidth, textLabelComponent.getPreferredSize().width);
panel.add(uiComponent);
panel = addRowIfNecessary(panel);
prevRole = null;
myComponents.put(token.getToken(), component);
if (myDefaultFocusRequestor == null) {
myDefaultFocusRequestor = uiComponent;
}
break;
default:
if (role == StdArrangementTokenUiRole.COMBO_BOX) {
prevTokens.add(token.getToken());
prevRole = role;
break;
}
panel.add(uiComponent);
myComponents.put(token.getToken(), component);
}
}
if (prevRole != null && !prevTokens.isEmpty()) {
component = ArrangementUtil.buildUiComponent(prevRole, prevTokens, myColorsProvider, mySettingsManager);
panel.add(component.getUiComponent());
component.setListener(this);
for (ArrangementSettingsToken prevToken : prevTokens) {
myComponents.put(prevToken, component);
}
}
addRowIfNecessary(panel);
}
@NotNull
private MultiRowFlowPanel addRowIfNecessary(@NotNull MultiRowFlowPanel panel) {
if (panel.getComponentCount() <= 0) {
return panel;
}
add(panel, new GridBag().anchor(GridBagConstraints.WEST).weightx(1).fillCellHorizontally().coverLine());
myRows.add(panel);
return new MultiRowFlowPanel(
FlowLayout.LEFT, ArrangementConstants.HORIZONTAL_GAP, ArrangementConstants.VERTICAL_GAP
);
}
@Override
public void stateChanged() {
if (!mySkipStateChange) {
apply();
}
}
@Nullable
private Pair<ArrangementMatchCondition, ArrangementSettingsToken> buildCondition() {
List<ArrangementMatchCondition> conditions = ContainerUtilRt.newArrayList();
ArrangementSettingsToken orderType = null;
for (ArrangementUiComponent component : myComponents.values()) {
if (!component.isEnabled() || !component.isSelected()) {
continue;
}
ArrangementSettingsToken token = component.getToken();
if (token != null && StdArrangementTokenType.ORDER.is(token)) {
orderType = token;
}
else {
conditions.add(component.getMatchCondition());
}
}
if (!conditions.isEmpty()) {
if (orderType == null) {
orderType = StdArrangementTokens.Order.KEEP;
}
return Pair.create(ArrangementUtil.combine(conditions.toArray(new ArrangementMatchCondition[conditions.size()])), orderType);
}
else {
return null;
}
}
@Override
protected void paintComponent(Graphics g) {
if (myFocusRequestor != null) {
if (myFocusRequestor.isFocusOwner()) {
myFocusRequestor = null;
}
else {
myFocusRequestor.requestFocusInWindow();
}
}
super.paintComponent(g);
}
/**
* Asks current editor to refresh its state in accordance with the arrangement rule shown at the given row.
*
* @param row row index of the rule which match condition should be edited (if defined);
* <code>'-1'</code> as an indication that no settings should be active
*/
public void reset(int row) {
// Reset state.
myRow = row;
myFocusRequestor = myDefaultFocusRequestor;
mySkipStateChange = true;
try {
for (ArrangementUiComponent component : myComponents.values()) {
component.reset();
}
}
finally {
mySkipStateChange = false;
}
ArrangementMatchingRulesModel model = myControl.getModel();
if (row < 0 || row >= model.getSize()) {
myRow = -1;
return;
}
Object element = model.getElementAt(row);
ArrangementSettingsToken orderType = element instanceof ArrangementMatchRule ? ((ArrangementMatchRule)element).getOrderType() : null;
final ArrangementMatchCondition condition;
final Map<ArrangementSettingsToken, Object> conditionTokens;
if (element instanceof EmptyArrangementRuleComponent) {
// We need to disable conditions which are not applicable for empty rules (e.g. we don't want to enable 'volatile' condition
// for java rearranger if no 'field' condition is selected.
condition = null;
conditionTokens = ContainerUtilRt.newHashMap();
}
else if (!(element instanceof StdArrangementMatchRule)) {
return;
}
else {
condition = ((StdArrangementMatchRule)element).getMatcher().getCondition();
conditionTokens = ArrangementUtil.extractTokens(condition);
}
mySkipStateChange = true;
try {
for (ArrangementUiComponent component : myComponents.values()) {
ArrangementSettingsToken token = component.getToken();
if (token != null && (component.getAvailableTokens().contains(orderType) || isEnabled(condition, token))) {
component.setEnabled(true);
if (component.getAvailableTokens().contains(orderType)) {
component.chooseToken(orderType);
}
else {
component.setSelected(conditionTokens.containsKey(token));
}
Object value = conditionTokens.get(token);
if (value != null) {
component.setData(value);
}
}
}
refreshConditions();
}
finally {
mySkipStateChange = false;
}
}
/**
* Disable conditions not applicable at the current context (e.g. disable 'synchronized' if no 'method' is selected).
*/
private void refreshConditions() {
Pair<ArrangementMatchCondition, ArrangementSettingsToken> pair = buildCondition();
ArrangementMatchCondition condition = pair == null ? null : pair.first;
for (ArrangementUiComponent component : myComponents.values()) {
ArrangementSettingsToken token = component.getToken();
if (token == null) {
continue;
}
boolean enabled = isEnabled(condition, token);
component.setEnabled(enabled);
if (!enabled) {
component.setSelected(false);
}
}
}
private boolean isEnabled(@Nullable ArrangementMatchCondition condition, @NotNull ArrangementSettingsToken token) {
return ArrangementSectionRuleManager.isEnabled(token) || mySettingsManager.isEnabled(token, condition);
}
private void apply() {
final Pair<ArrangementMatchCondition, ArrangementSettingsToken> pair = buildCondition();
final Object modelValue;
if (pair == null) {
modelValue = new EmptyArrangementRuleComponent(myControl.getRowHeight(myRow));
}
else {
modelValue = new StdArrangementMatchRule(new StdArrangementEntryMatcher(pair.first), pair.second);
}
myControl.getModel().set(myRow, modelValue);
myControl.repaintRows(myRow, myRow, true);
}
public void applyAvailableWidth(int width) {
for (MultiRowFlowPanel row : myRows) {
row.setForcedWidth(width - myLabelWidth);
}
validate();
}
private void applyBackground(@NotNull Color color) {
setBackground(color);
for (JComponent component : myRows) {
component.setBackground(color);
}
}
private void onMouseClicked(@NotNull MouseEvent e) {
if (myRow < 0) {
return;
}
Point locationOnScreen = e.getLocationOnScreen();
for (ArrangementUiComponent component : myComponents.values()) {
Rectangle screenBounds = component.getScreenBounds();
if (screenBounds == null || !screenBounds.contains(locationOnScreen)) {
continue;
}
if (component.isEnabled()) {
if (component.isSelected()) {
// don't allow to remove start/end section indication
final Set<ArrangementSettingsToken> mutexes = ArrangementSectionRuleManager.getSectionMutexes();
if (!mutexes.contains(component.getToken())) {
removeCondition(component);
}
}
else {
addCondition(component);
}
}
apply();
return;
}
}
private void addCondition(@NotNull ArrangementUiComponent component) {
mySkipStateChange = true;
try {
component.setSelected(true);
Collection<Set<ArrangementSettingsToken>> mutexes = mySettingsManager.getMutexes();
// Update 'mutex conditions', i.e. conditions which can't be active at the same time (e.g. type 'field' and type 'method').
for (Set<ArrangementSettingsToken> mutex : mutexes) {
updateMutexConditions(component, mutex);
}
updateMutexConditions(component, ArrangementSectionRuleManager.getSectionMutexes());
refreshConditions();
}
finally {
mySkipStateChange = false;
}
}
private void updateMutexConditions(@NotNull ArrangementUiComponent component, @NotNull Set<ArrangementSettingsToken> mutex) {
if (!mutex.contains(component.getToken())) {
return;
}
for (ArrangementSettingsToken key : mutex) {
if (key.equals(component.getToken())) {
continue;
}
ArrangementUiComponent c = myComponents.get(key);
if (c != null && c.isEnabled()) {
removeCondition(c);
}
}
}
private void removeCondition(@NotNull ArrangementUiComponent component) {
component.setSelected(false);
refreshConditions();
}
@Override
public String toString() {
return "matching rule editor";
}
}