9b9d492a471585640cec9cc08b3ada8a28799b26
[idea/community.git] / java / idea-ui / src / com / intellij / openapi / roots / ui / configuration / ErrorPaneConfigurable.java
1 // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.openapi.roots.ui.configuration;
3
4 import com.intellij.ide.JavaUiBundle;
5 import com.intellij.openapi.Disposable;
6 import com.intellij.openapi.options.Configurable;
7 import com.intellij.openapi.options.ConfigurationException;
8 import com.intellij.openapi.project.Project;
9 import com.intellij.openapi.roots.ui.configuration.projectRoot.StructureConfigurableContext;
10 import com.intellij.openapi.roots.ui.configuration.projectRoot.daemon.ProjectConfigurationProblem;
11 import com.intellij.openapi.roots.ui.configuration.projectRoot.daemon.ProjectStructureElement;
12 import com.intellij.openapi.roots.ui.configuration.projectRoot.daemon.ProjectStructureProblemDescription;
13 import com.intellij.openapi.util.Disposer;
14 import com.intellij.openapi.util.text.StringUtil;
15 import com.intellij.ui.*;
16 import com.intellij.ui.awt.RelativePoint;
17 import com.intellij.util.Alarm;
18 import com.intellij.util.ui.StartupUiUtil;
19 import com.intellij.util.ui.UIUtil;
20 import com.intellij.util.ui.update.MergingUpdateQueue;
21 import com.intellij.util.ui.update.Update;
22 import com.intellij.xml.util.XmlStringUtil;
23 import org.jetbrains.annotations.Nls;
24 import org.jetbrains.annotations.NotNull;
25 import org.jetbrains.annotations.Nullable;
26
27 import javax.swing.*;
28 import javax.swing.event.HyperlinkEvent;
29 import javax.swing.text.Element;
30 import java.awt.*;
31 import java.awt.event.MouseEvent;
32 import java.net.URL;
33 import java.util.ArrayList;
34 import java.util.List;
35
36 /**
37  * @author Konstantin Bulenkov
38  */
39 public class ErrorPaneConfigurable extends JPanel implements Configurable, Disposable, ConfigurationErrors {
40   private final Alarm myAlarm;
41   private final List<ConfigurationError> myErrors = new ArrayList<>();
42   private int myComputedErrorsStamp;
43   private int myShownErrorsStamp;
44   private final Object myLock = new Object();
45   private final MergingUpdateQueue myContentUpdateQueue;
46   private final JTextPane myContent = new JTextPane();
47   private final Runnable myOnErrorsChanged;
48
49   public ErrorPaneConfigurable(final Project project, StructureConfigurableContext context, Runnable onErrorsChanged) {
50     super(new BorderLayout());
51     myOnErrorsChanged = onErrorsChanged;
52     myContent.setEditorKit(UIUtil.getHTMLEditorKit());
53     myContent.setEditable(false);
54     myContent.setBackground(UIUtil.getListBackground());
55     final JScrollPane pane = ScrollPaneFactory.createScrollPane(myContent, true);
56     pane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
57     add(pane);
58     myContentUpdateQueue = new MergingUpdateQueue("ErrorPaneConfigurable Content Updates", 300, false, pane, this, pane);
59     myAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this);
60     project.getMessageBus().connect(this).subscribe(ConfigurationErrors.TOPIC, this);
61     myContent.addHyperlinkListener(new HyperlinkAdapter() {
62       @Override
63       public void hyperlinkActivated(HyperlinkEvent e) {
64         final URL url = e.getURL();
65         final AWTEvent awtEvent = EventQueue.getCurrentEvent();
66         if (!(awtEvent instanceof MouseEvent)) {
67           return;
68         }
69         final MouseEvent me = (MouseEvent)awtEvent;
70
71         if (url != null) {
72           ConfigurationError error = null;
73           Element element = e.getSourceElement();
74           while (element != null) {
75             if ("li".equals(element.getName())) {
76               final Element ol = element.getParentElement();
77               for (int i = 0; i < ol.getElementCount(); i++) {
78                 if (ol.getElement(i) == element) {
79                   error = getError(i, myShownErrorsStamp);
80                 }
81               }
82               break;
83             }
84             element = element.getParentElement();
85           }
86           if (error == null) return;
87           final String host = url.getHost();
88           String path = url.getPath();
89           if (path != null && path.startsWith("/")) {
90             path = StringUtil.unescapeXmlEntities(path.substring(1));
91           }
92           if (path != null) {
93             if ("fix".equals(host)) {
94               final MouseEvent mouseEvent = new MouseEvent(me.getComponent(), me.getID(), me.getWhen(), me.getModifiers(),
95                                                            me.getX() - 15, me.getY() + 10, me.getClickCount(), me.isPopupTrigger());
96               error.fix(myContent, new RelativePoint(mouseEvent));
97             } else {
98               error.navigate();
99             }
100           }
101         }
102       }
103     });
104
105     refresh();
106   }
107
108   private ConfigurationError getError(int i, int expectedStamp) {
109     synchronized (myLock) {
110       return expectedStamp == myComputedErrorsStamp ? myErrors.get(i) : null;
111     }
112   }
113
114   public void refresh() {
115     myAlarm.cancelAllRequests();
116     myAlarm.addRequest(() -> {
117       final String header = "<html>" +
118                             "<header><style type='text/css'>" +
119                             "body {" +
120                             "  color: #" + ColorUtil.toHex(new JBColor(Gray.x33, UIUtil.getLabelForeground())) + ";" +
121                             "  font-family: '" + StartupUiUtil.getLabelFont().getName() + ",serif';" +
122                             "  font-size: " + StartupUiUtil.getLabelFont().getSize() + ";" +
123                             "}" +
124                             "li {" +
125                             "  margin-bottom: 5;" +
126                             "}" +
127                             "ol {" +
128                             "}" +
129                             "a {" +
130                             " text-decoration: none;" +
131                             "}" +
132                             "</style>" +
133                             "</header>" +
134                             "<body>";
135       final StringBuilder html = new StringBuilder(header);
136       int i = 0;
137       html.append("<ol>");
138       ConfigurationError[] errors;
139       int currentStamp;
140       synchronized (myLock) {
141         errors = myErrors.toArray(new ConfigurationError[0]);
142         currentStamp = myComputedErrorsStamp;
143       }
144
145       for (ConfigurationError error : errors) {
146         i++;
147         if (i > 100) break;
148         html.append("<li>");
149         String description;
150         if (error instanceof ProjectConfigurationProblem) {
151           //todo[nik] pass ProjectStructureProblemDescription directly and get rid of ConfigurationError at all
152           ProjectStructureProblemDescription problemDescription = ((ProjectConfigurationProblem)error).getProblemDescription();
153           description = problemDescription.getDescription();
154           if (description == null) {
155             ProjectStructureElement place = problemDescription.getPlace().getContainingElement();
156             description = XmlStringUtil.convertToHtmlContent(problemDescription.getMessage(false));
157             if (problemDescription.canShowPlace()) {
158               description = place.getTypeName() + " <a href='http://navigate/" + i + "'>"
159                             + XmlStringUtil.convertToHtmlContent(place.getPresentableName()) + "</a>: "
160                             + StringUtil.decapitalize(description);
161             }
162           }
163           else {
164             description = XmlStringUtil.convertToHtmlContent(description);
165           }
166         }
167         else {
168           description = XmlStringUtil.convertToHtmlContent(error.getDescription());
169         }
170         if (error.canBeFixed()) {
171           description += " <a href='http://fix/" + i + "'>[Fix]</a>";
172         }
173         html.append(description).append("</li>");
174       }
175       html.append("</ol></body></html>");
176       myContentUpdateQueue.queue(new ShowErrorsUpdate(currentStamp, html.toString()));
177       if (myOnErrorsChanged != null) {
178         myOnErrorsChanged.run();
179       }
180     }, 100);
181   }
182
183   @Nls
184   @Override
185   public String getDisplayName() {
186     return JavaUiBundle.message("configurable.ErrorPaneConfigurable.display.name");
187   }
188
189   @Nullable
190   @Override
191   public JComponent createComponent() {
192     return this;
193   }
194
195   @Override
196   public boolean isModified() {
197     return false;
198   }
199
200   @Override
201   public void apply() throws ConfigurationException {
202
203   }
204
205   @Override
206   public void disposeUIResources() {
207     Disposer.dispose(this);
208   }
209
210   @Override
211   public void dispose() {
212   }
213
214   @Override
215   public void addError(@NotNull ConfigurationError error) {
216     synchronized (myLock) {
217       myErrors.add(error);
218       myComputedErrorsStamp++;
219     }
220     refresh();
221   }
222
223   @Override
224   public void removeError(@NotNull ConfigurationError error) {
225     synchronized (myLock) {
226       myErrors.remove(error);
227       myComputedErrorsStamp++;
228     }
229     refresh();
230   }
231
232   public int getErrorsCount() {
233     synchronized (myLock) {
234       return myErrors.size();
235     }
236   }
237
238   private class ShowErrorsUpdate extends Update {
239     private final int myCurrentStamp;
240     private final String myText;
241
242     ShowErrorsUpdate(int currentStamp, String text) {
243       super(currentStamp);
244       myCurrentStamp = currentStamp;
245       myText = text;
246     }
247
248     @Override
249     public void run() {
250       if (!Disposer.isDisposed(ErrorPaneConfigurable.this)) {
251         myContent.setText(myText);
252         myShownErrorsStamp = myCurrentStamp;
253       }
254     }
255
256     @Override
257     public boolean canEat(Update update) {
258       return update instanceof ShowErrorsUpdate && myCurrentStamp > ((ShowErrorsUpdate)update).myCurrentStamp;
259     }
260   }
261 }