001: /*
002: * Copyright 2001 Nicholas Allen (nallen@freenet.co.uk) This file is part of
003: * JavaCVS. JavaCVS is free software; you can redistribute it and/or modify it
004: * under the terms of the GNU General Public License as published by the Free
005: * Software Foundation; either version 2 of the License, or (at your option) any
006: * later version. JavaCVS is distributed in the hope that it will be useful, but
007: * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
008: * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
009: * details. You should have received a copy of the GNU General Public License
010: * along with JavaCVS; if not, write to the Free Software Foundation, Inc., 59
011: * Temple Place, Suite 330, Boston, MA 02111-1307 USA
012: */
013: package allensoft.javacvs.client.ui.swing;
014:
015: import java.awt.BorderLayout;
016: import java.awt.Color;
017: import java.awt.Dimension;
018: import java.awt.FontMetrics;
019: import java.awt.Graphics;
020: import java.awt.Graphics2D;
021: import java.awt.Rectangle;
022: import java.awt.RenderingHints;
023: import java.awt.event.MouseEvent;
024: import java.text.DateFormat;
025: import java.util.ArrayList;
026: import java.util.Iterator;
027: import java.util.List;
028: import java.util.StringTokenizer;
029:
030: import javax.swing.JPanel;
031: import javax.swing.JScrollPane;
032: import javax.swing.JSlider;
033: import javax.swing.event.ChangeEvent;
034: import javax.swing.event.ChangeListener;
035: import org.netbeans.lib.cvsclient.command.log.LogInformation;
036:
037: /**
038: * DOCUMENT ME!
039: *
040: * @author $author$
041: */
042: public class LogDetailsGraphPanel extends JPanel {
043: private LogDetailsGraph m_Graph;
044: private LogInformation logInformation;
045:
046: /**
047: * Creates a new LogDetailsGraphPanel object.
048: */
049: public LogDetailsGraphPanel() {
050: super (new BorderLayout());
051: m_Graph = new LogDetailsGraph();
052: add(new JScrollPane(m_Graph), BorderLayout.CENTER);
053: final JSlider slider = new JSlider(JSlider.VERTICAL, 0, 100, 50);
054: add(slider, BorderLayout.WEST);
055: slider.addChangeListener(new ChangeListener() {
056: public void stateChanged(ChangeEvent e) {
057: int n = slider.getValue();
058: if (n > 50)
059: m_Graph.setScale(1.0 + ((n - 50) / 10.0));
060: else
061: m_Graph.setScale(n / 50.0);
062: }
063: });
064: }
065:
066: public void setLogInformation(LogInformation logInformation,
067: List sortedRevisions) {
068: this .logInformation = logInformation;
069: Branch rootBranch = new Branch("1");
070: for (Iterator i = sortedRevisions.iterator(); i.hasNext();) {
071: LogInformation.Revision rev = (LogInformation.Revision) i
072: .next();
073: String revNumber = rev.getNumber();
074: Revision revision = new Revision(rev,
075: rev.getBranches() != null, logInformation
076: .getSymNamesForRevision(revNumber));
077: if (rev.getBranches() != null) {
078: StringTokenizer t = new StringTokenizer(rev
079: .getBranches(), " ");
080: while (t.hasMoreTokens()) {
081: String newBranchName = t.nextToken();
082: Branch newBranch = new Branch(newBranchName);
083: revision.addBranch(newBranch);
084: }
085: }
086: Branch addTo = findBranch(rootBranch, revision);
087: if (addTo != null) {
088: addTo.m_Revisions.add(revision);
089: }
090: }
091: m_Graph.setRootBranch(rootBranch);
092:
093: }
094:
095: private Branch findBranch(Branch branch, Revision revision) {
096: for (int i = branch.m_Revisions.size() - 1; i >= 0; i--) {
097: Revision r = (Revision) branch.m_Revisions.get(i);
098: for (int j = r.m_Branches.size() - 1; j >= 0; j--) {
099: Branch b = findBranch((Branch) r.m_Branches.get(j),
100: revision);
101: if (b != null) {
102: return b;
103: }
104: }
105: }
106: if (revision.m_RevisionDetails.getNumber().startsWith(
107: branch.m_Branch + ".")) {
108: return branch;
109: }
110: return null;
111: }
112:
113: private Branch m_RootBranch;
114:
115: private FontMetrics m_FontMetrics;
116:
117: private double m_dScale = 1;
118:
119: private int m_nMaxWidth;
120:
121: private int m_nMaxHeight;
122:
123: private Color m_BranchStartColor = Color.pink;
124:
125: private Color m_RevisionColor = new Color(120, 220, 130);
126:
127: /**
128: * Displays log details as a graph in a panel.
129: *
130: * @author Nicholas Allen
131: */
132: class LogDetailsGraph extends JPanel {
133:
134: /**
135: * Creates a new LogDetailsGraph object.
136: */
137: public LogDetailsGraph() {
138: setBackground(Color.white);
139: setToolTipText("<html>1<br>2<br>3");
140: setPreferredSize(new Dimension(300, 300));
141: }
142:
143: /**
144: * DOCUMENT ME!
145: *
146: * @param details DOCUMENT ME!
147: */
148: public void setRootBranch(Branch branch) {
149: m_RootBranch = branch;
150: m_FontMetrics = null;
151: repaint();
152: }
153:
154: /**
155: * DOCUMENT ME!
156: *
157: * @return DOCUMENT ME!
158: */
159: public double getScale() {
160: return m_dScale;
161: }
162:
163: /**
164: * DOCUMENT ME!
165: *
166: * @param d DOCUMENT ME!
167: */
168: public void setScale(double d) {
169: m_dScale = d;
170: updatePreferredSize();
171: }
172:
173: private void updatePreferredSize() {
174: if (m_FontMetrics != null) {
175: setPreferredSize(new Dimension(
176: (int) (m_nMaxWidth * m_dScale),
177: (int) (m_nMaxHeight * m_dScale)));
178: revalidate();
179: repaint();
180: }
181: }
182:
183: /**
184: * DOCUMENT ME!
185: *
186: * @param e DOCUMENT ME!
187: *
188: * @return DOCUMENT ME!
189: */
190: public String getToolTipText(MouseEvent e) {
191: Revision r = getRevisionAtPoint(e.getX(), e.getY());
192: if (r != null)
193: return r.getToolTipText();
194: return null;
195: }
196:
197: private Revision getRevisionAtPoint(int x, int y) {
198: if (m_RootBranch != null)
199: return m_RootBranch.getRevisionAtPoint(
200: (int) (x / m_dScale), (int) (y / m_dScale));
201: return null;
202: }
203:
204: protected void paintComponent(Graphics g) {
205: super .paintComponent(g);
206: Graphics2D g2 = (Graphics2D) g;
207: g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
208: RenderingHints.VALUE_ANTIALIAS_ON);
209: g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
210: RenderingHints.VALUE_FRACTIONALMETRICS_ON);
211: if (m_FontMetrics == null) {
212: m_FontMetrics = g.getFontMetrics(getFont());
213: if (m_RootBranch != null) {
214: m_nMaxWidth = 0;
215: m_nMaxHeight = 0;
216: m_RootBranch.calculateSizes();
217: m_RootBranch.layout(20, 20);
218: updatePreferredSize();
219: }
220: return;
221: }
222: g2.scale(m_dScale, m_dScale);
223: if (m_RootBranch != null)
224: m_RootBranch.paint(g);
225: }
226: }
227:
228: /** Represent a branch on the graph which has a sequence of revisions. */
229: private class Branch {
230: private int m_nX;
231:
232: private int m_nY;
233:
234: private int m_nWidth;
235:
236: private int m_nHeight;
237:
238: private boolean m_bLayedOut = false;
239:
240: private java.util.List m_Revisions = new ArrayList();
241:
242: private String m_Branch;
243:
244: Branch(String sBranch) {
245: m_Branch = sBranch;
246: }
247:
248: public Rectangle getBounds() {
249: return new Rectangle(m_nX, m_nY, m_nWidth, m_nHeight);
250: }
251:
252: public void calculateSizes() {
253: m_nWidth = 0;
254: m_nHeight = 0;
255: Iterator i = m_Revisions.iterator();
256: while (i.hasNext()) {
257: Revision r = (Revision) i.next();
258: r.calculateSizes();
259: if (r.m_nWidth > m_nWidth)
260: m_nWidth = r.m_nWidth;
261: m_nHeight += r.m_nHeight;
262: }
263: m_nHeight += (50 * (m_Revisions.size() - 1));
264: m_bLayedOut = false;
265: }
266:
267: public void layout(int x, int y) {
268: m_nX = x;
269: m_nY = y;
270: m_bLayedOut = true;
271: y += m_nHeight;
272: for (int i = m_Revisions.size() - 1; i >= 0; i--) {
273: Revision r = ((Revision) m_Revisions.get(i));
274: y -= r.m_nHeight;
275: r.layout(x + ((m_nWidth - r.m_nWidth) / 2), y);
276: y -= 50;
277: }
278: }
279:
280: private Revision getRevisionAtPoint(int x, int y) {
281: Iterator i = m_Revisions.iterator();
282: while (i.hasNext()) {
283: Revision r = (Revision) i.next();
284: Revision result = r.getRevisionAtPoint(x, y);
285: if (result != null)
286: return result;
287: }
288: return null;
289: }
290:
291: /**
292: * Gets the current overlap of any branches that have already been layed out
293: * with the supplied rectangle. Returns null if there is no overlap.
294: */
295: private Rectangle getBranchOverlap(Rectangle bounds) {
296: if (m_bLayedOut) {
297: Rectangle overlap = bounds.intersection(new Rectangle(
298: m_nX, m_nY, m_nWidth, m_nHeight));
299: if (overlap != null)
300: return overlap;
301: }
302: Iterator i = m_Revisions.iterator();
303: while (i.hasNext()) {
304: Revision r = (Revision) i.next();
305: Rectangle overlap = r.getBranchOverlap(bounds);
306: if (overlap != null)
307: return overlap;
308: }
309: return null;
310: }
311:
312: public void paint(Graphics g) {
313: int x = m_nX + (m_nWidth / 2);
314: for (int i = 0; i < m_Revisions.size(); i++) {
315: Revision r = (Revision) m_Revisions.get(i);
316: r.paint(g);
317: if (i < (m_Revisions.size() - 1))
318: g.drawLine(x, r.m_nY + r.m_nHeight, x, r.m_nY
319: + r.m_nHeight + 50);
320: }
321: }
322: }
323:
324: /**
325: * Represents one Revision object on the graph that displays details about a
326: * revision. A revision also contains a list of branches that started at this
327: * revision.
328: */
329: private class Revision {
330: private LogInformation.Revision m_RevisionDetails;
331:
332: private java.util.List m_Branches = new ArrayList();
333:
334: private int m_nX;
335:
336: private int m_nY;
337:
338: private int m_nWidth;
339:
340: private int m_nHeight;
341:
342: private boolean m_bBranchStart;
343:
344: private String m_sTooltip;
345:
346: private List m_lTags;
347:
348: Revision(LogInformation.Revision revisionDetails,
349: boolean branchStart, List tags) {
350: m_RevisionDetails = revisionDetails;
351: m_bBranchStart = branchStart;
352: m_lTags = tags;
353: }
354:
355: void addBranch(Branch branch) {
356: m_Branches.add(branch);
357: }
358:
359: private Revision getRevisionAtPoint(int x, int y) {
360: if (new Rectangle(m_nX, m_nY, m_nWidth, m_nHeight)
361: .contains(x, y))
362: return this ;
363: for (int i = 0; i < m_Branches.size(); i++) {
364: Branch b = ((Branch) m_Branches.get(i));
365: Revision result = b.getRevisionAtPoint(x, y);
366: if (result != null)
367: return result;
368: }
369: return null;
370: }
371:
372: /*
373: * Calculates the size this revision should be based on revision label, tags
374: * and font metrics.
375: */
376: public void calculateSizes() {
377: // Calculate maximum label width (revision number and tags).
378: m_nWidth = m_FontMetrics.stringWidth(m_RevisionDetails
379: .getNumber());
380: if (m_RevisionDetails.getAuthor() != null)
381: m_nWidth = Math.max(m_FontMetrics
382: .stringWidth(m_RevisionDetails.getAuthor()),
383: m_nWidth);
384: for (int i = 0; i < m_lTags.size(); i++)
385: m_nWidth = Math.max(m_FontMetrics
386: .stringWidth(((LogInformation.SymName) m_lTags
387: .get(i)).getName()), m_nWidth);
388: m_nHeight = m_FontMetrics.getHeight()
389: * (m_lTags.size() + 2);
390: // Allow a 10 pixel border around the revision box.
391: m_nWidth += (10 * 2);
392: m_nHeight += (10 * 2);
393: Iterator i = m_Branches.iterator();
394: while (i.hasNext())
395: ((Branch) i.next()).calculateSizes();
396: }
397:
398: public void layout(int x, int y) {
399: m_nX = x;
400: m_nY = y;
401: x += (m_nWidth + 50);
402: // Layout branches from this revision
403: for (int i = 0; i < m_Branches.size(); i++) {
404: Branch b = ((Branch) m_Branches.get(i));
405: // Find next suitable location for this branch. This is where the bounds
406: // of the branch don't overlap any other branch.
407: while (true) {
408: Rectangle bounds = new Rectangle(x, y, x
409: + b.m_nWidth, y + b.m_nHeight);
410: Rectangle overlap = getBranchOverlap(bounds);
411: if (overlap != null)
412: x = overlap.x + overlap.width + 50;
413: else
414: break;
415: }
416: b.layout(x, y);
417: x += (b.m_nWidth + 50);
418: }
419: if ((m_nX + m_nWidth) > m_nMaxWidth)
420: m_nMaxWidth = m_nX + m_nWidth;
421: if ((m_nY + m_nHeight) > m_nMaxHeight)
422: m_nMaxHeight = m_nY + m_nHeight;
423: }
424:
425: private Rectangle getBranchOverlap(Rectangle bounds) {
426: Iterator i = m_Branches.iterator();
427: while (i.hasNext()) {
428: Branch b = (Branch) i.next();
429: Rectangle overlap = b.getBranchOverlap(bounds);
430: if (overlap != null)
431: return overlap;
432: }
433: return null;
434: }
435:
436: public void paint(Graphics g) {
437: if (m_bBranchStart) {
438: g.setColor(m_BranchStartColor);
439: g
440: .fillRoundRect(m_nX, m_nY, m_nWidth, m_nHeight,
441: 25, 25);
442: g.setColor(Color.black);
443: g
444: .drawRoundRect(m_nX, m_nY, m_nWidth, m_nHeight,
445: 25, 25);
446: } else {
447: g.setColor(m_RevisionColor);
448: g.fillRect(m_nX, m_nY, m_nWidth, m_nHeight);
449: g.setColor(Color.black);
450: g.drawRect(m_nX, m_nY, m_nWidth, m_nHeight);
451: }
452: drawCenteredText(g, m_RevisionDetails.getAuthor(), 0);
453: drawCenteredText(g, m_RevisionDetails.getNumber(), 1);
454: int y = m_nY + 10 + m_FontMetrics.getHeight();
455: g.drawLine(m_nX, y, m_nX + m_nWidth, y);
456: y += m_FontMetrics.getHeight();
457: g.drawLine(m_nX, y, m_nX + m_nWidth, y);
458: for (int i = 0; i < m_lTags.size(); i++)
459: drawCenteredText(g, ((LogInformation.SymName) m_lTags
460: .get(i)).getName(), i + 2);
461: // Paint all the branches from this revision
462: Iterator i = m_Branches.iterator();
463: y = m_nY + (m_nHeight / 2);
464: while (i.hasNext()) {
465: Branch b = ((Branch) i.next());
466: b.paint(g);
467: // If this branch has revisions then draw a horizontal line to the first
468: // revision
469: if (b.m_Revisions.size() > 0) {
470: Revision r = (Revision) b.m_Revisions.get(0);
471: g.drawLine(m_nX + m_nWidth, y, r.m_nX, r.m_nY
472: + (r.m_nHeight / 2));
473: }
474: }
475: }
476:
477: private void drawCenteredText(Graphics g, String sText, int nRow) {
478: if (sText == null)
479: return;
480: g.drawString(sText,
481: m_nX
482: + ((m_nWidth - m_FontMetrics
483: .stringWidth(sText)) / 2), m_nY
484: + 10 + m_FontMetrics.getAscent()
485: + (nRow * m_FontMetrics.getHeight()));
486: }
487:
488: public String getToolTipText() {
489: if (m_sTooltip == null) {
490: String s = m_RevisionDetails.getMessage();
491: if ((s == null) || (s.trim().length() == 0))
492: return "<html>No log message";
493: StringBuffer b = new StringBuffer();
494: b.append("<html>");
495: if (m_RevisionDetails.getDate() != null) {
496: b.append("<b>");
497: b.append(DateFormat.getDateTimeInstance().format(
498: m_RevisionDetails.getDate()));
499: b.append("</b><br>");
500: }
501: for (int i = 0; i < s.length(); i++) {
502: char c = s.charAt(i);
503: if (c == '\n')
504: b.append("<br>");
505: else
506: b.append(c);
507: }
508: m_sTooltip = b.toString();
509: }
510: return m_sTooltip;
511: }
512: }
513: }
|