Added missing API dependency (for Turbo API).
[nbgit.git] / src / org / netbeans / modules / git / ui / annotate / AnnotationBar.java
blob637c39a448317aba7f50a8336af5037ba8c6f326
1 /*
2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
6 * The contents of this file are subject to the terms of either the GNU
7 * General Public License Version 2 only ("GPL") or the Common
8 * Development and Distribution License("CDDL") (collectively, the
9 * "License"). You may not use this file except in compliance with the
10 * License. You can obtain a copy of the License at
11 * http://www.netbeans.org/cddl-gplv2.html
12 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13 * specific language governing permissions and limitations under the
14 * License. When distributing the software, include this License Header
15 * Notice in each file and include the License file at
16 * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
17 * particular file as subject to the "Classpath" exception as provided
18 * by Sun in the GPL Version 2 section of the License file that
19 * accompanied this code. If applicable, add the following below the
20 * License Header, with the fields enclosed by brackets [] replaced by
21 * your own identifying information:
22 * "Portions Copyrighted [year] [name of copyright owner]"
24 * Contributor(s):
26 * The Original Software is NetBeans. The Initial Developer of the Original
27 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
28 * Microsystems, Inc. All Rights Reserved.
29 * Portions Copyright 2008 Alexander Coles (Ikonoklastik Productions).
31 * If you wish your version of this file to be governed by only the CDDL
32 * or only the GPL Version 2, indicate your decision by adding
33 * "[Contributor] elects to include this software in this distribution
34 * under the [CDDL or GPL Version 2] license." If you do not indicate a
35 * single choice of license, a recipient has the option to distribute
36 * your version of this file under either the CDDL, the GPL Version 2 or
37 * to extend the choice of license to its licensees as provided above.
38 * However, if you add GPL Version 2 code and therefore, elected the GPL
39 * Version 2 license, then the option applies only if the new code is
40 * made subject to such option by the copyright holder.
42 package org.netbeans.modules.git.ui.annotate;
44 import java.awt.Color;
45 import java.awt.Dimension;
46 import java.awt.Graphics;
47 import java.awt.Point;
48 import java.awt.Rectangle;
49 import java.awt.event.ActionEvent;
50 import java.awt.event.ActionListener;
51 import java.awt.event.ComponentEvent;
52 import java.awt.event.ComponentListener;
53 import java.awt.event.MouseAdapter;
54 import java.awt.event.MouseEvent;
55 import java.beans.PropertyChangeEvent;
56 import java.beans.PropertyChangeListener;
57 import java.io.CharConversionException;
58 import java.io.File;
59 import java.io.IOException;
60 import java.io.Reader;
61 import java.text.DateFormat;
62 import java.text.MessageFormat;
63 import java.util.ArrayList;
64 import java.util.Collections;
65 import java.util.HashMap;
66 import java.util.HashSet;
67 import java.util.Iterator;
68 import java.util.LinkedList;
69 import java.util.List;
70 import java.util.Map;
71 import java.util.ResourceBundle;
72 import java.util.logging.Level;
73 import javax.accessibility.Accessible;
74 import javax.swing.JComponent;
75 import javax.swing.JMenuItem;
76 import javax.swing.JPopupMenu;
77 import javax.swing.JSeparator;
78 import javax.swing.Timer;
79 import javax.swing.event.ChangeEvent;
80 import javax.swing.event.ChangeListener;
81 import javax.swing.event.DocumentEvent;
82 import javax.swing.event.DocumentListener;
83 import javax.swing.text.AbstractDocument;
84 import javax.swing.text.BadLocationException;
85 import javax.swing.text.Caret;
86 import javax.swing.text.Document;
87 import javax.swing.text.Element;
88 import javax.swing.text.JTextComponent;
89 import javax.swing.text.Position;
90 import javax.swing.text.StyledDocument;
91 import javax.swing.text.View;
92 import org.netbeans.api.diff.Difference;
93 import org.netbeans.api.editor.fold.FoldHierarchy;
94 import org.netbeans.editor.BaseDocument;
95 import org.netbeans.editor.BaseTextUI;
96 import org.netbeans.editor.EditorUI;
97 import org.netbeans.editor.StatusBar;
98 import org.netbeans.editor.Utilities;
99 import org.netbeans.modules.git.Git;
100 import org.netbeans.modules.git.GitProgressSupport;
101 import org.netbeans.modules.git.ui.diff.DiffAction;
102 import org.netbeans.modules.git.ui.update.RevertModifications;
103 import org.netbeans.modules.git.ui.update.RevertModificationsAction;
104 import org.netbeans.modules.git.util.GitLogMessage;
105 import org.netbeans.modules.versioning.Utils;
106 import org.netbeans.spi.diff.DiffProvider;
107 import org.openide.filesystems.FileObject;
108 import org.openide.filesystems.FileUtil;
109 import org.openide.loaders.DataObject;
110 import org.openide.text.NbDocument;
111 import org.openide.util.Lookup;
112 import org.openide.util.NbBundle;
113 import org.openide.util.RequestProcessor;
114 import org.openide.xml.XMLUtil;
117 * Represents annotation sidebar componnet in editor. It's
118 * created by {@link AnnotationBarManager}.
120 * <p>It reponds to following external signals:
121 * <ul>
122 * <li> {@link #annotate} message
123 * </ul>
125 * @author Petr Kuzel
127 final class AnnotationBar extends JComponent implements Accessible, PropertyChangeListener, DocumentListener, ChangeListener, ActionListener, Runnable, ComponentListener {
130 * Target text component for which the annotation bar is aiming.
132 private final JTextComponent textComponent;
135 * User interface related to the target text component.
137 private final EditorUI editorUI;
140 * Fold hierarchy of the text component user interface.
142 private final FoldHierarchy foldHierarchy;
144 /**
145 * Document related to the target text component.
147 private final BaseDocument doc;
150 * Caret of the target text component.
152 private final Caret caret;
155 * Caret batch timer launched on receiving
156 * annotation data structures (AnnotateLine).
158 private Timer caretTimer;
161 * Controls annotation bar visibility.
163 private boolean annotated;
166 * Maps document {@link javax.swing.text.Element}s (representing lines) to
167 * {@link AnnotateLine}. <code>null</code> means that
168 * no data are available, yet. So alternative
169 * {@link #elementAnnotationsSubstitute} text shoudl be used.
171 * @thread it is accesed from multiple threads all mutations
172 * and iterations must be under elementAnnotations lock,
174 private Map<Element, AnnotateLine> elementAnnotations;
177 * Represents text that should be displayed in
178 * visible bar with yet <code>null</code> elementAnnotations.
180 private String elementAnnotationsSubstitute;
182 private Color backgroundColor = Color.WHITE;
183 private Color foregroundColor = Color.BLACK;
184 private Color selectedColor = Color.BLUE;
187 * Most recent status message.
189 private String recentStatusMessage;
192 * Revision associated with caret line.
194 private String recentRevision;
197 * File for revision associated with caret line.
199 private File recentFile;
202 * Request processor to create threads that may be cancelled.
204 RequestProcessor requestProcessor = null;
207 * Latest annotation comment fetching task launched.
209 private RequestProcessor.Task latestAnnotationTask = null;
212 * Holds false if Rollback Changes action is NOT valid for current ievision, true otherwise.
214 private boolean recentRevisionCanBeRolledBack;
217 * The log messaages for the file stored in the AnnotationBar;
219 private GitLogMessage [] logs;
222 * Creates new instance initializing final fields.
224 public AnnotationBar(JTextComponent target) {
225 this.textComponent = target;
226 this.editorUI = Utilities.getEditorUI(target);
227 this.foldHierarchy = FoldHierarchy.get(editorUI.getComponent());
228 this.doc = editorUI.getDocument();
229 this.caret = textComponent.getCaret();
232 // public contract ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
235 * Makes the bar visible and sensitive to
236 * LogOutoutListener events that should deliver
237 * actual content to be displayed.
239 public void annotate() {
240 annotated = true;
241 elementAnnotations = null;
243 doc.addDocumentListener(this);
244 textComponent.addComponentListener(this);
245 editorUI.addPropertyChangeListener(this);
247 revalidate(); // resize the component
250 public void setAnnotationMessage(String message) {
251 elementAnnotationsSubstitute = message;
252 revalidate();
256 * Result computed show it...
257 * Takes AnnotateLines and shows them.
259 public void annotationLines(File file, List<AnnotateLine> annotateLines) {
260 List<AnnotateLine> lines = new LinkedList<AnnotateLine>(annotateLines);
261 int lineCount = lines.size();
262 /** 0 based line numbers => 1 based line numbers*/
263 int ann2editorPermutation[] = new int[lineCount];
264 for (int i = 0; i< lineCount; i++) {
265 ann2editorPermutation[i] = i+1;
268 DiffProvider diff = (DiffProvider) Lookup.getDefault().lookup(DiffProvider.class);
269 if (diff != null) {
270 Reader r = new LinesReader(lines);
271 Reader docReader = Utils.getDocumentReader(doc);
272 try {
274 Difference[] differences = diff.computeDiff(r, docReader);
276 // customize annotation line numbers to match different reality
277 // compule line permutation
279 for (int i = 0; i < differences.length; i++) {
280 Difference d = differences[i];
281 if (d.getType() == Difference.ADD) continue;
283 int editorStart;
284 int firstShift = d.getFirstEnd() - d.getFirstStart() +1;
285 if (d.getType() == Difference.CHANGE) {
286 int firstLen = d.getFirstEnd() - d.getFirstStart();
287 int secondLen = d.getSecondEnd() - d.getSecondStart();
288 if (secondLen >= firstLen) continue; // ADD or pure CHANGE
289 editorStart = d.getSecondStart();
290 firstShift = firstLen - secondLen;
291 } else { // DELETE
292 editorStart = d.getSecondStart() + 1;
295 for (int c = editorStart + firstShift -1; c<lineCount; c++) {
296 ann2editorPermutation[c] -= firstShift;
300 for (int i = differences.length -1; i >= 0; i--) {
301 Difference d = differences[i];
302 if (d.getType() == Difference.DELETE) continue;
304 int firstStart;
305 int firstShift = d.getSecondEnd() - d.getSecondStart() +1;
306 if (d.getType() == Difference.CHANGE) {
307 int firstLen = d.getFirstEnd() - d.getFirstStart();
308 int secondLen = d.getSecondEnd() - d.getSecondStart();
309 if (secondLen <= firstLen) continue; // REMOVE or pure CHANGE
310 firstShift = secondLen - firstLen;
311 firstStart = d.getFirstStart();
312 } else {
313 firstStart = d.getFirstStart() + 1;
316 for (int k = firstStart-1; k<lineCount; k++) {
317 ann2editorPermutation[k] += firstShift;
321 } catch (IOException e) {
322 Git.LOG.log(Level.INFO, "Cannot compute local diff required for annotations, ignoring..."); // NOI18N
326 try {
327 doc.atomicLock();
328 StyledDocument sd = (StyledDocument) doc;
329 Iterator<AnnotateLine> it = lines.iterator();
330 elementAnnotations = Collections.synchronizedMap(new HashMap<Element, AnnotateLine>(lines.size()));
331 while (it.hasNext()) {
332 AnnotateLine line = it.next();
333 int lineNum = ann2editorPermutation[line.getLineNum() -1];
334 try {
335 int lineOffset = NbDocument.findLineOffset(sd, lineNum -1);
336 Element element = sd.getParagraphElement(lineOffset);
337 elementAnnotations.put(element, line);
338 } catch (IndexOutOfBoundsException ex) {
339 // TODO how could I get line behind document end?
340 // furtunately user does not spot it
341 Git.LOG.log(Level.INFO, null, ex);
344 } finally {
345 doc.atomicUnlock();
348 // lazy listener registration
349 caret.addChangeListener(this);
350 this.caretTimer = new Timer(500, this);
351 caretTimer.setRepeats(false);
353 onCurrentLine();
354 revalidate();
355 repaint();
358 // implementation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
361 * Gets a the file related to the document
363 * @return the file related to the document, <code>null</code> if none
364 * exists.
366 private File getCurrentFile() {
367 File result = null;
369 DataObject dobj = (DataObject)doc.getProperty(Document.StreamDescriptionProperty);
370 if (dobj != null) {
371 FileObject fo = dobj.getPrimaryFile();
372 result = FileUtil.toFile(fo);
375 return result;
379 * Registers "close" popup menu, tooltip manager // NOI18N
380 * and repaint on documet change manager.
382 public void addNotify() {
383 super.addNotify();
386 this.addMouseListener(new MouseAdapter() {
387 public void mousePressed(MouseEvent e) {
388 maybeShowPopup(e);
391 public void mouseReleased(MouseEvent e) {
392 maybeShowPopup(e);
395 private void maybeShowPopup(MouseEvent e) {
396 if (e.isPopupTrigger()) {
397 e.consume();
398 createPopup().show(e.getComponent(),
399 e.getX(), e.getY());
404 // register with tooltip manager
405 setToolTipText(""); // NOI18N
409 private JPopupMenu createPopup() {
410 final ResourceBundle loc = NbBundle.getBundle(AnnotationBar.class);
411 final JPopupMenu popupMenu = new JPopupMenu();
413 final File file = getCurrentFile();
415 final JMenuItem diffMenu = new JMenuItem(loc.getString("CTL_MenuItem_DiffToRevision")); // NOI18N
416 diffMenu.addActionListener(new ActionListener() {
417 public void actionPerformed(ActionEvent e) {
418 if (recentRevision != null) {
419 if (getPreviousRevision(recentRevision) != null) {
420 DiffAction.diff(recentFile, getPreviousRevision(recentRevision), recentRevision);
425 popupMenu.add(diffMenu);
427 JMenuItem rollbackMenu = new JMenuItem(loc.getString("CTL_MenuItem_Revert")); // NOI18N
428 rollbackMenu.addActionListener(new ActionListener() {
429 public void actionPerformed(ActionEvent e) {
430 revert(file, recentRevision);
433 popupMenu.add(rollbackMenu);
434 rollbackMenu.setEnabled(recentRevisionCanBeRolledBack);
436 JMenuItem menu;
437 menu = new JMenuItem(loc.getString("CTL_MenuItem_CloseAnnotations")); // NOI18N
438 menu.addActionListener(new ActionListener() {
439 public void actionPerformed(ActionEvent e) {
440 hideBar();
443 popupMenu.add(new JSeparator());
444 popupMenu.add(menu);
446 diffMenu.setVisible(false);
447 rollbackMenu.setVisible(false);
448 if (recentRevision != null) {
449 if (getPreviousRevision(recentRevision) != null) {
450 String format = loc.getString("CTL_MenuItem_DiffToRevision"); // NOI18N
451 diffMenu.setText(MessageFormat.format(format, new Object [] { recentRevision, getPreviousRevision(recentRevision) }));
452 diffMenu.setVisible(true);
453 rollbackMenu.setVisible(true);
457 return popupMenu;
460 private void revert(final File file, String revision) {
461 final File root = Git.getInstance().getTopmostManagedParent(file);
462 File[] files = new File [1];
463 files[0] = file;
464 final RevertModifications revertModifications = new RevertModifications(root, files, revision);
465 if(!revertModifications.showDialog()) {
466 return;
468 final String revStr = revertModifications.getSelectionRevision();
469 final boolean doBackup = revertModifications.isBackupRequested();
471 RequestProcessor rp = Git.getInstance().getRequestProcessor(root);
472 GitProgressSupport support = new GitProgressSupport() {
473 public void perform() {
474 RevertModificationsAction.performRevert(root, revStr, file, doBackup, this.getLogger());
477 support.start(rp, root.getAbsolutePath(), NbBundle.getMessage(AnnotationBar.class, "MSG_Revert_Progress")); // NOI18N
480 private String getPreviousRevision(String revision) {
481 for(int i = 0; i < logs.length; i++) {
482 if (logs[i].getRevision() == Long.parseLong(revision)) {
483 if (i < logs.length - 1)
484 return Long.toString(logs[i+1].getRevision());
487 return null;
491 * Hides the annotation bar from user.
493 void hideBar() {
494 annotated = false;
495 revalidate();
496 release();
500 * Gets a request processor which is able to cancel tasks.
502 private RequestProcessor getRequestProcessor() {
503 if (requestProcessor == null) {
504 requestProcessor = new RequestProcessor("AnnotationBarRP", 1, true); // NOI18N
507 return requestProcessor;
511 * Shows commit message in status bar and or revision change repaints side
512 * bar (to highlight same revision). This process is started in a
513 * seperate thread.
515 private void onCurrentLine() {
516 if (latestAnnotationTask != null) {
517 latestAnnotationTask.cancel();
520 latestAnnotationTask = getRequestProcessor().post(this);
523 // latestAnnotationTask business logic
524 public void run() {
525 // get resource bundle
526 ResourceBundle loc = NbBundle.getBundle(AnnotationBar.class);
527 // give status bar "wait" indication // NOI18N
528 StatusBar statusBar = editorUI.getStatusBar();
529 recentStatusMessage = loc.getString("CTL_StatusBar_WaitFetchAnnotation"); // NOI18N
530 statusBar.setText(StatusBar.CELL_MAIN, recentStatusMessage);
532 recentRevisionCanBeRolledBack = false;
533 // determine current line
534 int line = -1;
535 int offset = caret.getDot();
536 try {
537 line = Utilities.getLineOffset(doc, offset);
538 } catch (BadLocationException ex) {
539 Git.LOG.log(Level.SEVERE, "Can not get line for caret at offset ", offset); // NOI18N
540 clearRecentFeedback();
541 return;
544 // handle locally modified lines
545 AnnotateLine al = getAnnotateLine(line);
546 if (al == null) {
547 AnnotationMarkProvider amp = AnnotationMarkInstaller.getMarkProvider(textComponent);
548 if (amp != null) {
549 amp.setMarks(Collections.<AnnotationMark>emptyList());
551 clearRecentFeedback();
552 if (recentRevision != null) {
553 recentRevision = null;
554 repaint();
556 return;
559 // handle unchanged lines
560 String revision = al.getRevision();
561 if (revision.equals(recentRevision) == false) {
562 recentRevision = revision;
563 File file = Git.getInstance().getTopmostManagedParent(getCurrentFile());
564 recentFile = new File(file, al.getFileName());
565 recentRevisionCanBeRolledBack = al.canBeRolledBack();
566 repaint();
568 AnnotationMarkProvider amp = AnnotationMarkInstaller.getMarkProvider(textComponent);
569 if (amp != null) {
571 List<AnnotationMark> marks = new ArrayList<AnnotationMark>(elementAnnotations.size());
572 // I cannot affort to lock elementAnnotations for long time
573 // it's accessed from editor thread too
574 Iterator<Map.Entry<Element, AnnotateLine>> it2;
575 synchronized(elementAnnotations) {
576 it2 = new HashSet<Map.Entry<Element, AnnotateLine>>(elementAnnotations.entrySet()).iterator();
578 while (it2.hasNext()) {
579 Map.Entry<Element, AnnotateLine> next = it2.next();
580 AnnotateLine annotateLine = next.getValue();
581 if (revision.equals(annotateLine.getRevision())) {
582 Element element = next.getKey();
583 if (elementAnnotations.containsKey(element) == false) {
584 continue;
586 int elementOffset = element.getStartOffset();
587 int lineNumber = NbDocument.findLineNumber((StyledDocument)doc, elementOffset);
588 AnnotationMark mark = new AnnotationMark(lineNumber, revision);
589 marks.add(mark);
592 if (Thread.interrupted()) {
593 clearRecentFeedback();
594 return;
597 amp.setMarks(marks);
601 if (al.getCommitMessage() != null) {
602 recentStatusMessage = al.getCommitMessage();
603 statusBar.setText(StatusBar.CELL_MAIN, al.getAuthor() + ": " + recentStatusMessage); // NOI18N
604 } else {
605 clearRecentFeedback();
610 * Clears the status bar if it contains the latest status message
611 * displayed by this annotation bar.
613 private void clearRecentFeedback() {
614 StatusBar statusBar = editorUI.getStatusBar();
615 if (statusBar.getText(StatusBar.CELL_MAIN) == recentStatusMessage) {
616 statusBar.setText(StatusBar.CELL_MAIN, ""); // NOI18N
621 * Components created by SibeBarFactory are positioned
622 * using a Layout manager that determines componnet size
623 * by retireving preferred size.
625 * <p>Once componnet needs resizing it simply calls
626 * {@link #revalidate} that triggers new layouting
627 * that consults prefered size.
629 public Dimension getPreferredSize() {
630 Dimension dim = textComponent.getSize();
631 int width = annotated ? getBarWidth() : 0;
632 dim.width = width;
633 dim.height *=2; // XXX
634 return dim;
638 * Gets the maximum size of this component.
640 * @return the maximum size of this component
642 public Dimension getMaximumSize() {
643 return getPreferredSize();
647 * Gets the preferred width of this component.
649 * @return the preferred width of this component
651 private int getBarWidth() {
652 String longestString = ""; // NOI18N
653 if (elementAnnotations == null) {
654 longestString = elementAnnotationsSubstitute;
655 } else {
656 synchronized(elementAnnotations) {
657 Iterator<AnnotateLine> it = elementAnnotations.values().iterator();
658 while (it.hasNext()) {
659 AnnotateLine line = it.next();
660 String displayName = getDisplayName(line); // NOI18N
661 if (displayName.length() > longestString.length()) {
662 longestString = displayName;
667 char[] data = longestString.toCharArray();
668 int w = getGraphics().getFontMetrics().charsWidth(data, 0, data.length);
669 return w + 4;
672 private String getDisplayName(AnnotateLine line) {
673 return line.getRevision() + " " + line.getAuthor(); // NOI18N
677 * Pair method to {@link #annotate}. It releases
678 * all resources.
680 private void release() {
681 editorUI.removePropertyChangeListener(this);
682 textComponent.removeComponentListener(this);
683 doc.removeDocumentListener(this);
684 caret.removeChangeListener(this);
685 if (caretTimer != null) {
686 caretTimer.removeActionListener(this);
688 elementAnnotations = null;
689 // cancel running annotation task if active
690 if(latestAnnotationTask != null) {
691 latestAnnotationTask.cancel();
693 AnnotationMarkProvider amp = AnnotationMarkInstaller.getMarkProvider(textComponent);
694 if (amp != null) {
695 amp.setMarks(Collections.<AnnotationMark>emptyList());
698 clearRecentFeedback();
702 * Paints one view that corresponds to a line (or
703 * multiple lines if folding takes effect).
705 private void paintView(View view, Graphics g, int yBase) {
706 JTextComponent component = editorUI.getComponent();
707 if (component == null) return;
708 BaseTextUI textUI = (BaseTextUI)component.getUI();
710 Element rootElem = textUI.getRootView(component).getElement();
711 int line = rootElem.getElementIndex(view.getStartOffset());
713 String annotation = ""; // NOI18N
714 AnnotateLine al = null;
715 if (elementAnnotations != null) {
716 al = getAnnotateLine(line);
717 if (al != null) {
718 annotation = getDisplayName(al); // NOI18N
720 } else {
721 annotation = elementAnnotationsSubstitute;
724 if (al != null && al.getRevision().equals(recentRevision)) {
725 g.setColor(selectedColor());
726 } else {
727 g.setColor(foregroundColor());
729 g.drawString(annotation, 2, yBase + editorUI.getLineAscent());
733 * Presents commit message as tooltips.
735 public String getToolTipText (MouseEvent e) {
736 if (editorUI == null)
737 return null;
738 int line = getLineFromMouseEvent(e);
740 StringBuffer annotation = new StringBuffer();
741 if (elementAnnotations != null) {
742 AnnotateLine al = getAnnotateLine(line);
744 if (al != null) {
745 String escapedAuthor = NbBundle.getMessage(AnnotationBar.class, "TT_Annotation"); // NOI18N
746 try {
747 escapedAuthor = XMLUtil.toElementContent(al.getAuthor());
748 } catch (CharConversionException e1) {
749 Git.LOG.log(Level.INFO, "HG.AB: can not HTML escape: ", al.getAuthor()); // NOI18N
752 // always return unique string to avoid tooltip sharing on mouse move over same revisions -->
753 annotation.append("<html><!-- line=" + line++ + " -->" + al.getRevision() + " - <b>" + escapedAuthor + "</b>"); // NOI18N
754 if (al.getDate() != null) {
755 annotation.append(" " + DateFormat.getDateInstance().format(al.getDate())); // NOI18N
757 if (al.getCommitMessage() != null) {
758 String escaped = null;
759 try {
760 escaped = XMLUtil.toElementContent(al.getCommitMessage());
761 } catch (CharConversionException e1) {
762 Git.LOG.log(Level.INFO, "HG.AB: can not HTML escape: ", al.getCommitMessage()); // NOI18N
764 if (escaped != null) {
765 String lined = escaped.replaceAll(System.getProperty("line.separator"), "<br>"); // NOI18N
766 annotation.append("<p>" + lined); // NOI18N
770 } else {
771 annotation.append(elementAnnotationsSubstitute);
774 return annotation.toString();
778 * Locates AnnotateLine associated with given line. The
779 * line is translated to Element that is used as map lookup key.
780 * The map is initially filled up with Elements sampled on
781 * annotate() method.
783 * <p>Key trick is that Element's identity is maintained
784 * until line removal (and is restored on undo).
786 * @param line
787 * @return found AnnotateLine or <code>null</code>
789 private AnnotateLine getAnnotateLine(int line) {
790 StyledDocument sd = (StyledDocument) doc;
791 int lineOffset = NbDocument.findLineOffset(sd, line);
792 Element element = sd.getParagraphElement(lineOffset);
793 AnnotateLine al = elementAnnotations.get(element);
795 if (al != null) {
796 int startOffset = element.getStartOffset();
797 int endOffset = element.getEndOffset();
798 try {
799 int len = endOffset - startOffset;
800 String text = doc.getText(startOffset, len -1);
801 String content = al.getContent();
802 if (text.equals(content)) {
803 return al;
805 } catch (BadLocationException e) {
806 Git.LOG.log(Level.INFO, "HG.AB: can not locate line annotation."); // NOI18N
810 return null;
814 * GlyphGutter copy pasted bolerplate method.
815 * It invokes {@link #paintView} that contains
816 * actual business logic.
818 public void paintComponent(Graphics g) {
819 super.paintComponent(g);
821 Rectangle clip = g.getClipBounds();
823 JTextComponent component = editorUI.getComponent();
824 if (component == null) return;
826 BaseTextUI textUI = (BaseTextUI)component.getUI();
827 View rootView = Utilities.getDocumentView(component);
828 if (rootView == null) return;
830 g.setColor(backgroundColor());
831 g.fillRect(clip.x, clip.y, clip.width, clip.height);
833 AbstractDocument doc = (AbstractDocument)component.getDocument();
834 doc.readLock();
835 try{
836 foldHierarchy.lock();
837 try{
838 int startPos = textUI.getPosFromY(clip.y);
839 int startViewIndex = rootView.getViewIndex(startPos, Position.Bias.Forward);
840 int rootViewCount = rootView.getViewCount();
842 if (startViewIndex >= 0 && startViewIndex < rootViewCount) {
843 // find the nearest visible line with an annotation
844 Rectangle rec = textUI.modelToView(component, rootView.getView(startViewIndex).getStartOffset());
845 int y = (rec == null) ? 0 : rec.y;
847 int clipEndY = clip.y + clip.height;
848 for (int i = startViewIndex; i < rootViewCount; i++){
849 View view = rootView.getView(i);
850 paintView(view, g, y);
851 y += editorUI.getLineHeight();
852 if (y >= clipEndY) {
853 break;
858 } finally {
859 foldHierarchy.unlock();
861 } catch (BadLocationException ble){
862 Git.LOG.log(Level.WARNING, null, ble);
863 } finally {
864 doc.readUnlock();
868 private Color backgroundColor() {
869 if (textComponent != null) {
870 return textComponent.getBackground();
872 return backgroundColor;
875 private Color foregroundColor() {
876 if (textComponent != null) {
877 return textComponent.getForeground();
879 return foregroundColor;
882 private Color selectedColor() {
883 if (backgroundColor == backgroundColor()) {
884 return selectedColor;
886 if (textComponent != null) {
887 return textComponent.getForeground();
889 return selectedColor;
894 /** GlyphGutter copy pasted utility method. */
895 private int getLineFromMouseEvent(MouseEvent e){
896 int line = -1;
897 if (editorUI != null) {
898 try{
899 JTextComponent component = editorUI.getComponent();
900 BaseTextUI textUI = (BaseTextUI)component.getUI();
901 int clickOffset = textUI.viewToModel(component, new Point(0, e.getY()));
902 line = Utilities.getLineOffset(doc, clickOffset);
903 }catch (BadLocationException ble){
906 return line;
909 /** Implementation */
910 public void propertyChange(PropertyChangeEvent evt) {
911 if (evt == null) return;
912 String id = evt.getPropertyName();
913 if (EditorUI.COMPONENT_PROPERTY.equals(id)) { // NOI18N
914 if (evt.getNewValue() == null){
915 // component deinstalled, lets uninstall all isteners
916 release();
922 /** Implementation */
923 public void changedUpdate(DocumentEvent e) {
926 /** Implementation */
927 public void insertUpdate(DocumentEvent e) {
928 // handle new lines, Enter hit at end of line changes
929 // the line element instance
930 // XXX Actually NB document implementation triggers this method two times
931 // - first time with one removed and two added lines
932 // - second time with two removed and two added lines
933 if (elementAnnotations != null) {
934 Element[] elements = e.getDocument().getRootElements();
935 synchronized(elementAnnotations) { // atomic change
936 for (int i = 0; i < elements.length; i++) {
937 Element element = elements[i];
938 DocumentEvent.ElementChange change = e.getChange(element);
939 if (change == null) continue;
940 Element[] removed = change.getChildrenRemoved();
941 Element[] added = change.getChildrenAdded();
943 if (removed.length == added.length) {
944 for (int c = 0; c<removed.length; c++) {
945 AnnotateLine recent = elementAnnotations.get(removed[c]);
946 if (recent != null) {
947 elementAnnotations.remove(removed[c]);
948 elementAnnotations.put(added[c], recent);
951 } else if (removed.length == 1 && added.length > 0) {
952 Element key = removed[0];
953 AnnotateLine recent = elementAnnotations.get(key);
954 if (recent != null) {
955 elementAnnotations.remove(key);
956 elementAnnotations.put(added[0], recent);
962 repaint();
965 /** Implementation */
966 public void removeUpdate(DocumentEvent e) {
967 if (e.getDocument().getLength() == 0) { // external reload
968 hideBar();
970 repaint();
973 /** Caret */
974 public void stateChanged(ChangeEvent e) {
975 assert e.getSource() == caret;
976 caretTimer.restart();
979 /** Timer */
980 public void actionPerformed(ActionEvent e) {
981 assert e.getSource() == caretTimer;
982 onCurrentLine();
985 /** on JTextPane */
986 public void componentHidden(ComponentEvent e) {
989 /** on JTextPane */
990 public void componentMoved(ComponentEvent e) {
993 /** on JTextPane */
994 public void componentResized(ComponentEvent e) {
995 revalidate();
998 /** on JTextPane */
999 public void componentShown(ComponentEvent e) {
1002 public void setLogs(GitLogMessage [] logs) {
1003 this.logs = logs;