0001: /*
0002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
0003: *
0004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
0005: *
0006: * The contents of this file are subject to the terms of either the GNU
0007: * General Public License Version 2 only ("GPL") or the Common
0008: * Development and Distribution License("CDDL") (collectively, the
0009: * "License"). You may not use this file except in compliance with the
0010: * License. You can obtain a copy of the License at
0011: * http://www.netbeans.org/cddl-gplv2.html
0012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
0013: * specific language governing permissions and limitations under the
0014: * License. When distributing the software, include this License Header
0015: * Notice in each file and include the License file at
0016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
0017: * particular file as subject to the "Classpath" exception as provided
0018: * by Sun in the GPL Version 2 section of the License file that
0019: * accompanied this code. If applicable, add the following below the
0020: * License Header, with the fields enclosed by brackets [] replaced by
0021: * your own identifying information:
0022: * "Portions Copyrighted [year] [name of copyright owner]"
0023: *
0024: * Contributor(s):
0025: *
0026: * The Original Software is NetBeans. The Initial Developer of the Original
0027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
0028: * Microsystems, Inc. All Rights Reserved.
0029: *
0030: * If you wish your version of this file to be governed by only the CDDL
0031: * or only the GPL Version 2, indicate your decision by adding
0032: * "[Contributor] elects to include this software in this distribution
0033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
0034: * single choice of license, a recipient has the option to distribute
0035: * your version of this file under either the CDDL, the GPL Version 2 or
0036: * to extend the choice of license to its licensees as provided above.
0037: * However, if you add GPL Version 2 code and therefore, elected the GPL
0038: * Version 2 license, then the option applies only if the new code is
0039: * made subject to such option by the copyright holder.
0040: */
0041:
0042: package org.netbeans.modules.subversion.ui.blame;
0043:
0044: import org.netbeans.editor.*;
0045: import org.netbeans.editor.Utilities;
0046: import org.netbeans.api.editor.fold.*;
0047: import org.netbeans.api.diff.*;
0048: import org.netbeans.spi.diff.*;
0049: import org.netbeans.modules.subversion.ui.update.RevertModifications;
0050: import org.netbeans.modules.subversion.ui.update.RevertModificationsAction;
0051: import org.netbeans.modules.subversion.ui.diff.DiffAction;
0052: import org.netbeans.modules.subversion.RepositoryFile;
0053: import org.netbeans.modules.subversion.Subversion;
0054: import org.netbeans.modules.subversion.client.SvnProgressSupport;
0055: import org.netbeans.modules.subversion.util.SvnUtils;
0056: import org.netbeans.modules.subversion.util.Context;
0057: import org.netbeans.modules.versioning.util.Utils;
0058: import org.openide.*;
0059: import org.openide.loaders.*;
0060: import org.openide.filesystems.*;
0061: import org.openide.text.*;
0062: import org.openide.util.*;
0063: import org.openide.xml.*;
0064: import org.tigris.subversion.svnclientadapter.SVNUrl;
0065: import org.tigris.subversion.svnclientadapter.SVNRevision;
0066: import javax.swing.*;
0067: import javax.swing.Timer;
0068: import javax.swing.event.*;
0069: import javax.swing.text.*;
0070: import javax.accessibility.Accessible;
0071: import java.awt.*;
0072: import java.awt.event.*;
0073: import java.beans.*;
0074: import java.util.*;
0075: import java.util.List;
0076: import java.io.*;
0077: import java.text.DateFormat;
0078: import java.text.MessageFormat;
0079: import java.util.logging.Level;
0080: import org.netbeans.modules.subversion.client.SvnClientExceptionHandler;
0081: import org.tigris.subversion.svnclientadapter.ISVNNotifyListener;
0082: import org.tigris.subversion.svnclientadapter.SVNClientException;
0083:
0084: /**
0085: * Represents annotation sidebar componnet in editor. It's
0086: * created by {@link AnnotationBarManager}.
0087: *
0088: * <p>It reponds to following external signals:
0089: * <ul>
0090: * <li> {@link #annotate} message
0091: * </ul>
0092: *
0093: * @author Petr Kuzel
0094: */
0095: final class AnnotationBar extends JComponent implements Accessible,
0096: PropertyChangeListener, DocumentListener, ChangeListener,
0097: ActionListener, Runnable, ComponentListener {
0098:
0099: /**
0100: * Target text component for which the annotation bar is aiming.
0101: */
0102: private final JTextComponent textComponent;
0103:
0104: /**
0105: * User interface related to the target text component.
0106: */
0107: private final EditorUI editorUI;
0108:
0109: /**
0110: * Fold hierarchy of the text component user interface.
0111: */
0112: private final FoldHierarchy foldHierarchy;
0113:
0114: /**
0115: * Document related to the target text component.
0116: */
0117: private final BaseDocument doc;
0118:
0119: /**
0120: * Caret of the target text component.
0121: */
0122: private final Caret caret;
0123:
0124: /**
0125: * Caret batch timer launched on receiving
0126: * annotation data structures (AnnotateLine).
0127: */
0128: private Timer caretTimer;
0129:
0130: /**
0131: * Controls annotation bar visibility.
0132: */
0133: private boolean annotated;
0134:
0135: /**
0136: * Maps document {@link javax.swing.text.Element}s (representing lines) to
0137: * {@link AnnotateLine}. <code>null</code> means that
0138: * no data are available, yet. So alternative
0139: * {@link #elementAnnotationsSubstitute} text shoudl be used.
0140: *
0141: * @thread it is accesed from multiple threads all mutations
0142: * and iterations must be under elementAnnotations lock,
0143: */
0144: private Map<Element, AnnotateLine> elementAnnotations;
0145:
0146: /**
0147: * Represents text that should be displayed in
0148: * visible bar with yet <code>null</code> elementAnnotations.
0149: */
0150: private String elementAnnotationsSubstitute;
0151:
0152: private Color backgroundColor = Color.WHITE;
0153: private Color foregroundColor = Color.BLACK;
0154: private Color selectedColor = Color.BLUE;
0155:
0156: /**
0157: * Most recent status message.
0158: */
0159: private String recentStatusMessage;
0160:
0161: /**
0162: * Revision associated with caret line.
0163: */
0164: private String recentRevision;
0165:
0166: /**
0167: * Request processor to create threads that may be cancelled.
0168: */
0169: RequestProcessor requestProcessor = null;
0170:
0171: /**
0172: * Latest annotation comment fetching task launched.
0173: */
0174: private RequestProcessor.Task latestAnnotationTask = null;
0175:
0176: /**
0177: * Holds false if Rollback Changes action is NOT valid for current revision, true otherwise.
0178: */
0179: private boolean recentRevisionCanBeRolledBack;
0180:
0181: /**
0182: * Rendering hints for annotations sidebar inherited from editor settings.
0183: */
0184: private final Map renderingHints;
0185:
0186: /**
0187: * Creates new instance initializing final fields.
0188: */
0189: public AnnotationBar(JTextComponent target) {
0190: this .textComponent = target;
0191: this .editorUI = Utilities.getEditorUI(target);
0192: this .foldHierarchy = FoldHierarchy.get(editorUI.getComponent());
0193: this .doc = editorUI.getDocument();
0194: this .caret = textComponent.getCaret();
0195: if (textComponent instanceof JEditorPane) {
0196: JEditorPane jep = (JEditorPane) textComponent;
0197: Class kitClass = jep.getEditorKit().getClass();
0198: Object userSetHints = Settings.getValue(kitClass,
0199: SettingsNames.RENDERING_HINTS);
0200: renderingHints = (userSetHints instanceof Map && ((Map) userSetHints)
0201: .size() > 0) ? (Map) userSetHints : null;
0202: } else {
0203: renderingHints = null;
0204: }
0205: }
0206:
0207: // public contract ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0208:
0209: /**
0210: * Makes the bar visible and sensitive to
0211: * LogOutoutListener events that should deliver
0212: * actual content to be displayed.
0213: */
0214: public void annotate() {
0215: annotated = true;
0216: elementAnnotations = null;
0217:
0218: doc.addDocumentListener(this );
0219: textComponent.addComponentListener(this );
0220: editorUI.addPropertyChangeListener(this );
0221:
0222: revalidate(); // resize the component
0223: }
0224:
0225: public void setAnnotationMessage(String message) {
0226: elementAnnotationsSubstitute = message;
0227: revalidate();
0228: }
0229:
0230: /**
0231: * Result computed show it...
0232: * Takes AnnotateLines and shows them.
0233: */
0234: public void annotationLines(File file,
0235: List<AnnotateLine> annotateLines) {
0236: List<AnnotateLine> lines = new LinkedList<AnnotateLine>(
0237: annotateLines);
0238: int lineCount = lines.size();
0239: /** 0 based line numbers => 1 based line numbers*/
0240: int ann2editorPermutation[] = new int[lineCount];
0241: for (int i = 0; i < lineCount; i++) {
0242: ann2editorPermutation[i] = i + 1;
0243: }
0244:
0245: DiffProvider diff = (DiffProvider) Lookup.getDefault().lookup(
0246: DiffProvider.class);
0247: if (diff != null) {
0248: Reader r = new LinesReader(lines);
0249: Reader docReader = Utils.getDocumentReader(doc);
0250: try {
0251:
0252: Difference[] differences = diff.computeDiff(r,
0253: docReader);
0254:
0255: // customize annotation line numbers to match different reality
0256: // compule line permutation
0257:
0258: for (int i = 0; i < differences.length; i++) {
0259: Difference d = differences[i];
0260: if (d.getType() == Difference.ADD)
0261: continue;
0262:
0263: int editorStart;
0264: int firstShift = d.getFirstEnd()
0265: - d.getFirstStart() + 1;
0266: if (d.getType() == Difference.CHANGE) {
0267: int firstLen = d.getFirstEnd()
0268: - d.getFirstStart();
0269: int secondLen = d.getSecondEnd()
0270: - d.getSecondStart();
0271: if (secondLen >= firstLen)
0272: continue; // ADD or pure CHANGE
0273: editorStart = d.getSecondStart();
0274: firstShift = firstLen - secondLen;
0275: } else { // DELETE
0276: editorStart = d.getSecondStart() + 1;
0277: }
0278:
0279: for (int c = editorStart + firstShift - 1; c < lineCount; c++) {
0280: ann2editorPermutation[c] -= firstShift;
0281: }
0282: }
0283:
0284: for (int i = differences.length - 1; i >= 0; i--) {
0285: Difference d = differences[i];
0286: if (d.getType() == Difference.DELETE)
0287: continue;
0288:
0289: int firstStart;
0290: int firstShift = d.getSecondEnd()
0291: - d.getSecondStart() + 1;
0292: if (d.getType() == Difference.CHANGE) {
0293: int firstLen = d.getFirstEnd()
0294: - d.getFirstStart();
0295: int secondLen = d.getSecondEnd()
0296: - d.getSecondStart();
0297: if (secondLen <= firstLen)
0298: continue; // REMOVE or pure CHANGE
0299: firstShift = secondLen - firstLen;
0300: firstStart = d.getFirstStart();
0301: } else {
0302: firstStart = d.getFirstStart() + 1;
0303: }
0304:
0305: for (int k = firstStart - 1; k < lineCount; k++) {
0306: ann2editorPermutation[k] += firstShift;
0307: }
0308: }
0309:
0310: } catch (IOException e) {
0311: Subversion.LOG
0312: .log(
0313: Level.INFO,
0314: "Cannot compute local diff required for annotations, ignoring...",
0315: e);
0316: }
0317: }
0318:
0319: try {
0320: doc.atomicLock();
0321: StyledDocument sd = (StyledDocument) doc;
0322: Iterator<AnnotateLine> it = lines.iterator();
0323: elementAnnotations = Collections
0324: .synchronizedMap(new HashMap<Element, AnnotateLine>(
0325: lines.size()));
0326: while (it.hasNext()) {
0327: AnnotateLine line = it.next();
0328: int lineNum = ann2editorPermutation[line.getLineNum() - 1];
0329: try {
0330: int lineOffset = NbDocument.findLineOffset(sd,
0331: lineNum - 1);
0332: Element element = sd
0333: .getParagraphElement(lineOffset);
0334: elementAnnotations.put(element, line);
0335: } catch (IndexOutOfBoundsException ex) {
0336: // TODO how could I get line behind document end?
0337: // furtunately user does not spot it
0338: Subversion.LOG.log(Level.INFO, null, ex);
0339: }
0340: }
0341: } finally {
0342: doc.atomicUnlock();
0343: }
0344:
0345: // lazy listener registration
0346: caret.addChangeListener(this );
0347: this .caretTimer = new Timer(500, this );
0348: caretTimer.setRepeats(false);
0349:
0350: onCurrentLine();
0351: revalidate();
0352: repaint();
0353: }
0354:
0355: private ISVNNotifyListener svnClientListener;
0356:
0357: void setSVNClienListener(ISVNNotifyListener svnClientListener) {
0358: this .svnClientListener = svnClientListener;
0359:
0360: File file = getCurrentFile();
0361: Subversion.getInstance()
0362: .addSVNNotifyListener(svnClientListener);
0363: }
0364:
0365: // implementation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0366:
0367: /**
0368: * Gets a the file related to the document
0369: *
0370: * @return the file related to the document, <code>null</code> if none
0371: * exists.
0372: */
0373: private File getCurrentFile() {
0374: File result = null;
0375:
0376: DataObject dobj = (DataObject) doc
0377: .getProperty(Document.StreamDescriptionProperty);
0378: if (dobj != null) {
0379: FileObject fo = dobj.getPrimaryFile();
0380: result = FileUtil.toFile(fo);
0381: }
0382:
0383: return result;
0384: }
0385:
0386: /**
0387: * Registers "close" popup menu, tooltip manager
0388: * and repaint on documet change manager.
0389: */
0390: public void addNotify() {
0391: super .addNotify();
0392:
0393: this .addMouseListener(new MouseAdapter() {
0394: public void mousePressed(MouseEvent e) {
0395: maybeShowPopup(e);
0396: }
0397:
0398: public void mouseReleased(MouseEvent e) {
0399: maybeShowPopup(e);
0400: }
0401:
0402: private void maybeShowPopup(MouseEvent e) {
0403: if (e.isPopupTrigger()) {
0404: e.consume();
0405: createPopup().show(e.getComponent(), e.getX(),
0406: e.getY());
0407: }
0408: }
0409: });
0410:
0411: // register with tooltip manager
0412: setToolTipText(""); // NOI18N
0413:
0414: }
0415:
0416: private JPopupMenu createPopup() {
0417: final ResourceBundle loc = NbBundle
0418: .getBundle(AnnotationBar.class);
0419: final JPopupMenu popupMenu = new JPopupMenu();
0420:
0421: final File file = getCurrentFile();
0422:
0423: final JMenuItem diffMenu = new JMenuItem(loc
0424: .getString("CTL_MenuItem_DiffToRevision"));
0425: diffMenu.addActionListener(new ActionListener() {
0426: public void actionPerformed(ActionEvent e) {
0427: if (recentRevision != null) {
0428: if (getPreviousRevision(recentRevision) != null) {
0429: DiffAction.diff(file,
0430: getPreviousRevision(recentRevision),
0431: recentRevision);
0432: }
0433: }
0434: }
0435: });
0436: popupMenu.add(diffMenu);
0437:
0438: JMenuItem rollbackMenu = new JMenuItem(loc
0439: .getString("CTL_MenuItem_Revert"));
0440: rollbackMenu.addActionListener(new ActionListener() {
0441: public void actionPerformed(ActionEvent e) {
0442: revert(file, recentRevision);
0443: }
0444: });
0445: popupMenu.add(rollbackMenu);
0446: rollbackMenu.setEnabled(recentRevisionCanBeRolledBack);
0447:
0448: JMenuItem menu;
0449: menu = new JMenuItem(loc
0450: .getString("CTL_MenuItem_CloseAnnotations"));
0451: menu.addActionListener(new ActionListener() {
0452: public void actionPerformed(ActionEvent e) {
0453: hideBar();
0454: }
0455: });
0456: popupMenu.addSeparator();
0457: popupMenu.add(menu);
0458:
0459: diffMenu.setVisible(false);
0460: rollbackMenu.setVisible(false);
0461: if (recentRevision != null) {
0462: if (getPreviousRevision(recentRevision) != null) {
0463: String format = loc
0464: .getString("CTL_MenuItem_DiffToRevision");
0465: diffMenu.setText(MessageFormat.format(format,
0466: new Object[] { recentRevision,
0467: getPreviousRevision(recentRevision) }));
0468: diffMenu.setVisible(true);
0469: }
0470: rollbackMenu.setVisible(true);
0471: }
0472:
0473: return popupMenu;
0474: }
0475:
0476: private void revert(File file, String revision) {
0477: final Context ctx = new Context(file);
0478:
0479: final SVNUrl url;
0480: try {
0481: url = SvnUtils.getRepositoryRootUrl(file);
0482: } catch (SVNClientException ex) {
0483: SvnClientExceptionHandler.notifyException(ex, true, true);
0484: return;
0485: }
0486: final RepositoryFile repositoryFile = new RepositoryFile(url,
0487: url, SVNRevision.HEAD);
0488:
0489: final RevertModifications revertModifications = new RevertModifications(
0490: repositoryFile, revision);
0491: if (!revertModifications.showDialog()) {
0492: return;
0493: }
0494:
0495: RequestProcessor rp = Subversion.getInstance()
0496: .getRequestProcessor(url);
0497: SvnProgressSupport support = new SvnProgressSupport() {
0498: public void perform() {
0499: RevertModificationsAction
0500: .performRevert(revertModifications
0501: .getRevisionInterval(),
0502: revertModifications.revertNewFiles(),
0503: ctx, this );
0504: }
0505: };
0506: support.start(rp, url, NbBundle.getMessage(AnnotationBar.class,
0507: "MSG_Revert_Progress")); // NOI18N
0508: }
0509:
0510: private String getPreviousRevision(String revision) {
0511: return Long.toString(Long.parseLong(revision) - 1);
0512: }
0513:
0514: /**
0515: * Hides the annotation bar from user.
0516: */
0517: void hideBar() {
0518: annotated = false;
0519: revalidate();
0520: release();
0521: }
0522:
0523: /**
0524: * Gets a request processor which is able to cancel tasks.
0525: */
0526: private RequestProcessor getRequestProcessor() {
0527: if (requestProcessor == null) {
0528: requestProcessor = new RequestProcessor("AnnotationBarRP",
0529: 1, true); // NOI18N
0530: }
0531:
0532: return requestProcessor;
0533: }
0534:
0535: /**
0536: * Shows commit message in status bar and or revision change repaints side
0537: * bar (to highlight same revision). This process is started in a
0538: * seperate thread.
0539: */
0540: private void onCurrentLine() {
0541: if (latestAnnotationTask != null) {
0542: latestAnnotationTask.cancel();
0543: }
0544:
0545: latestAnnotationTask = getRequestProcessor().post(this );
0546: }
0547:
0548: // latestAnnotationTask business logic
0549: public void run() {
0550: // get resource bundle
0551: ResourceBundle loc = NbBundle.getBundle(AnnotationBar.class);
0552: // give status bar "wait" indication
0553: StatusBar statusBar = editorUI.getStatusBar();
0554: recentStatusMessage = loc
0555: .getString("CTL_StatusBar_WaitFetchAnnotation");
0556: statusBar.setText(StatusBar.CELL_MAIN, recentStatusMessage);
0557:
0558: recentRevisionCanBeRolledBack = false;
0559: // determine current line
0560: int line = -1;
0561: int offset = caret.getDot();
0562: try {
0563: line = Utilities.getLineOffset(doc, offset);
0564: } catch (BadLocationException ex) {
0565: Subversion.LOG.log(Level.SEVERE,
0566: "Can not get line for caret at offset " + offset,
0567: ex); // NOI18N
0568: clearRecentFeedback();
0569: return;
0570: }
0571:
0572: // handle locally modified lines
0573: AnnotateLine al = getAnnotateLine(line);
0574: if (al == null) {
0575: AnnotationMarkProvider amp = AnnotationMarkInstaller
0576: .getMarkProvider(textComponent);
0577: if (amp != null) {
0578: amp.setMarks(Collections.<AnnotationMark> emptyList());
0579: }
0580: clearRecentFeedback();
0581: if (recentRevision != null) {
0582: recentRevision = null;
0583: repaint();
0584: }
0585: return;
0586: }
0587:
0588: recentRevisionCanBeRolledBack = al.canBeRolledBack();
0589:
0590: // handle unchanged lines
0591: String revision = al.getRevision();
0592: if (revision.equals(recentRevision) == false) {
0593: recentRevision = revision;
0594: repaint();
0595:
0596: AnnotationMarkProvider amp = AnnotationMarkInstaller
0597: .getMarkProvider(textComponent);
0598: if (amp != null) {
0599:
0600: List<AnnotationMark> marks = new ArrayList<AnnotationMark>(
0601: elementAnnotations.size());
0602: // I cannot affort to lock elementAnnotations for long time
0603: // it's accessed from editor thread too
0604: Iterator<Map.Entry<Element, AnnotateLine>> it2;
0605: synchronized (elementAnnotations) {
0606: it2 = new HashSet<Map.Entry<Element, AnnotateLine>>(
0607: elementAnnotations.entrySet()).iterator();
0608: }
0609: while (it2.hasNext()) {
0610: Map.Entry<Element, AnnotateLine> next = it2.next();
0611: AnnotateLine annotateLine = next.getValue();
0612: if (revision.equals(annotateLine.getRevision())) {
0613: Element element = next.getKey();
0614: if (elementAnnotations.containsKey(element) == false) {
0615: continue;
0616: }
0617: int elementOffset = element.getStartOffset();
0618: int lineNumber = NbDocument.findLineNumber(
0619: (StyledDocument) doc, elementOffset);
0620: AnnotationMark mark = new AnnotationMark(
0621: lineNumber, revision);
0622: marks.add(mark);
0623: }
0624:
0625: if (Thread.interrupted()) {
0626: clearRecentFeedback();
0627: return;
0628: }
0629: }
0630: amp.setMarks(marks);
0631: }
0632: }
0633:
0634: if (al.getCommitMessage() != null) {
0635: recentStatusMessage = al.getCommitMessage();
0636: statusBar.setText(StatusBar.CELL_MAIN, al.getAuthor()
0637: + ": " + recentStatusMessage); // NOI18N
0638: } else {
0639: clearRecentFeedback();
0640: }
0641: ;
0642: }
0643:
0644: /**
0645: * Clears the status bar if it contains the latest status message
0646: * displayed by this annotation bar.
0647: */
0648: private void clearRecentFeedback() {
0649: StatusBar statusBar = editorUI.getStatusBar();
0650: if (statusBar.getText(StatusBar.CELL_MAIN) == recentStatusMessage) {
0651: statusBar.setText(StatusBar.CELL_MAIN, ""); // NOI18N
0652: }
0653: }
0654:
0655: /**
0656: * Components created by SibeBarFactory are positioned
0657: * using a Layout manager that determines componnet size
0658: * by retireving preferred size.
0659: *
0660: * <p>Once componnet needs resizing it simply calls
0661: * {@link #revalidate} that triggers new layouting
0662: * that consults prefered size.
0663: */
0664: public Dimension getPreferredSize() {
0665: Dimension dim = textComponent.getSize();
0666: int width = annotated ? getBarWidth() : 0;
0667: dim.width = width;
0668: dim.height *= 2; // XXX
0669: return dim;
0670: }
0671:
0672: /**
0673: * Gets the maximum size of this component.
0674: *
0675: * @return the maximum size of this component
0676: */
0677: public Dimension getMaximumSize() {
0678: return getPreferredSize();
0679: }
0680:
0681: /**
0682: * Gets the preferred width of this component.
0683: *
0684: * @return the preferred width of this component
0685: */
0686: private int getBarWidth() {
0687: String longestString = ""; // NOI18N
0688: if (elementAnnotations == null) {
0689: longestString = elementAnnotationsSubstitute;
0690: } else {
0691: synchronized (elementAnnotations) {
0692: Iterator<AnnotateLine> it = elementAnnotations.values()
0693: .iterator();
0694: while (it.hasNext()) {
0695: AnnotateLine line = it.next();
0696: String displayName = getDisplayName(line); // NOI18N
0697: if (displayName.length() > longestString.length()) {
0698: longestString = displayName;
0699: }
0700: }
0701: }
0702: }
0703: char[] data = longestString.toCharArray();
0704: Graphics g = getGraphics();
0705: if (g != null) {
0706: int w = g.getFontMetrics().charsWidth(data, 0, data.length);
0707: return w + 4;
0708: } else {
0709: return 0;
0710: }
0711:
0712: }
0713:
0714: private String getDisplayName(AnnotateLine line) {
0715: return line.getRevision() + " " + line.getAuthor(); // NOI18N
0716: }
0717:
0718: /**
0719: * Pair method to {@link #annotate}. It releases
0720: * all resources.
0721: */
0722: private void release() {
0723: editorUI.removePropertyChangeListener(this );
0724: textComponent.removeComponentListener(this );
0725: doc.removeDocumentListener(this );
0726: caret.removeChangeListener(this );
0727: if (caretTimer != null) {
0728: caretTimer.removeActionListener(this );
0729: }
0730: elementAnnotations = null;
0731: // cancel running annotation task if active
0732: if (latestAnnotationTask != null) {
0733: latestAnnotationTask.cancel();
0734: }
0735: AnnotationMarkProvider amp = AnnotationMarkInstaller
0736: .getMarkProvider(textComponent);
0737: if (amp != null) {
0738: amp.setMarks(Collections.<AnnotationMark> emptyList());
0739: }
0740:
0741: Subversion.getInstance().removeSVNNotifyListener(
0742: svnClientListener);
0743:
0744: clearRecentFeedback();
0745:
0746: }
0747:
0748: /**
0749: * Paints one view that corresponds to a line (or
0750: * multiple lines if folding takes effect).
0751: */
0752: private void paintView(View view, Graphics g, int yBase) {
0753: JTextComponent component = editorUI.getComponent();
0754: if (component == null)
0755: return;
0756: BaseTextUI textUI = (BaseTextUI) component.getUI();
0757:
0758: Element rootElem = textUI.getRootView(component).getElement();
0759: int line = rootElem.getElementIndex(view.getStartOffset());
0760:
0761: String annotation = ""; // NOI18N
0762: AnnotateLine al = null;
0763: if (elementAnnotations != null) {
0764: al = getAnnotateLine(line);
0765: if (al != null) {
0766: annotation = getDisplayName(al); // NOI18N
0767: }
0768: } else {
0769: annotation = elementAnnotationsSubstitute;
0770: }
0771:
0772: if (al != null && al.getRevision().equals(recentRevision)) {
0773: g.setColor(selectedColor());
0774: } else {
0775: g.setColor(foregroundColor());
0776: }
0777: g.drawString(annotation, 2, yBase + editorUI.getLineAscent());
0778: }
0779:
0780: /**
0781: * Presents commit message as tooltips.
0782: */
0783: public String getToolTipText(MouseEvent e) {
0784: if (editorUI == null)
0785: return null;
0786: int line = getLineFromMouseEvent(e);
0787:
0788: StringBuffer annotation = new StringBuffer();
0789: if (elementAnnotations != null) {
0790: AnnotateLine al = getAnnotateLine(line);
0791:
0792: if (al != null) {
0793: String escapedAuthor = NbBundle.getMessage(
0794: AnnotationBar.class, "TT_Annotation"); // NOI18N
0795: try {
0796: escapedAuthor = XMLUtil.toElementContent(al
0797: .getAuthor());
0798: } catch (CharConversionException e1) {
0799: Subversion.LOG.log(Level.INFO,
0800: " can not HTML escape: " + al.getAuthor(),
0801: e1);
0802: }
0803:
0804: // always return unique string to avoid tooltip sharing on mouse move over same revisions -->
0805: annotation.append("<html><!-- line=" + line++ + " -->"
0806: + al.getRevision() + " - <b>" + escapedAuthor
0807: + "</b>"); // NOI18N
0808: if (al.getDate() != null) {
0809: annotation.append(" "
0810: + DateFormat.getDateInstance().format(
0811: al.getDate())); // NOI18N
0812: }
0813: if (al.getCommitMessage() != null) {
0814: String escaped = null;
0815: try {
0816: escaped = XMLUtil.toElementContent(al
0817: .getCommitMessage());
0818: } catch (CharConversionException e1) {
0819: Subversion.LOG.log(Level.INFO,
0820: " can not HTML escape: "
0821: + al.getCommitMessage(), e1); // NOI18N
0822: }
0823: if (escaped != null) {
0824: String lined = escaped.replaceAll(System
0825: .getProperty("line.separator"), "<br>"); // NOI18N
0826: annotation.append("<p>" + lined); // NOI18N
0827: }
0828: }
0829: }
0830: } else {
0831: annotation.append(elementAnnotationsSubstitute);
0832: }
0833:
0834: return annotation.toString();
0835: }
0836:
0837: /**
0838: * Locates AnnotateLine associated with given line. The
0839: * line is translated to Element that is used as map lookup key.
0840: * The map is initially filled up with Elements sampled on
0841: * annotate() method.
0842: *
0843: * <p>Key trick is that Element's identity is maintained
0844: * until line removal (and is restored on undo).
0845: *
0846: * @param line
0847: * @return found AnnotateLine or <code>null</code>
0848: */
0849: private AnnotateLine getAnnotateLine(int line) {
0850: StyledDocument sd = (StyledDocument) doc;
0851: int lineOffset = NbDocument.findLineOffset(sd, line);
0852: Element element = sd.getParagraphElement(lineOffset);
0853: AnnotateLine al = elementAnnotations.get(element);
0854:
0855: if (al != null) {
0856: int startOffset = element.getStartOffset();
0857: int endOffset = element.getEndOffset();
0858: try {
0859: int len = endOffset - startOffset;
0860: String text = doc.getText(startOffset, len - 1);
0861: String content = al.getContent();
0862: if (text.equals(content)) {
0863: return al;
0864: }
0865: } catch (BadLocationException e) {
0866: Subversion.LOG.log(Level.INFO,
0867: " can not locate line annotation.", e); // NOI18N
0868: }
0869: }
0870:
0871: return null;
0872: }
0873:
0874: /**
0875: * GlyphGutter copy pasted bolerplate method.
0876: * It invokes {@link #paintView} that contains
0877: * actual business logic.
0878: */
0879: public void paintComponent(Graphics g) {
0880: super .paintComponent(g);
0881:
0882: Rectangle clip = g.getClipBounds();
0883:
0884: JTextComponent component = editorUI.getComponent();
0885: if (component == null)
0886: return;
0887:
0888: BaseTextUI textUI = (BaseTextUI) component.getUI();
0889: View rootView = Utilities.getDocumentView(component);
0890: if (rootView == null)
0891: return;
0892:
0893: g.setColor(backgroundColor());
0894: g.fillRect(clip.x, clip.y, clip.width, clip.height);
0895:
0896: if (renderingHints != null) {
0897: ((Graphics2D) g).addRenderingHints(renderingHints);
0898: }
0899:
0900: AbstractDocument doc = (AbstractDocument) component
0901: .getDocument();
0902: doc.readLock();
0903: try {
0904: foldHierarchy.lock();
0905: try {
0906: int startPos = textUI.getPosFromY(clip.y);
0907: int startViewIndex = rootView.getViewIndex(startPos,
0908: Position.Bias.Forward);
0909: int rootViewCount = rootView.getViewCount();
0910:
0911: if (startViewIndex >= 0
0912: && startViewIndex < rootViewCount) {
0913: // find the nearest visible line with an annotation
0914: Rectangle rec = textUI.modelToView(component,
0915: rootView.getView(startViewIndex)
0916: .getStartOffset());
0917: int y = (rec == null) ? 0 : rec.y;
0918:
0919: int clipEndY = clip.y + clip.height;
0920: for (int i = startViewIndex; i < rootViewCount; i++) {
0921: View view = rootView.getView(i);
0922: paintView(view, g, y);
0923: y += editorUI.getLineHeight();
0924: if (y >= clipEndY) {
0925: break;
0926: }
0927: }
0928: }
0929:
0930: } finally {
0931: foldHierarchy.unlock();
0932: }
0933: } catch (BadLocationException ble) {
0934: Subversion.LOG.log(Level.SEVERE, null, ble);
0935: } finally {
0936: doc.readUnlock();
0937: }
0938: }
0939:
0940: private Color backgroundColor() {
0941: if (textComponent != null) {
0942: return textComponent.getBackground();
0943: }
0944: return backgroundColor;
0945: }
0946:
0947: private Color foregroundColor() {
0948: if (textComponent != null) {
0949: return textComponent.getForeground();
0950: }
0951: return foregroundColor;
0952: }
0953:
0954: private Color selectedColor() {
0955: if (backgroundColor == backgroundColor()) {
0956: return selectedColor;
0957: }
0958: if (textComponent != null) {
0959: return textComponent.getForeground();
0960: }
0961: return selectedColor;
0962:
0963: }
0964:
0965: /** GlyphGutter copy pasted utility method. */
0966: private int getLineFromMouseEvent(MouseEvent e) {
0967: int line = -1;
0968: if (editorUI != null) {
0969: try {
0970: JTextComponent component = editorUI.getComponent();
0971: BaseTextUI textUI = (BaseTextUI) component.getUI();
0972: int clickOffset = textUI.viewToModel(component,
0973: new Point(0, e.getY()));
0974: line = Utilities.getLineOffset(doc, clickOffset);
0975: } catch (BadLocationException ble) {
0976: }
0977: }
0978: return line;
0979: }
0980:
0981: /** Implementation */
0982: public void propertyChange(PropertyChangeEvent evt) {
0983: if (evt == null)
0984: return;
0985: String id = evt.getPropertyName();
0986: if (EditorUI.COMPONENT_PROPERTY.equals(id)) { // NOI18N
0987: if (evt.getNewValue() == null) {
0988: // component deinstalled, lets uninstall all isteners
0989: release();
0990: }
0991: }
0992:
0993: }
0994:
0995: /** Implementation */
0996: public void changedUpdate(DocumentEvent e) {
0997: }
0998:
0999: /** Implementation */
1000: public void insertUpdate(DocumentEvent e) {
1001: // handle new lines, Enter hit at end of line changes
1002: // the line element instance
1003: // XXX Actually NB document implementation triggers this method two times
1004: // - first time with one removed and two added lines
1005: // - second time with two removed and two added lines
1006: if (elementAnnotations != null) {
1007: Element[] elements = e.getDocument().getRootElements();
1008: synchronized (elementAnnotations) { // atomic change
1009: for (int i = 0; i < elements.length; i++) {
1010: Element element = elements[i];
1011: DocumentEvent.ElementChange change = e
1012: .getChange(element);
1013: if (change == null)
1014: continue;
1015: Element[] removed = change.getChildrenRemoved();
1016: Element[] added = change.getChildrenAdded();
1017:
1018: if (removed.length == added.length) {
1019: for (int c = 0; c < removed.length; c++) {
1020: AnnotateLine recent = elementAnnotations
1021: .get(removed[c]);
1022: if (recent != null) {
1023: elementAnnotations.remove(removed[c]);
1024: elementAnnotations
1025: .put(added[c], recent);
1026: }
1027: }
1028: } else if (removed.length == 1 && added.length > 0) {
1029: Element key = removed[0];
1030: AnnotateLine recent = elementAnnotations
1031: .get(key);
1032: if (recent != null) {
1033: elementAnnotations.remove(key);
1034: elementAnnotations.put(added[0], recent);
1035: }
1036: }
1037: }
1038: }
1039: }
1040: repaint();
1041: }
1042:
1043: /** Implementation */
1044: public void removeUpdate(DocumentEvent e) {
1045: if (e.getDocument().getLength() == 0) { // external reload
1046: hideBar();
1047: }
1048: repaint();
1049: }
1050:
1051: /** Caret */
1052: public void stateChanged(ChangeEvent e) {
1053: assert e.getSource() == caret;
1054: caretTimer.restart();
1055: }
1056:
1057: /** Timer */
1058: public void actionPerformed(ActionEvent e) {
1059: assert e.getSource() == caretTimer;
1060: onCurrentLine();
1061: }
1062:
1063: /** on JTextPane */
1064: public void componentHidden(ComponentEvent e) {
1065: }
1066:
1067: /** on JTextPane */
1068: public void componentMoved(ComponentEvent e) {
1069: }
1070:
1071: /** on JTextPane */
1072: public void componentResized(ComponentEvent e) {
1073: revalidate();
1074: }
1075:
1076: /** on JTextPane */
1077: public void componentShown(ComponentEvent e) {
1078: }
1079: }
|