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]"
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
;
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
;
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:
122 * <li> {@link #annotate} message
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
;
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() {
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
;
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);
270 Reader r
= new LinesReader(lines
);
271 Reader docReader
= Utils
.getDocumentReader(doc
);
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;
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
;
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;
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();
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
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];
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
);
348 // lazy listener registration
349 caret
.addChangeListener(this);
350 this.caretTimer
= new Timer(500, this);
351 caretTimer
.setRepeats(false);
358 // implementation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
361 * Gets a the file related to the document
363 * @return the file related to the document, <code>null</code> if none
366 private File
getCurrentFile() {
369 DataObject dobj
= (DataObject
)doc
.getProperty(Document
.StreamDescriptionProperty
);
371 FileObject fo
= dobj
.getPrimaryFile();
372 result
= FileUtil
.toFile(fo
);
379 * Registers "close" popup menu, tooltip manager // NOI18N
380 * and repaint on documet change manager.
382 public void addNotify() {
386 this.addMouseListener(new MouseAdapter() {
387 public void mousePressed(MouseEvent e
) {
391 public void mouseReleased(MouseEvent e
) {
395 private void maybeShowPopup(MouseEvent e
) {
396 if (e
.isPopupTrigger()) {
398 createPopup().show(e
.getComponent(),
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
);
437 menu
= new JMenuItem(loc
.getString("CTL_MenuItem_CloseAnnotations")); // NOI18N
438 menu
.addActionListener(new ActionListener() {
439 public void actionPerformed(ActionEvent e
) {
443 popupMenu
.add(new JSeparator());
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);
460 private void revert(final File file
, String revision
) {
461 final File root
= Git
.getInstance().getTopmostManagedParent(file
);
462 File
[] files
= new File
[1];
464 final RevertModifications revertModifications
= new RevertModifications(root
, files
, revision
);
465 if(!revertModifications
.showDialog()) {
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());
491 * Hides the annotation bar from user.
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
515 private void onCurrentLine() {
516 if (latestAnnotationTask
!= null) {
517 latestAnnotationTask
.cancel();
520 latestAnnotationTask
= getRequestProcessor().post(this);
523 // latestAnnotationTask business logic
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
535 int offset
= caret
.getDot();
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();
544 // handle locally modified lines
545 AnnotateLine al
= getAnnotateLine(line
);
547 AnnotationMarkProvider amp
= AnnotationMarkInstaller
.getMarkProvider(textComponent
);
549 amp
.setMarks(Collections
.<AnnotationMark
>emptyList());
551 clearRecentFeedback();
552 if (recentRevision
!= null) {
553 recentRevision
= null;
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();
568 AnnotationMarkProvider amp
= AnnotationMarkInstaller
.getMarkProvider(textComponent
);
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) {
586 int elementOffset
= element
.getStartOffset();
587 int lineNumber
= NbDocument
.findLineNumber((StyledDocument
)doc
, elementOffset
);
588 AnnotationMark mark
= new AnnotationMark(lineNumber
, revision
);
592 if (Thread
.interrupted()) {
593 clearRecentFeedback();
601 if (al
.getCommitMessage() != null) {
602 recentStatusMessage
= al
.getCommitMessage();
603 statusBar
.setText(StatusBar
.CELL_MAIN
, al
.getAuthor() + ": " + recentStatusMessage
); // NOI18N
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;
633 dim
.height
*=2; // XXX
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
;
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
);
672 private String
getDisplayName(AnnotateLine line
) {
673 return line
.getRevision() + " " + line
.getAuthor(); // NOI18N
677 * Pair method to {@link #annotate}. It releases
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
);
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
);
718 annotation
= getDisplayName(al
); // NOI18N
721 annotation
= elementAnnotationsSubstitute
;
724 if (al
!= null && al
.getRevision().equals(recentRevision
)) {
725 g
.setColor(selectedColor());
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)
738 int line
= getLineFromMouseEvent(e
);
740 StringBuffer annotation
= new StringBuffer();
741 if (elementAnnotations
!= null) {
742 AnnotateLine al
= getAnnotateLine(line
);
745 String escapedAuthor
= NbBundle
.getMessage(AnnotationBar
.class, "TT_Annotation"); // NOI18N
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;
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
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
783 * <p>Key trick is that Element's identity is maintained
784 * until line removal (and is restored on undo).
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
);
796 int startOffset
= element
.getStartOffset();
797 int endOffset
= element
.getEndOffset();
799 int len
= endOffset
- startOffset
;
800 String text
= doc
.getText(startOffset
, len
-1);
801 String content
= al
.getContent();
802 if (text
.equals(content
)) {
805 } catch (BadLocationException e
) {
806 Git
.LOG
.log(Level
.INFO
, "HG.AB: can not locate line annotation."); // NOI18N
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();
836 foldHierarchy
.lock();
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();
859 foldHierarchy
.unlock();
861 } catch (BadLocationException ble
){
862 Git
.LOG
.log(Level
.WARNING
, null, ble
);
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
){
897 if (editorUI
!= null) {
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
){
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
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
);
965 /** Implementation */
966 public void removeUpdate(DocumentEvent e
) {
967 if (e
.getDocument().getLength() == 0) { // external reload
974 public void stateChanged(ChangeEvent e
) {
975 assert e
.getSource() == caret
;
976 caretTimer
.restart();
980 public void actionPerformed(ActionEvent e
) {
981 assert e
.getSource() == caretTimer
;
986 public void componentHidden(ComponentEvent e
) {
990 public void componentMoved(ComponentEvent e
) {
994 public void componentResized(ComponentEvent e
) {
999 public void componentShown(ComponentEvent e
) {
1002 public void setLogs(GitLogMessage
[] logs
) {