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