[vcs-log] shorten reference name more gradually
[idea/community.git] / platform / vcs-log / impl / src / com / intellij / vcs / log / ui / render / LabelPainter.java
1 /*
2  * Copyright 2000-2016 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.render;
17 /*
18  * Copyright 2000-2015 JetBrains s.r.o.
19  *
20  * Licensed under the Apache License, Version 2.0 (the "License");
21  * you may not use this file except in compliance with the License.
22  * You may obtain a copy of the License at
23  *
24  * http://www.apache.org/licenses/LICENSE-2.0
25  *
26  * Unless required by applicable law or agreed to in writing, software
27  * distributed under the License is distributed on an "AS IS" BASIS,
28  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29  * See the License for the specific language governing permissions and
30  * limitations under the License.
31  */
32
33 import com.intellij.openapi.ui.GraphicsConfig;
34 import com.intellij.openapi.util.Pair;
35 import com.intellij.openapi.util.text.StringUtil;
36 import com.intellij.ui.ColorUtil;
37 import com.intellij.ui.JBColor;
38 import com.intellij.ui.SimpleColoredComponent;
39 import com.intellij.util.ObjectUtils;
40 import com.intellij.util.containers.ContainerUtil;
41 import com.intellij.util.containers.MultiMap;
42 import com.intellij.util.ui.GraphicsUtil;
43 import com.intellij.util.ui.JBUI;
44 import com.intellij.util.ui.UIUtil;
45 import com.intellij.vcs.log.RefGroup;
46 import com.intellij.vcs.log.VcsLogRefManager;
47 import com.intellij.vcs.log.VcsRef;
48 import com.intellij.vcs.log.VcsRefType;
49 import com.intellij.vcs.log.data.VcsLogData;
50 import org.jetbrains.annotations.NotNull;
51 import org.jetbrains.annotations.Nullable;
52
53 import javax.swing.*;
54 import java.awt.*;
55 import java.util.Collection;
56 import java.util.List;
57 import java.util.Map;
58
59 public class LabelPainter implements ReferencePainter {
60   public static final int TOP_TEXT_PADDING = JBUI.scale(1);
61   public static final int BOTTOM_TEXT_PADDING = JBUI.scale(2);
62   public static final int RIGHT_PADDING = JBUI.scale(2);
63   public static final int LEFT_PADDING = JBUI.scale(2);
64   public static final int MIDDLE_PADDING = JBUI.scale(2);
65   private static final int MAX_LENGTH = 22;
66   private static final String THREE_DOTS = "...";
67   private static final String TWO_DOTS = "..";
68   private static final String SEPARATOR = "/";
69   @SuppressWarnings("UseJBColor") private static final JBColor BACKGROUND = new JBColor(Color.BLACK, Color.WHITE);
70   private static final float BALANCE = 0.08f;
71
72   @NotNull private final VcsLogData myLogData;
73
74   @NotNull private List<Pair<String, LabelIcon>> myLabels = ContainerUtil.newArrayList();
75   private int myHeight = JBUI.scale(22);
76   private int myWidth = 0;
77   @NotNull private Color myBackground = UIUtil.getTableBackground();
78   @Nullable private Color myGreyBackground = null;
79   @NotNull private Color myForeground = UIUtil.getTableForeground();
80
81   public LabelPainter(@NotNull VcsLogData data) {
82     myLogData = data;
83   }
84
85   public void customizePainter(@NotNull JComponent component,
86                                @NotNull Collection<VcsRef> references,
87                                @NotNull Color background,
88                                @NotNull Color foreground,
89                                boolean isSelected,
90                                int availableWidth) {
91     myBackground = background;
92     myForeground = foreground;
93
94     FontMetrics metrics = component.getFontMetrics(getReferenceFont());
95     myHeight = metrics.getHeight() + TOP_TEXT_PADDING + BOTTOM_TEXT_PADDING;
96
97     VcsLogRefManager manager = ReferencePainter.getRefManager(myLogData, references);
98     List<RefGroup> refGroups = manager == null ? ContainerUtil.emptyList() : manager.groupForTable(references);
99
100     myGreyBackground = calculateGreyBackground(refGroups, background, isSelected);
101     Pair<List<Pair<String, LabelIcon>>, Integer> presentation =
102       calculatePresentation(refGroups, metrics, myHeight, myGreyBackground != null ? myGreyBackground : myBackground, availableWidth);
103
104     myLabels = presentation.first;
105     myWidth = presentation.second;
106   }
107
108   @NotNull
109   private static Pair<List<Pair<String, LabelIcon>>, Integer> calculatePresentation(@NotNull List<RefGroup> refGroups,
110                                                                                     @NotNull FontMetrics fontMetrics,
111                                                                                     int height,
112                                                                                     @NotNull Color background,
113                                                                                     int availableWidth) {
114     int width = LEFT_PADDING + RIGHT_PADDING;
115
116     List<Pair<String, LabelIcon>> labels = ContainerUtil.newArrayList();
117     if (refGroups.isEmpty()) return Pair.create(labels, width);
118
119     for (RefGroup group : refGroups) {
120       if (group.isExpanded()) {
121         for (VcsRef ref : group.getRefs()) {
122           LabelIcon labelIcon = new LabelIcon(height, background, ref.getType().getBackgroundColor());
123           width += labelIcon.getIconWidth() + MIDDLE_PADDING;
124
125           String text = shortenRefName(ref.getName(), fontMetrics, availableWidth - width);
126           width += fontMetrics.stringWidth(text);
127
128           labels.add(Pair.create(text, labelIcon));
129         }
130       }
131       else {
132         LabelIcon labelIcon = new LabelIcon(height, background, getGroupColors(group));
133         width += labelIcon.getIconWidth() + MIDDLE_PADDING;
134
135         String text = shortenRefName(group.getName(), fontMetrics, availableWidth - width);
136         width += fontMetrics.stringWidth(text);
137
138         labels.add(Pair.create(text, labelIcon));
139       }
140     }
141
142     return Pair.create(labels, width);
143   }
144
145   @Nullable
146   private static Color calculateGreyBackground(@NotNull List<RefGroup> refGroups, @NotNull Color background, boolean isSelected) {
147     if (isSelected) return null;
148
149     boolean paintGreyBackground;
150     for (RefGroup group : refGroups) {
151       if (group.isExpanded()) {
152         paintGreyBackground = ContainerUtil.find(group.getRefs(), ref -> !ref.getName().isEmpty()) != null;
153       }
154       else {
155         paintGreyBackground = !group.getName().isEmpty();
156       }
157
158       if (paintGreyBackground) return ColorUtil.mix(background, BACKGROUND, BALANCE);
159     }
160
161     return null;
162   }
163
164   @NotNull
165   private static String shortenRefName(@NotNull String refName, @NotNull FontMetrics fontMetrics, int availableWidth) {
166     if (fontMetrics.stringWidth(refName) > availableWidth && refName.length() > MAX_LENGTH) {
167       int separatorIndex = refName.indexOf(SEPARATOR);
168       if (separatorIndex > TWO_DOTS.length()) {
169         refName = TWO_DOTS + refName.substring(separatorIndex);
170       }
171
172       if (fontMetrics.stringWidth(refName) <= availableWidth) return refName;
173
174       if (availableWidth > 0) {
175         for (int i = refName.length(); i > MAX_LENGTH; i--) {
176           String result = StringUtil.shortenTextWithEllipsis(refName, i, 0, THREE_DOTS);
177           if (fontMetrics.stringWidth(result) <= availableWidth) {
178             return result;
179           }
180         }
181       }
182       return StringUtil.shortenTextWithEllipsis(refName, MAX_LENGTH, 0, THREE_DOTS);
183     }
184     return refName;
185   }
186
187   @NotNull
188   public static Color[] getGroupColors(@NotNull RefGroup group) {
189     MultiMap<VcsRefType, VcsRef> referencesByType = ContainerUtil.groupBy(group.getRefs(), VcsRef::getType);
190     Color[] colors;
191     if (referencesByType.size() == 1) {
192       Map.Entry<VcsRefType, Collection<VcsRef>> firstItem =
193         ObjectUtils.assertNotNull(ContainerUtil.getFirstItem(referencesByType.entrySet()));
194       boolean multiple = firstItem.getValue().size() > 1;
195       Color color = firstItem.getKey().getBackgroundColor();
196       colors = multiple ? new Color[]{color, color} : new Color[]{color};
197     }
198     else {
199       List<Color> colorsList = ContainerUtil.newArrayList();
200       for (VcsRefType type : referencesByType.keySet()) {
201         if (referencesByType.get(type).size() > 1) {
202           colorsList.add(type.getBackgroundColor());
203         }
204         colorsList.add(type.getBackgroundColor());
205       }
206       colors = colorsList.toArray(new Color[colorsList.size()]);
207     }
208     return colors;
209   }
210
211   public void paint(@NotNull Graphics2D g2, int x, int y, int height) {
212     if (myLabels.isEmpty()) return;
213
214     GraphicsConfig config = GraphicsUtil.setupAAPainting(g2);
215     g2.setFont(getReferenceFont());
216     g2.setStroke(new BasicStroke(1.5f));
217
218     FontMetrics fontMetrics = g2.getFontMetrics();
219     int baseLine = SimpleColoredComponent.getTextBaseLine(fontMetrics, height);
220
221     g2.setColor(myBackground);
222     g2.fillRect(x, y, myWidth, height);
223
224     if (myGreyBackground != null) {
225       g2.setColor(myGreyBackground);
226       g2.fillRect(x, y + baseLine - fontMetrics.getAscent() - TOP_TEXT_PADDING,
227                   myWidth - RIGHT_PADDING + LEFT_PADDING,
228                   fontMetrics.getHeight() + TOP_TEXT_PADDING + BOTTOM_TEXT_PADDING);
229     }
230
231     x += LEFT_PADDING;
232
233     for (Pair<String, LabelIcon> label : myLabels) {
234       LabelIcon icon = label.second;
235       String text = label.first;
236
237       icon.paintIcon(null, g2, x, y + (height - icon.getIconHeight()) / 2);
238       x += icon.getIconWidth();
239
240       g2.setColor(myForeground);
241       g2.drawString(text, x, y + baseLine);
242       x += fontMetrics.stringWidth(text) + MIDDLE_PADDING;
243     }
244
245     config.restore();
246   }
247
248   public Dimension getSize() {
249     if (myLabels.isEmpty()) return new Dimension();
250     return new Dimension(myWidth, myHeight);
251   }
252
253   @Override
254   public boolean isLeftAligned() {
255     return false;
256   }
257
258   @Override
259   public Font getReferenceFont() {
260     Font font = RectanglePainter.getFont();
261     return font.deriveFont(font.getSize() - 1f);
262   }
263 }
264