blob: 3a220b910c0544fd08a2b585ac3e3c037022c640 [file] [log] [blame]
package org.jetbrains.plugins.gradle.ui;
import com.intellij.openapi.util.text.StringUtil;
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.util.*;
import java.util.List;
/**
* Allows to build controls that show target user text with 'reach info' (e.g. inline icon button).
* <p/>
* Not thread-safe.
*
* @author Denis Zhdanov
* @since 1/16/12 5:06 PM
*/
public class RichTextControlBuilder {
private static final String RICH_TEXT_TOKEN_START = "{@";
private static final String RICH_TEXT_TOKEN_END = "}";
private final Map<String, RichTextProcessor> myProcessors = new HashMap<String, RichTextProcessor>();
private final JPanel myResult = new JPanel(new GridBagLayout());
private final Collection<JComponent> myComponents = new ArrayList<JComponent>();
private Color myForegroundColor;
private Color myBackgroundColor;
private Font myFont;
public RichTextControlBuilder() {
myForegroundColor = myResult.getForeground();
myBackgroundColor = myResult.getBackground();
myFont = myResult.getFont();
registerProcessor(new RichTextActionProcessor());
}
/**
* Defines target text to show by the UI component built by the current builder.
* <p/>
* The text can be 'reach', i.e. contain text like '{@value #RICH_TEXT_TOKEN_START}key data{@value #RICH_TEXT_TOKEN_END}'.
* Then 'data' will be delivered to the {@link RichTextProcessor} registered for the
* {@link RichTextProcessor#getKey() target key}. {@link RichTextProcessor#process(String) Returned component} (if any) will be
* shown within the resulting UI control.
* <p/>
* For example, there is a predefined {@link RichTextActionProcessor} that resolves references to the actions (shows its icon at
* the resulting component).
*
* @param text target text to show by the resulting control
* @throws IllegalArgumentException if given text is malformed
*/
public void setText(@NotNull String text) throws IllegalArgumentException {
myResult.removeAll();
myComponents.clear();
List<JComponent> rowComponents = new ArrayList<JComponent>();
RichTextProcessor metaDataProcessor = null;
StringBuilder metaTokenData = new StringBuilder();
boolean ignoreNext = false;
for (String s : StringUtil.tokenize(new StringTokenizer(text, " \n", true))) {
if (ignoreNext || s.isEmpty()) {
ignoreNext = false;
continue;
}
if (metaDataProcessor != null) {
// Inside meta-token.
final int i = s.indexOf(RICH_TEXT_TOKEN_END);
if (i >= 0) {
// Meta-token ends within the current string.
metaTokenData.append(s.substring(0, i));
final JComponent component = metaDataProcessor.process(metaTokenData.toString());
if (component != null) {
rowComponents.add(component);
}
metaTokenData.setLength(0);
metaDataProcessor = null;
if (i + RICH_TEXT_TOKEN_END.length() < s.length()) {
s = s.substring(i + RICH_TEXT_TOKEN_END.length());
}
else {
continue;
}
}
else {
// Meta-token data continues here.
metaTokenData.append(s);
continue;
}
}
final int start = s.indexOf(RICH_TEXT_TOKEN_START);
// Check if meta data starts here.
if (start >= 0) {
if (start + RICH_TEXT_TOKEN_START.length() >= s.length()) {
throw new IllegalArgumentException(String.format(
"Invalid rich text detected. Meta data key is assumed to directly follow '%s' (no white spaces between them). "
+ "Given text: '%s'", RICH_TEXT_TOKEN_START, text));
}
// Define meta-key end offset.
int end = s.indexOf(RICH_TEXT_TOKEN_END);
boolean metaDataComplete = true;
if (end < start) {
end = s.length();
metaDataComplete = false;
}
String metaKey = s.substring(start + RICH_TEXT_TOKEN_START.length(), end);
metaDataProcessor = myProcessors.get(metaKey);
if (metaDataProcessor == null) {
throw new IllegalArgumentException(String.format(
"No processor is registered for the meta-key '%s' (processors are available only for these keys - %s). Rich text: '%s'",
metaKey, myProcessors.keySet(), text
));
}
// There is a possible case like {@key}. We can complete meta-processing here than.
if (metaDataComplete) {
final JComponent component = metaDataProcessor.process("");
if (component != null) {
rowComponents.add(component);
}
metaDataProcessor = null;
if (end < s.length()) {
// Handle situation like '{@key}text', i.e. there is no white space between the meta-data and the text that follows it.
s = s.substring(end);
}
else {
continue;
}
}
else {
ignoreNext = true;
continue;
}
}
if (s.contains("\n")) {
addRow(rowComponents);
rowComponents.clear();
}
else {
final JLabel label = new JLabel(s);
label.setForeground(myForegroundColor);
label.setBackground(myBackgroundColor);
label.setFont(myFont);
rowComponents.add(label);
}
}
if (!rowComponents.isEmpty()) {
addRow(rowComponents);
}
}
public void setForegroundColor(@NotNull Color foregroundColor) {
myForegroundColor = foregroundColor;
}
public void setBackgroundColor(@NotNull Color backgroundColor) {
myBackgroundColor = backgroundColor;
}
public void setFont(@NotNull Font font) {
myFont = font;
}
/**
* Registers given processor within the current builder, i.e. it will be
* {@link RichTextProcessor#process(String) asked to process} meta data for the
* {@link RichTextProcessor#getKey target key} (if any).
*
* @param processor processor to register
*/
public void registerProcessor(@NotNull RichTextProcessor processor) {
myProcessors.put(processor.getKey(), processor);
}
/**
* @return component built within the provided information
*/
@NotNull
public JComponent build() {
for (JComponent component : myComponents) {
component.setForeground(myForegroundColor);
component.setBackground(myBackgroundColor);
component.setFont(myFont);
}
myResult.setForeground(myForegroundColor);
myResult.setBackground(myBackgroundColor);
myResult.setFont(myFont);
GridBagConstraints constraints = new GridBagConstraints();
constraints.weighty = 1;
constraints.fill = GridBagConstraints.VERTICAL;
myResult.add(Box.createVerticalStrut(1), constraints);
return myResult;
}
private void addRow(@NotNull Collection<JComponent> rowComponents) {
JPanel row = new MultiRowFlowPanel(FlowLayout.CENTER, getGapToUse(0), getGapToUse(3));
row.setBackground(myBackgroundColor);
myComponents.add(row);
if (rowComponents.isEmpty()) {
row.add(new JLabel(" "));
}
else {
for (JComponent component : rowComponents) {
row.add(component);
}
}
GridBagConstraints constraints = new GridBagConstraints();
constraints.gridwidth = GridBagConstraints.REMAINDER;
constraints.weightx = 1;
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.insets.top = 3;
myResult.add(row, constraints);
}
private static int getGapToUse(int gap) {
// There is a problem with flow layout controls paint under Alloy LAF - it looks like it doesn't take given gaps into consideration.
// Alloy LAF sources are closed, so we use this dirty hack here.
return UIUtil.isUnderAlloyLookAndFeel() ? gap - 4 : gap;
}
/**
* Encapsulates functionality for showing particular {@link RichTextControlBuilder#setText(String) rich text}.
*/
public interface RichTextProcessor {
/**
* @return flavor of the rich text that can be managed by the current processor
*/
@NotNull
String getKey();
/**
* Callback that receives target rich text data and returns UI control that represents it.
* <p/>
* For example, it can receive action id and return action's icon button.
*
* @param text target rich text
* @return UI control that represents given rich text in a way specific to the current processor (if any)
*/
@Nullable
JComponent process(@NotNull String text);
}
}