/* * 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'} 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())) { component.handleMouseClickOnSelected(); refreshConditions(); } } 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() && !c.alwaysCanBeActive()) { removeCondition(c); } } } private void removeCondition(@NotNull ArrangementUiComponent component) { component.setSelected(false); component.setData(true); refreshConditions(); } @Override public String toString() { return "matching rule editor"; } }