All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.modules.versioning.annotate.AnnotationBar Maven / Gradle / Ivy

There is a newer version: RELEASE240
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.netbeans.modules.versioning.annotate;

import org.openide.util.RequestProcessor;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.ErrorManager;
import org.openide.xml.XMLUtil;
import org.openide.text.NbDocument;
import org.netbeans.modules.versioning.util.Utils;
import org.netbeans.api.editor.fold.FoldHierarchy;
import org.netbeans.api.diff.Difference;
import org.netbeans.editor.*;
import org.netbeans.editor.Utilities;
import org.netbeans.spi.diff.DiffProvider;

import javax.swing.*;
import javax.swing.Timer;
import javax.swing.text.*;
import javax.swing.event.DocumentListener;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.ChangeEvent;
import javax.accessibility.Accessible;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import java.awt.event.*;
import java.awt.*;
import java.util.*;
import java.util.List;
import java.io.File;
import java.io.Reader;
import java.io.IOException;
import java.io.CharConversionException;
import java.text.DateFormat;
import org.netbeans.api.editor.mimelookup.MimeLookup;
import org.netbeans.api.editor.settings.EditorStyleConstants;
import org.netbeans.api.editor.settings.FontColorNames;
import org.netbeans.api.editor.settings.FontColorSettings;
import org.openide.util.Mutex;

/**
 * Represents annotation sidebar componnet in editor. It's
 * created by {@link AnnotationBarManager}.
 *
 * 

It reponds to following external signals: *

    *
  • {@link #annotate} message *
* * @author Petr Kuzel */ final class AnnotationBar extends JComponent implements Accessible, PropertyChangeListener, DocumentListener, ChangeListener, ActionListener, Runnable, ComponentListener { /** * Target text component for which the annotation bar is aiming. */ private final JTextComponent textComponent; /** * User interface related to the target text component. */ private final EditorUI editorUI; /** * Fold hierarchy of the text component user interface. */ private final FoldHierarchy foldHierarchy; /** * Document related to the target text component. */ private final BaseDocument doc; /** * Caret of the target text component. */ private Caret caret; /** * Caret batch timer launched on receiving * annotation data structures (AnnotateLine). */ private Timer caretTimer; /** * Controls annotation bar visibility. */ private boolean annotated; /** * Maps document {@link javax.swing.text.Element}s (representing lines) to * {@link VcsAnnotation}. null means that * no data are available, yet. So alternative * {@link #elementAnnotationsSubstitute} text shoudl be used. * * @thread it is accesed from multiple threads all mutations * and iterations must be under elementAnnotations lock, */ private Map elementAnnotations; /** * Represents text that should be displayed in * visible bar with yet null elementAnnotations. */ private String elementAnnotationsSubstitute; private Color backgroundColor = Color.WHITE; private Color foregroundColor = Color.BLACK; private Color selectedColor = Color.BLUE; /** * Most recent status message. */ private String recentStatusMessage; /** * Revision associated with caret line. */ private String recentRevision; /** * Request processor to create threads that may be cancelled. */ RequestProcessor requestProcessor = null; /** * Latest annotation comment fetching task launched. */ private RequestProcessor.Task latestAnnotationTask = null; /** * Rendering hints for annotations sidebar inherited from editor settings. */ private final Map renderingHints; private VcsAnnotations vcsAnnotations; /** * Creates new instance initializing final fields. */ public AnnotationBar(JTextComponent target) { this.textComponent = target; this.editorUI = Utilities.getEditorUI(target); this.foldHierarchy = FoldHierarchy.get(editorUI.getComponent()); this.doc = editorUI.getDocument(); if (textComponent instanceof JEditorPane) { String mimeType = org.netbeans.lib.editor.util.swing.DocumentUtilities.getMimeType(textComponent); FontColorSettings fcs = MimeLookup.getLookup(mimeType).lookup(FontColorSettings.class); renderingHints = (Map) fcs.getFontColors(FontColorNames.DEFAULT_COLORING).getAttribute(EditorStyleConstants.RenderingHints); } else { renderingHints = null; } setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); elementAnnotationsSubstitute = ""; //NOI18N } // public contract ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** * Makes the bar visible and sensitive to * LogOutoutListener events that should deliver * actual content to be displayed. */ public void annotate() { annotated = true; elementAnnotations = null; doc.addDocumentListener(this); textComponent.addComponentListener(this); editorUI.addPropertyChangeListener(this); revalidate(); // resize the component } public void setAnnotationMessage(String message) { elementAnnotationsSubstitute = message; revalidate(); } /** * Result computed show it... * Takes AnnotateLines and shows them. */ public void annotationLines(File file, VcsAnnotations vcsannotations) { this.vcsAnnotations = vcsannotations; VcsAnnotation [] annotateLines = vcsannotations.getAnnotations(); List lines = new LinkedList(Arrays.asList(annotateLines)); int lineCount = lines.size(); /** 0 based line numbers => 1 based line numbers*/ int ann2editorPermutation[] = new int[lineCount]; for (int i = 0; i< lineCount; i++) { ann2editorPermutation[i] = i+1; } DiffProvider diff = (DiffProvider) Lookup.getDefault().lookup(DiffProvider.class); if (diff != null) { Reader r = new LinesReader(lines); Reader docReader = Utils.getDocumentReader(doc); try { Difference[] differences = diff.computeDiff(r, docReader); // customize annotation line numbers to match different reality // compule line permutation for (int i = 0; i < differences.length; i++) { Difference d = differences[i]; if (d.getType() == Difference.ADD) continue; int editorStart; int firstShift = d.getFirstEnd() - d.getFirstStart() +1; if (d.getType() == Difference.CHANGE) { int firstLen = d.getFirstEnd() - d.getFirstStart(); int secondLen = d.getSecondEnd() - d.getSecondStart(); if (secondLen >= firstLen) continue; // ADD or pure CHANGE editorStart = d.getSecondStart(); firstShift = firstLen - secondLen; } else { // DELETE editorStart = d.getSecondStart() + 1; } for (int c = editorStart + firstShift -1; c= 0; i--) { Difference d = differences[i]; if (d.getType() == Difference.DELETE) continue; int firstStart; int firstShift = d.getSecondEnd() - d.getSecondStart() +1; if (d.getType() == Difference.CHANGE) { int firstLen = d.getFirstEnd() - d.getFirstStart(); int secondLen = d.getSecondEnd() - d.getSecondStart(); if (secondLen <= firstLen) continue; // REMOVE or pure CHANGE firstShift = secondLen - firstLen; firstStart = d.getFirstStart(); } else { firstStart = d.getFirstStart() + 1; } for (int k = firstStart-1; k it = lines.iterator(); elementAnnotations = Collections.synchronizedMap(new HashMap(lines.size())); while (it.hasNext()) { VcsAnnotation line = it.next(); int lineNum = ann2editorPermutation[line.getLineNumber() -1]; try { int lineOffset = NbDocument.findLineOffset(sd, lineNum -1); Element element = sd.getParagraphElement(lineOffset); elementAnnotations.put(element, line); } catch (IndexOutOfBoundsException ex) { // TODO how could I get line behind document end? // furtunately user does not spot it ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, ex); } } } finally { doc.atomicUnlock(); } // lazy listener registration caret = textComponent.getCaret(); if (caret != null) { caret.addChangeListener(this); } textComponent.addPropertyChangeListener(this); this.caretTimer = new Timer(500, this); caretTimer.setRepeats(false); onCurrentLine(); revalidate(); repaint(); } // implementation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** * Registers "close" popup menu, tooltip manager * and repaint on documet change manager. */ public void addNotify() { super.addNotify(); this.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { maybeShowPopup(e); } public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } private void maybeShowPopup(MouseEvent e) { if (e.isPopupTrigger()) { e.consume(); createPopup(getLineFromMouseEvent(e)).show(e.getComponent(), e.getX(), e.getY()); } } }); // register with tooltip manager setToolTipText(""); // NOI18N } private JPopupMenu createPopup(int line) { final ResourceBundle loc = NbBundle.getBundle(AnnotationBar.class); final JPopupMenu popupMenu = new JPopupMenu(); VcsAnnotation annotation = getAnnotateLine(line); if (annotation != null && vcsAnnotations != null) { Action [] actions = vcsAnnotations.getActions(annotation); for (Action action : actions) { popupMenu.add(Utils.toMenuItem(action)); } popupMenu.addSeparator(); } JMenuItem menu = new JMenuItem(loc.getString("CTL_MenuItem_CloseAnnotations")); menu.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { hideBar(); } }); popupMenu.add(menu); return popupMenu; } /** * Hides the annotation bar from user. */ void hideBar() { annotated = false; revalidate(); release(); } /** * Gets a request processor which is able to cancel tasks. */ private RequestProcessor getRequestProcessor() { if (requestProcessor == null) { requestProcessor = new RequestProcessor("AnnotationBarRP", 1, true); // NOI18N } return requestProcessor; } /** * Shows commit message in status bar and or revision change repaints side * bar (to highlight same revision). This process is started in a * seperate thread. */ private void onCurrentLine() { if (latestAnnotationTask != null) { latestAnnotationTask.cancel(); } if (isAnnotated()) { latestAnnotationTask = getRequestProcessor().post(this); } } // latestAnnotationTask business logic @Override public void run() { Caret carett = this.caret; if (carett == null || !isAnnotated()) { // closed in the meantime return; } // get resource bundle ResourceBundle loc = NbBundle.getBundle(AnnotationBar.class); // give status bar "wait" indication StatusBar statusBar = editorUI.getStatusBar(); recentStatusMessage = loc.getString("CTL_StatusBar_WaitFetchAnnotation"); statusBar.setText(StatusBar.CELL_MAIN, recentStatusMessage); // determine current line int line = -1; int offset = carett.getDot(); try { line = Utilities.getLineOffset(doc, offset); } catch (BadLocationException ex) { ErrorManager err = ErrorManager.getDefault(); err.annotate(ex, "Can not get line for caret at offset " + offset); // NOI18N err.notify(ex); clearRecentFeedback(); return; } // handle locally modified lines VcsAnnotation al = getAnnotateLine(line); if (al == null) { AnnotationMarkProvider amp = AnnotationMarkInstaller.getMarkProvider(textComponent); if (amp != null) { amp.setMarks(Collections.emptyList()); } clearRecentFeedback(); if (recentRevision != null) { recentRevision = null; repaint(); } return; } // handle unchanged lines String revision = al.getRevision(); if (!revision.equals(recentRevision)) { recentRevision = revision; repaint(); AnnotationMarkProvider amp = AnnotationMarkInstaller.getMarkProvider(textComponent); if (amp != null) { List marks = new ArrayList(elementAnnotations.size()); // I cannot affort to lock elementAnnotations for long time // it's accessed from editor thread too Iterator> it2; synchronized(elementAnnotations) { it2 = new HashSet>(elementAnnotations.entrySet()).iterator(); } while (it2.hasNext()) { Map.Entry next = it2.next(); VcsAnnotation annotateLine = next.getValue(); if (revision.equals(annotateLine.getRevision())) { Element element = next.getKey(); if (!elementAnnotations.containsKey(element)) { continue; } int elementOffset = element.getStartOffset(); int lineNumber = NbDocument.findLineNumber((StyledDocument)doc, elementOffset); AnnotationMark mark = new AnnotationMark(lineNumber, revision); marks.add(mark); } if (Thread.interrupted()) { clearRecentFeedback(); return; } } amp.setMarks(marks); } } if (al.getDescription() != null) { recentStatusMessage = al.getDescription(); statusBar.setText(StatusBar.CELL_MAIN, al.getAuthor() + ": " + recentStatusMessage); // NOI18N } else { clearRecentFeedback(); }; } /** * Clears the status bar if it contains the latest status message * displayed by this annotation bar. */ private void clearRecentFeedback() { StatusBar statusBar = editorUI.getStatusBar(); if (statusBar.getText(StatusBar.CELL_MAIN) == recentStatusMessage) { statusBar.setText(StatusBar.CELL_MAIN, ""); // NOI18N } } /** * Components created by SibeBarFactory are positioned * using a Layout manager that determines componnet size * by retireving preferred size. * *

Once componnet needs resizing it simply calls * {@link #revalidate} that triggers new layouting * that consults prefered size. */ public Dimension getPreferredSize() { Dimension dim = textComponent.getSize(); dim.width = annotated ? getBarWidth() : 0; dim.height *=2; // XXX return dim; } /** * Gets the preferred width of this component. * * @return the preferred width of this component */ private int getBarWidth() { String longestString = ""; // NOI18N if (elementAnnotations == null) { longestString = elementAnnotationsSubstitute; } else { synchronized(elementAnnotations) { for (VcsAnnotation line : elementAnnotations.values()) { String displayName = getDisplayName(line); // NOI18N if (displayName.length() > longestString.length()) { longestString = displayName; } } } } char[] data = longestString.toCharArray(); int w = getGraphics().getFontMetrics().charsWidth(data, 0, data.length); return w + 4; } private String getDisplayName(VcsAnnotation line) { return line.getRevision() + " " + line.getAuthor(); // NOI18N } /** * Pair method to {@link #annotate}. It releases * all resources. */ private void release() { editorUI.removePropertyChangeListener(this); textComponent.removeComponentListener(this); textComponent.removePropertyChangeListener(this); doc.removeDocumentListener(this); if (caret != null) { caret.removeChangeListener(this); } if (caretTimer != null) { caretTimer.removeActionListener(this); } elementAnnotations = null; // cancel running annotation task if active if(latestAnnotationTask != null) { latestAnnotationTask.cancel(); } AnnotationMarkProvider amp = AnnotationMarkInstaller.getMarkProvider(textComponent); if (amp != null) { amp.setMarks(Collections.emptyList()); } clearRecentFeedback(); } /** * Paints one view that corresponds to a line (or * multiple lines if folding takes effect). */ private void paintView(View view, Graphics g, int yBase) { JTextComponent component = editorUI.getComponent(); if (component == null) return; BaseTextUI textUI = (BaseTextUI)component.getUI(); Element rootElem = textUI.getRootView(component).getElement(); int line = rootElem.getElementIndex(view.getStartOffset()); String annotation = ""; // NOI18N VcsAnnotation al = null; if (elementAnnotations != null) { al = getAnnotateLine(line); if (al != null) { annotation = getDisplayName(al); // NOI18N } } else { annotation = elementAnnotationsSubstitute; } if (al != null && al.getRevision().equals(recentRevision)) { g.setColor(selectedColor()); } else { g.setColor(foregroundColor()); } g.drawString(annotation, 2, yBase + editorUI.getLineAscent()); } /** * Presents commit message as tooltips. */ public String getToolTipText (MouseEvent e) { if (editorUI == null) return null; int line = getLineFromMouseEvent(e); StringBuffer annotation = new StringBuffer(); if (elementAnnotations != null) { VcsAnnotation al = getAnnotateLine(line); if (al != null) { String escapedAuthor = NbBundle.getMessage(AnnotationBar.class, "TT_Annotation"); // NOI18N try { escapedAuthor = XMLUtil.toElementContent(al.getAuthor()); } catch (CharConversionException e1) { ErrorManager err = ErrorManager.getDefault(); err.annotate(e1, "CVS.AB: can not HTML escape: " + al.getAuthor()); // NOI18N err.notify(ErrorManager.INFORMATIONAL, e1); } // always return unique string to avoid tooltip sharing on mouse move over same revisions --> annotation.append("").append(al.getRevision()).append(" - ").append(escapedAuthor).append(""); // NOI18N if (al.getDate() != null) { annotation.append(" ").append(DateFormat.getDateInstance().format(al.getDate())); // NOI18N } if (al.getDescription() != null) { String escaped = null; try { escaped = XMLUtil.toElementContent(al.getDescription()); } catch (CharConversionException e1) { ErrorManager err = ErrorManager.getDefault(); err.annotate(e1, "CVS.AB: can not HTML escape: " + al.getDescription()); // NOI18N err.notify(ErrorManager.INFORMATIONAL, e1); } if (escaped != null) { String lined = escaped.replaceAll(System.getProperty("line.separator"), "
"); // NOI18N annotation.append("

").append(lined); // NOI18N } } } } else { annotation.append(elementAnnotationsSubstitute); } return annotation.toString(); } /** * Locates AnnotateLine associated with given line. The * line is translated to Element that is used as map lookup key. * The map is initially filled up with Elements sampled on * annotate() method. * *

Key trick is that Element's identity is maintained * until line removal (and is restored on undo). * * @param line * @return found AnnotateLine or null */ private VcsAnnotation getAnnotateLine(int line) { StyledDocument sd = (StyledDocument) doc; int lineOffset = NbDocument.findLineOffset(sd, line); Element element = sd.getParagraphElement(lineOffset); VcsAnnotation al = elementAnnotations.get(element); if (al != null) { int startOffset = element.getStartOffset(); int endOffset = element.getEndOffset(); try { int len = endOffset - startOffset; String text = doc.getText(startOffset, len -1); String content = al.getDocumentText(); if (text.equals(content)) { return al; } } catch (BadLocationException e) { ErrorManager err = ErrorManager.getDefault(); err.annotate(e, "CVS.AB: can not locate line annotation."); // NOI18N err.notify(ErrorManager.INFORMATIONAL, e); } } return null; } /** * GlyphGutter copy pasted bolerplate method. * It invokes {@link #paintView} that contains * actual business logic. */ public void paintComponent(Graphics g) { super.paintComponent(g); Rectangle clip = g.getClipBounds(); JTextComponent component = editorUI.getComponent(); if (component == null) return; BaseTextUI textUI = (BaseTextUI)component.getUI(); View rootView = Utilities.getDocumentView(component); if (rootView == null) return; g.setColor(backgroundColor()); g.fillRect(clip.x, clip.y, clip.width, clip.height); if (renderingHints != null) { ((Graphics2D) g).addRenderingHints(renderingHints); } AbstractDocument doc = (AbstractDocument)component.getDocument(); doc.readLock(); try{ foldHierarchy.lock(); try{ int startPos = textUI.getPosFromY(clip.y); int startViewIndex = rootView.getViewIndex(startPos,Position.Bias.Forward); int rootViewCount = rootView.getViewCount(); if (startViewIndex >= 0 && startViewIndex < rootViewCount) { int clipEndY = clip.y + clip.height; for (int i = startViewIndex; i < rootViewCount; i++){ View view = rootView.getView(i); Rectangle rec = component.modelToView(view.getStartOffset()); if (rec == null) { break; } int y = rec.y; paintView(view, g, y); if (y >= clipEndY) { break; } } } } finally { foldHierarchy.unlock(); } } catch (BadLocationException ble){ ErrorManager.getDefault().notify(ble); } finally { doc.readUnlock(); } } private Color backgroundColor() { if (textComponent != null) { return textComponent.getBackground(); } return backgroundColor; } private Color foregroundColor() { if (textComponent != null) { return textComponent.getForeground(); } return foregroundColor; } private Color selectedColor() { if (backgroundColor == backgroundColor()) { return selectedColor; } if (textComponent != null) { return textComponent.getCaretColor(); } return selectedColor; } /** GlyphGutter copy pasted utility method. */ private int getLineFromMouseEvent(MouseEvent e){ int line = -1; if (editorUI != null) { try { JTextComponent component = editorUI.getComponent(); BaseTextUI textUI = (BaseTextUI)component.getUI(); int clickOffset = textUI.viewToModel(component, new Point(0, e.getY())); line = Utilities.getLineOffset(doc, clickOffset); } catch (BadLocationException ble) { // there's not such line, return -1 } } return line; } /** Implementation */ public void propertyChange(PropertyChangeEvent evt) { if (evt == null) return; String id = evt.getPropertyName(); if (evt.getSource() == textComponent) { if ("caret".equals(id)) { if (caret != null) { caret.removeChangeListener(this); } caret = textComponent.getCaret(); if (caret != null) { caret.addChangeListener(this); } } return; } if (EditorUI.COMPONENT_PROPERTY.equals(id)) { // NOI18N if (evt.getNewValue() == null){ // component deinstalled, lets uninstall all isteners release(); } } } /** Implementation */ public void changedUpdate(DocumentEvent e) { } /** Implementation */ public void insertUpdate(DocumentEvent e) { // handle new lines, Enter hit at end of line changes // the line element instance // XXX Actually NB document implementation triggers this method two times // - first time with one removed and two added lines // - second time with two removed and two added lines if (elementAnnotations != null) { Element[] elements = e.getDocument().getRootElements(); synchronized(elementAnnotations) { // atomic change for (int i = 0; i < elements.length; i++) { Element element = elements[i]; DocumentEvent.ElementChange change = e.getChange(element); if (change == null) continue; Element[] removed = change.getChildrenRemoved(); Element[] added = change.getChildrenAdded(); if (removed.length == added.length) { for (int c = 0; c 0) { Element key = removed[0]; VcsAnnotation recent = elementAnnotations.get(key); if (recent != null) { elementAnnotations.remove(key); elementAnnotations.put(added[0], recent); } } } } } repaint(); } /** Implementation */ @Override public void removeUpdate(DocumentEvent e) { final int length = e.getDocument().getLength(); Mutex.EVENT.readAccess(new Runnable() { @Override public void run () { if (length == 0) { // external reload hideBar(); } repaint(); } }); } /** Caret */ public void stateChanged(ChangeEvent e) { assert e.getSource() == caret; caretTimer.restart(); } /** Timer */ public void actionPerformed(ActionEvent e) { assert e.getSource() == caretTimer; onCurrentLine(); } /** on JTextPane */ public void componentHidden(ComponentEvent e) { } /** on JTextPane */ public void componentMoved(ComponentEvent e) { } /** on JTextPane */ public void componentResized(ComponentEvent e) { revalidate(); } /** on JTextPane */ public void componentShown(ComponentEvent e) { } boolean isAnnotated() { return annotated; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy