[vcs-log] do not end copied from details text with \n IDEA-134711
[idea/community.git] / platform / vcs-log / impl / src / com / intellij / vcs / log / ui / frame / DetailsPanel.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.vcs.log.ui.frame;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.progress.util.ProgressWindow;
20 import com.intellij.openapi.project.Project;
21 import com.intellij.openapi.util.Comparing;
22 import com.intellij.openapi.util.text.StringUtil;
23 import com.intellij.openapi.vcs.changes.issueLinks.IssueLinkHtmlRenderer;
24 import com.intellij.openapi.vfs.VirtualFile;
25 import com.intellij.ui.BrowserHyperlinkListener;
26 import com.intellij.ui.JBColor;
27 import com.intellij.ui.components.JBLoadingPanel;
28 import com.intellij.ui.components.JBScrollPane;
29 import com.intellij.ui.components.panels.NonOpaquePanel;
30 import com.intellij.util.NotNullProducer;
31 import com.intellij.util.SystemProperties;
32 import com.intellij.util.containers.ContainerUtil;
33 import com.intellij.util.text.DateFormatUtil;
34 import com.intellij.util.ui.UIUtil;
35 import com.intellij.vcs.log.Hash;
36 import com.intellij.vcs.log.VcsFullCommitDetails;
37 import com.intellij.vcs.log.VcsRef;
38 import com.intellij.vcs.log.data.LoadingDetails;
39 import com.intellij.vcs.log.data.VcsLogDataHolder;
40 import com.intellij.vcs.log.data.VisiblePack;
41 import com.intellij.vcs.log.printer.idea.PrintParameters;
42 import com.intellij.vcs.log.ui.VcsLogColorManager;
43 import com.intellij.vcs.log.ui.render.RefPainter;
44 import com.intellij.vcs.log.ui.tables.GraphTableModel;
45 import net.miginfocom.swing.MigLayout;
46 import org.jetbrains.annotations.NotNull;
47 import org.jetbrains.annotations.Nullable;
48
49 import javax.swing.*;
50 import javax.swing.border.CompoundBorder;
51 import javax.swing.border.MatteBorder;
52 import javax.swing.event.HyperlinkEvent;
53 import javax.swing.event.HyperlinkListener;
54 import javax.swing.event.ListSelectionEvent;
55 import javax.swing.event.ListSelectionListener;
56 import javax.swing.text.BadLocationException;
57 import javax.swing.text.DefaultCaret;
58 import javax.swing.text.Document;
59 import javax.swing.text.Position;
60 import javax.swing.text.html.HTMLEditorKit;
61 import javax.swing.text.html.parser.ParserDelegator;
62 import java.awt.*;
63 import java.io.*;
64 import java.util.Collection;
65 import java.util.Collections;
66 import java.util.List;
67
68 /**
69  * @author Kirill Likhodedov
70  */
71 class DetailsPanel extends JPanel implements ListSelectionListener {
72
73   private static final Logger LOG = Logger.getInstance("Vcs.Log");
74
75   private static final String STANDARD_LAYER = "Standard";
76   private static final String MESSAGE_LAYER = "Message";
77
78   @NotNull private final VcsLogDataHolder myLogDataHolder;
79   @NotNull private final VcsLogGraphTable myGraphTable;
80
81   @NotNull private final RefsPanel myRefsPanel;
82   @NotNull private final DataPanel myCommitDetailsPanel;
83   @NotNull private final MessagePanel myMessagePanel;
84   @NotNull private final JScrollPane myScrollPane;
85   @NotNull private final JPanel myMainContentPanel;
86
87   @NotNull private final JBLoadingPanel myLoadingPanel;
88   @NotNull private final VcsLogColorManager myColorManager;
89
90   @NotNull private VisiblePack myDataPack;
91   @Nullable private VcsFullCommitDetails myCurrentCommitDetails;
92
93   DetailsPanel(@NotNull VcsLogDataHolder logDataHolder,
94                @NotNull VcsLogGraphTable graphTable,
95                @NotNull VcsLogColorManager colorManager,
96                @NotNull VisiblePack initialDataPack) {
97     myLogDataHolder = logDataHolder;
98     myGraphTable = graphTable;
99     myColorManager = colorManager;
100     myDataPack = initialDataPack;
101
102     myRefsPanel = new RefsPanel(myColorManager);
103     myCommitDetailsPanel = new DataPanel(logDataHolder.getProject(), logDataHolder.isMultiRoot());
104
105     myScrollPane = new JBScrollPane(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
106     myMainContentPanel = new JPanel(new MigLayout("flowy, ins 0, hidemode 3, gapy 0")) {
107       @Override
108       public Dimension getPreferredSize() {
109         Dimension size = super.getPreferredSize();
110         size.width = myScrollPane.getViewport().getWidth() - 5;
111         return size;
112       }
113     };
114     myMainContentPanel.setOpaque(false);
115     myScrollPane.setOpaque(false);
116     myScrollPane.getViewport().setOpaque(false);
117     myScrollPane.setViewportView(myMainContentPanel);
118     myMainContentPanel.add(myRefsPanel, "");
119     myMainContentPanel.add(myCommitDetailsPanel, "");
120
121     myLoadingPanel = new JBLoadingPanel(new BorderLayout(), logDataHolder, ProgressWindow.DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS) {
122       @Override
123       public Color getBackground() {
124         return getDetailsBackground();
125       }
126     };
127     myLoadingPanel.add(myScrollPane);
128
129     myMessagePanel = new MessagePanel();
130
131     setLayout(new CardLayout());
132     add(myLoadingPanel, STANDARD_LAYER);
133     add(myMessagePanel, MESSAGE_LAYER);
134
135     showMessage("No commits selected");
136   }
137
138   @Override
139   public Color getBackground() {
140     return getDetailsBackground();
141   }
142
143   private static Color getDetailsBackground() {
144     return UIUtil.getTableBackground();
145   }
146
147   void updateDataPack(@NotNull VisiblePack dataPack) {
148     myDataPack = dataPack;
149   }
150
151   @Override
152   public void valueChanged(@Nullable ListSelectionEvent notUsed) {
153     if (notUsed != null && notUsed.getValueIsAdjusting()) return;
154
155     VcsFullCommitDetails newCommitDetails = null;
156
157     int[] rows = myGraphTable.getSelectedRows();
158     if (rows.length < 1) {
159       showMessage("No commits selected");
160     }
161     else if (rows.length > 1) {
162       showMessage("Several commits selected");
163     }
164     else {
165       ((CardLayout)getLayout()).show(this, STANDARD_LAYER);
166       int row = rows[0];
167       GraphTableModel tableModel = (GraphTableModel)myGraphTable.getModel();
168       VcsFullCommitDetails commitData = myLogDataHolder.getCommitDetailsGetter().getCommitData(row, tableModel);
169       if (commitData == null) {
170         showMessage("No commits selected");
171         return;
172       }
173       if (commitData instanceof LoadingDetails) {
174         myLoadingPanel.startLoading();
175         myCommitDetailsPanel.setData(null);
176         myRefsPanel.setRefs(Collections.<VcsRef>emptyList());
177         updateDetailsBorder(null);
178       }
179       else {
180         myLoadingPanel.stopLoading();
181         myCommitDetailsPanel.setData(commitData);
182         myRefsPanel.setRefs(sortRefs(commitData.getId(), commitData.getRoot()));
183         updateDetailsBorder(commitData);
184         newCommitDetails = commitData;
185       }
186
187       List<String> branches = null;
188       if (!(commitData instanceof LoadingDetails)) {
189         branches = myLogDataHolder.getContainingBranchesGetter().requestContainingBranches(commitData.getRoot(), commitData.getId());
190       }
191       myCommitDetailsPanel.setBranches(branches);
192
193       if (!Comparing.equal(myCurrentCommitDetails, newCommitDetails)) {
194         myCurrentCommitDetails = newCommitDetails;
195         myScrollPane.getVerticalScrollBar().setValue(0);
196       }
197     }
198   }
199
200   private void updateDetailsBorder(@Nullable VcsFullCommitDetails data) {
201     if (data == null || !myColorManager.isMultipleRoots()) {
202       myMainContentPanel.setBorder(BorderFactory.createEmptyBorder());
203     }
204     else {
205       JBColor color = VcsLogGraphTable.getRootBackgroundColor(data.getRoot(), myColorManager);
206       myMainContentPanel.setBorder(new CompoundBorder(new MatteBorder(0, VcsLogGraphTable.ROOT_INDICATOR_COLORED_WIDTH, 0, 0, color),
207                                                       new MatteBorder(0, VcsLogGraphTable.ROOT_INDICATOR_WHITE_WIDTH, 0, 0,
208                                                                       new JBColor(new NotNullProducer<Color>() {
209                                                                         @NotNull
210                                                                         @Override
211                                                                         public Color produce() {
212                                                                           return getDetailsBackground();
213                                                                         }
214                                                                       }))));
215     }
216   }
217
218   private void showMessage(String text) {
219     myLoadingPanel.stopLoading();
220     ((CardLayout)getLayout()).show(this, MESSAGE_LAYER);
221     myMessagePanel.setText(text);
222   }
223
224   @NotNull
225   private List<VcsRef> sortRefs(@NotNull Hash hash, @NotNull VirtualFile root) {
226     Collection<VcsRef> refs = myDataPack.getRefsModel().refsToCommit(hash);
227     return ContainerUtil.sorted(refs, myLogDataHolder.getLogProvider(root).getReferenceManager().getLabelsOrderComparator());
228   }
229
230   private static class DataPanel extends JEditorPane {
231     public static final int BRANCHES_LIMIT = 6;
232     public static final int BRANCHES_TABLE_COLUMN_COUNT = 3;
233     @NotNull public static final String LEFT_ALIGN = "left";
234     @NotNull private static String SHOW_OR_HIDE_BRANCHES = "Show or Hide Branches";
235
236     @NotNull private final Project myProject;
237     private final boolean myMultiRoot;
238     private String myMainText;
239     @Nullable private List<String> myBranches;
240     private boolean myExpanded = false;
241
242     DataPanel(@NotNull Project project, boolean multiRoot) {
243       super(UIUtil.HTML_MIME, "");
244       myProject = project;
245       myMultiRoot = multiRoot;
246       setEditable(false);
247       setOpaque(false);
248       putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE);
249
250       DefaultCaret caret = (DefaultCaret)getCaret();
251       caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
252
253       addHyperlinkListener(new HyperlinkListener() {
254         public void hyperlinkUpdate(HyperlinkEvent e) {
255           if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED && SHOW_OR_HIDE_BRANCHES.equals(e.getDescription())) {
256             myExpanded = !myExpanded;
257             update();
258           }
259           else {
260             BrowserHyperlinkListener.INSTANCE.hyperlinkUpdate(e);
261           }
262         }
263       });
264     }
265
266     void setData(@Nullable VcsFullCommitDetails commit) {
267       if (commit == null) {
268         myMainText = null;
269       }
270       else {
271         String header = commit.getId().toShortString() + " " + getAuthorText(commit) +
272                         (myMultiRoot ? " [" + commit.getRoot().getName() + "]" : "");
273         String body = getMessageText(commit);
274         myMainText = header + "<br/>" + body;
275       }
276       update();
277     }
278
279     void setBranches(@Nullable List<String> branches) {
280       if (branches == null) {
281         myBranches = null;
282       }
283       else {
284         myBranches = branches;
285       }
286       myExpanded = false;
287       update();
288     }
289
290     private void update() {
291       if (myMainText == null) {
292         setText("");
293       }
294       else {
295         setText("<html><head>" +
296                 UIUtil.getCssFontDeclaration(UIUtil.getLabelFont()) +
297                 "</head><body>" +
298                 myMainText +
299                 "<br/>" +
300                 "<br/>" +
301                 getBranchesText() +
302                 "</body></html>");
303       }
304       revalidate();
305       repaint();
306     }
307
308     @NotNull
309     private String getBranchesText() {
310       if (myBranches == null) {
311         return "<i>In branches: loading...</i>";
312       }
313       if (myExpanded) {
314         int rowCount = (int) Math.ceil((double)myBranches.size() / BRANCHES_TABLE_COLUMN_COUNT);
315         HtmlTableBuilder builder = new HtmlTableBuilder();
316
317         for (int i = 0; i < rowCount; i++) {
318           builder.startRow();
319           if (i == 0) {
320             builder.append("<i>In " + myBranches.size() + " branches, </i><a href=\"" + SHOW_OR_HIDE_BRANCHES + "\"><i>hide</i></a>: ");
321           } else {
322             builder.append("");
323           }
324
325           for (int j = 0; j < BRANCHES_TABLE_COLUMN_COUNT; j++) {
326             int index = rowCount * j + i;
327             if (index >= myBranches.size()) {
328               builder.append("");
329             } else if (index != myBranches.size() - 1)  {
330               builder.append(myBranches.get(index) + "," + StringUtil.repeat("&nbsp;", 20), LEFT_ALIGN);
331             } else {
332               builder.append(myBranches.get(index), LEFT_ALIGN);
333             }
334           }
335
336           builder.endRow();
337         }
338
339         return builder.build();
340       }
341       else {
342         String branchText;
343         if (myBranches.size() <= BRANCHES_LIMIT) {
344           branchText = StringUtil.join(myBranches, ", ");
345         }
346         else {
347           branchText = StringUtil.join(ContainerUtil.getFirstItems(myBranches, BRANCHES_LIMIT), ", ") +
348                        ", ... <a href=\"" +
349                        SHOW_OR_HIDE_BRANCHES +
350                        "\"><i>Show All</i></a>";
351         }
352         return "<i>In " + myBranches.size() + StringUtil.pluralize(" branch", myBranches.size()) + ":</i> " + branchText;
353       }
354     }
355
356     @Override
357     public Dimension getPreferredSize() {
358       Dimension size = super.getPreferredSize();
359       size.height = Math.max(size.height, 4 * getFontMetrics(getFont()).getHeight());
360       return size;
361     }
362
363     private String getMessageText(VcsFullCommitDetails commit) {
364       String fullMessage = commit.getFullMessage();
365       int separator = fullMessage.indexOf("\n\n");
366       String subject = separator > 0 ? fullMessage.substring(0, separator) : fullMessage;
367       String description = fullMessage.substring(subject.length());
368       return "<b>" + escapeMultipleSpaces(IssueLinkHtmlRenderer.formatTextWithLinks(myProject, subject)) + "</b>" +
369              escapeMultipleSpaces(IssueLinkHtmlRenderer.formatTextWithLinks(myProject, description));
370     }
371
372     private String escapeMultipleSpaces(String text) {
373       StringBuilder result = new StringBuilder();
374       for (int i = 0; i < text.length(); i++) {
375         if (text.charAt(i) == ' ') {
376           if (i == text.length() - 1 || text.charAt(i + 1) != ' ') {
377             result.append(' ');
378           }
379           else {
380             result.append("&nbsp;");
381           }
382         }
383         else {
384           result.append(text.charAt(i));
385         }
386       }
387       return result.toString();
388     }
389
390     private static String getAuthorText(VcsFullCommitDetails commit) {
391       String authorText = commit.getAuthor().getName() + " at " + DateFormatUtil.formatDateTime(commit.getAuthorTime());
392       if (!commit.getAuthor().equals(commit.getCommitter())) {
393         String commitTime;
394         if (commit.getAuthorTime() != commit.getCommitTime()) {
395           commitTime = " at " + DateFormatUtil.formatDateTime(commit.getCommitTime());
396         }
397         else {
398           commitTime = "";
399         }
400         authorText += " (committed by " + commit.getCommitter().getName() + commitTime + ")";
401       }
402       else if (commit.getAuthorTime() != commit.getCommitTime()) {
403         authorText += " (committed at " + DateFormatUtil.formatDateTime(commit.getCommitTime()) + ")";
404       }
405       return authorText;
406     }
407
408     @Override
409     public String getSelectedText() {
410       Document doc = getDocument();
411       int start = getSelectionStart();
412       int end = getSelectionEnd();
413
414       try {
415         Position p0 = doc.createPosition(start);
416         Position p1 = doc.createPosition(end);
417         StringWriter sw = new StringWriter(p1.getOffset() - p0.getOffset());
418         getEditorKit().write(sw, doc, p0.getOffset(), p1.getOffset() - p0.getOffset());
419
420         MyHtml2Text parser = new MyHtml2Text();
421         parser.parse(new StringReader(sw.toString()));
422         return parser.getText();
423       }
424       catch (BadLocationException e) {
425         LOG.warn(e);
426       }
427       catch (IOException e) {
428         LOG.warn(e);
429       }
430       return super.getSelectedText();
431     }
432
433     private static class MyHtml2Text extends HTMLEditorKit.ParserCallback {
434       @NotNull private final StringBuilder myBuffer = new StringBuilder();
435
436       public void parse(Reader in) throws IOException {
437         myBuffer.setLength(0);
438         new ParserDelegator().parse(in, this, Boolean.TRUE);
439       }
440
441       public void handleText(char[] text, int pos) {
442         if (myBuffer.length() > 0) myBuffer.append(SystemProperties.getLineSeparator());
443
444         myBuffer.append(text);
445       }
446
447       public String getText() {
448         return myBuffer.toString();
449       }
450     }
451
452     @Override
453     public Color getBackground() {
454       return getDetailsBackground();
455     }
456   }
457
458   private static class RefsPanel extends JPanel {
459
460     @NotNull private final RefPainter myRefPainter;
461     @NotNull private List<VcsRef> myRefs;
462
463     RefsPanel(@NotNull VcsLogColorManager colorManager) {
464       super(new FlowLayout(FlowLayout.LEADING, 0, 2));
465       myRefPainter = new RefPainter(colorManager, false);
466       myRefs = Collections.emptyList();
467       setOpaque(false);
468     }
469
470     void setRefs(@NotNull List<VcsRef> refs) {
471       removeAll();
472       myRefs = refs;
473       for (VcsRef ref : refs) {
474         add(new SingleRefPanel(myRefPainter, ref));
475       }
476       setVisible(!myRefs.isEmpty());
477       revalidate();
478       repaint();
479     }
480
481     @Override
482     public Color getBackground() {
483       return getDetailsBackground();
484     }
485   }
486
487   private static class SingleRefPanel extends JPanel {
488     @NotNull private final RefPainter myRefPainter;
489     @NotNull private VcsRef myRef;
490
491     SingleRefPanel(@NotNull RefPainter refPainter, @NotNull VcsRef ref) {
492       myRefPainter = refPainter;
493       myRef = ref;
494       setOpaque(false);
495     }
496
497     @Override
498     protected void paintComponent(Graphics g) {
499       myRefPainter.draw((Graphics2D)g, Collections.singleton(myRef), 0, getWidth());
500     }
501
502     @Override
503     public Color getBackground() {
504       return getDetailsBackground();
505     }
506
507     @Override
508     public Dimension getPreferredSize() {
509       int width = myRefPainter.getComponentWidth(myRef.getName(), getFontMetrics(RefPainter.DEFAULT_FONT));
510       return new Dimension(width, PrintParameters.HEIGHT_CELL + UIUtil.DEFAULT_VGAP);
511     }
512   }
513
514   private static class MessagePanel extends NonOpaquePanel {
515
516     private final JLabel myLabel;
517
518     MessagePanel() {
519       super(new BorderLayout());
520       myLabel = new JLabel();
521       myLabel.setForeground(UIUtil.getInactiveTextColor());
522       myLabel.setHorizontalAlignment(SwingConstants.CENTER);
523       myLabel.setVerticalAlignment(SwingConstants.CENTER);
524       add(myLabel);
525     }
526
527     void setText(String text) {
528       myLabel.setText(text);
529     }
530
531     @Override
532     public Color getBackground() {
533       return getDetailsBackground();
534     }
535   }
536 }