From 05365198357a80a6f50a227979b28f6c39c82f2f Mon Sep 17 00:00:00 2001 From: Michael Bien Date: Sun, 16 Feb 2025 07:22:16 +0100 Subject: [PATCH] Improved document status indicator tooltips - status indicator tooltip will now show a warning/error annotation histograms - tooltip position moved left from scrollbar - reduced tooltip delay - tooltip will follow mouse once shown to make it easier to find annotations other changes - deprecation fixes - override annotations and other minor code updates --- .../nbproject/project.properties | 2 +- .../editor/errorstripe/AnnotationView.java | 167 ++++++++++++------ .../errorstripe/AnnotationViewData.java | 4 +- .../errorstripe/AnnotationViewDataImpl.java | 115 +++++++----- 4 files changed, 191 insertions(+), 97 deletions(-) diff --git a/ide/editor.errorstripe/nbproject/project.properties b/ide/editor.errorstripe/nbproject/project.properties index 2b67e507c439..def1c77ec5c2 100644 --- a/ide/editor.errorstripe/nbproject/project.properties +++ b/ide/editor.errorstripe/nbproject/project.properties @@ -16,7 +16,7 @@ # under the License. javac.compilerargs=-Xlint -Xlint:-serial -javac.source=1.8 +javac.release=17 spec.version.base=2.62.0 nbm.needs.restart=true diff --git a/ide/editor.errorstripe/src/org/netbeans/modules/editor/errorstripe/AnnotationView.java b/ide/editor.errorstripe/src/org/netbeans/modules/editor/errorstripe/AnnotationView.java index 61d33e10a136..51e1900cdcda 100644 --- a/ide/editor.errorstripe/src/org/netbeans/modules/editor/errorstripe/AnnotationView.java +++ b/ide/editor.errorstripe/src/org/netbeans/modules/editor/errorstripe/AnnotationView.java @@ -33,7 +33,7 @@ import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.reflect.Field; -import java.text.MessageFormat; +import java.util.Map; import javax.accessibility.Accessible; import javax.accessibility.AccessibleContext; import javax.accessibility.AccessibleRole; @@ -41,8 +41,11 @@ import javax.swing.ImageIcon; import javax.swing.JComponent; import javax.swing.JLayeredPane; +import javax.swing.JScrollBar; import javax.swing.JScrollPane; +import javax.swing.JToolTip; import javax.swing.SwingUtilities; +import javax.swing.ToolTipManager; import javax.swing.UIManager; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; @@ -53,13 +56,13 @@ import javax.swing.text.JTextComponent; import javax.swing.text.StyledDocument; import javax.swing.text.View; +import org.netbeans.api.editor.document.LineDocumentUtils; import org.netbeans.api.editor.fold.FoldHierarchy; import org.netbeans.api.editor.fold.FoldHierarchyEvent; import org.netbeans.api.editor.fold.FoldHierarchyListener; import org.netbeans.editor.BaseDocument; import org.netbeans.editor.BaseTextUI; -import org.netbeans.editor.Utilities; import org.netbeans.lib.editor.util.StringEscapeUtils; import org.netbeans.modules.editor.errorstripe.caret.CaretMark; import org.netbeans.modules.editor.errorstripe.privatespi.Mark; @@ -183,7 +186,7 @@ private synchronized void updateForNewDocument() { while (position == _modelToView(startLine - 1, componentHeight, usableHeight) && startLine > 0) startLine--; - while ((endLine + 1) < Utilities.getRowCount(doc) && position == _modelToView(endLine + 1, componentHeight, usableHeight)) + while ((endLine + 1) < LineDocumentUtils.getLineCount(doc) && position == _modelToView(endLine + 1, componentHeight, usableHeight)) endLine++; return new int[] {startLine, endLine}; @@ -453,36 +456,29 @@ public void run() { final boolean clearModelToViewCache= readAndDestroyClearModelToViewCache(); //Fix for #54193: - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - synchronized (AnnotationView.this) { - if (clearMarksCache) { - data.clear(); - } - if (clearModelToViewCache) { - modelToViewCache = null; - } + SwingUtilities.invokeLater(() -> { + synchronized (AnnotationView.this) { + if (clearMarksCache) { + data.clear(); + } + if (clearModelToViewCache) { + modelToViewCache = null; } - - invalidate(); - repaint(); } + invalidate(); + repaint(); }); } } private void documentChange() { - fullRepaint(lines != Utilities.getRowCount(doc)); + fullRepaint(lines != LineDocumentUtils.getLineCount(doc)); } private double getComponentHeight() { final double[] ret = new double[1]; - pane.getDocument().render(new Runnable() { - @Override - public void run() { - ret[0] = pane.getUI().getRootView(pane).getPreferredSpan(View.Y_AXIS); - } + pane.getDocument().render(() -> { + ret[0] = pane.getUI().getRootView(pane).getPreferredSpan(View.Y_AXIS); }); return ret[0]; } @@ -533,8 +529,8 @@ private int getYFromPos(int offset) throws BadLocationException { // For some reason the offset may become -1; uncomment following line to see that offset = Math.max(offset, 0); - if (ui instanceof BaseTextUI) { - result = ((BaseTextUI) ui).getYFromPos(offset); + if (ui instanceof BaseTextUI baseTextUI) { + result = baseTextUI.getYFromPos(offset); } else { Rectangle r = pane.modelToView(offset); @@ -549,11 +545,11 @@ private int getYFromPos(int offset) throws BadLocationException { } private synchronized int getModelToViewImpl(int line) throws BadLocationException { - int docLines = Utilities.getRowCount(doc); + int docLines = LineDocumentUtils.getLineCount(doc); if (modelToViewCache == null || height != pane.getHeight() || lines != docLines) { - modelToViewCache = new int[Utilities.getRowCount(doc) + 2]; - lines = Utilities.getRowCount(doc); + modelToViewCache = new int[LineDocumentUtils.getLineCount(doc) + 2]; + lines = LineDocumentUtils.getLineCount(doc); height = pane.getHeight(); } @@ -563,7 +559,7 @@ private synchronized int getModelToViewImpl(int line) throws BadLocationExceptio int result = modelToViewCache[line + 1]; if (result == 0) { - int lineOffset = Utilities.getRowStartFromLineOffset((BaseDocument) pane.getDocument(), line); + int lineOffset = LineDocumentUtils.getLineStartFromIndex((BaseDocument) pane.getDocument(), line); modelToViewCache[line + 1] = result = getYFromPos(lineOffset); } @@ -619,7 +615,7 @@ private double _modelToView(int line, double componentHeight, double usableHeigh if (positionOffset == -1) { return null; } - int line = Utilities.getLineOffset(doc, positionOffset); + int line = LineDocumentUtils.getLineIndex(doc, positionOffset); if (ERR.isLoggable(VIEW_TO_MODEL_IMPORTANCE)) { ERR.log(VIEW_TO_MODEL_IMPORTANCE, "AnnotationView.viewToModel: line=" + line); // NOI18N @@ -643,7 +639,7 @@ private double _modelToView(int line, double componentHeight, double usableHeigh if (positionOffset == -1) { return null; } - int line = Utilities.getLineOffset(doc, positionOffset) + 1; + int line = LineDocumentUtils.getLineIndex(doc, positionOffset) + 1; int[] span = getLinesSpan(line); double normalizedOffset = modelToView(span[0]); @@ -756,13 +752,24 @@ public void mouseMoved(MouseEvent e) { checkCursor(e); } + private int initialToolTipDelay; + private int dismissToolTipDelay; + @Override public void mouseExited(MouseEvent e) { + ToolTipManager ttm = ToolTipManager.sharedInstance(); + ttm.setInitialDelay(initialToolTipDelay); + ttm.setDismissDelay(dismissToolTipDelay); resetCursor(); } @Override public void mouseEntered(MouseEvent e) { + ToolTipManager ttm = ToolTipManager.sharedInstance(); + initialToolTipDelay = ttm.getInitialDelay(); + dismissToolTipDelay = ttm.getDismissDelay(); + ttm.setInitialDelay(200); + ttm.setDismissDelay(60_000); checkCursor(e); } @@ -777,7 +784,7 @@ public void mouseClicked(MouseEvent e) { Mark mark = getMarkForPoint(e.getPoint().getY()); if (mark!= null) { - pane.setCaretPosition(Utilities.getRowStartFromLineOffset(doc, mark.getAssignedLines()[0])); + pane.setCaretPosition(LineDocumentUtils.getLineStartFromIndex(doc, mark.getAssignedLines()[0])); } } @@ -796,52 +803,108 @@ private void checkCursor(MouseEvent e) { setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); } + @Override + public Point getToolTipLocation(MouseEvent event) { + String text = getToolTipText(event); + if (text == null) { + return null; + } + // position left from scrollbar + JToolTip tt = new JToolTip(); + tt.setTipText(text); + int offset = (int) (tt.getPreferredSize().width + new JScrollBar().getPreferredSize().getWidth()); + return new Point(-offset, event.getY()); + } + + private String lastToolTipText; + private MouseEvent lastMouseEvent; + @Override public String getToolTipText(MouseEvent event) { + if (event != lastMouseEvent) { + lastMouseEvent = event; + lastToolTipText = getToolTipTextImpl(event); + } + return lastToolTipText; + } + + private String getToolTipTextImpl(MouseEvent event) { if (ERR.isLoggable(ErrorManager.INFORMATIONAL)) { ERR.log(ErrorManager.INFORMATIONAL, "getToolTipText: event=" + event); // NOI18N } int y = event.getY(); + // document status indicator if (y <= topOffset()) { - int[] errWar = data.computeErrorsAndWarnings(); - int errors = errWar[0]; - int warnings = errWar[1]; + AnnotationViewData.Stats stats = data.computeAnnotationStatistics(); + int errors = stats.errors(); + int warnings = stats.warnings(); if (errors == 0 && warnings == 0) { - return NbBundle.getBundle(AnnotationView.class).getString("TP_NoErrors"); // NOI18N + return NbBundle.getMessage(AnnotationView.class, "TP_NoErrors"); // NOI18N } - if (errors == 0 && warnings != 0) { - return MessageFormat.format(NbBundle.getBundle(AnnotationView.class).getString("TP_X_warning(s)"), new Object[] {Integer.valueOf(warnings)}); // NOI18N - } + StringBuilder text = new StringBuilder(); + text.append(""); + appendHistogram(text, stats.err_histogram(), "#000000", "#ff8888"); + appendHistogram(text, stats.war_histogram(), "#000000", "#ffff88"); - if (errors != 0 && warnings == 0) { - return MessageFormat.format(NbBundle.getBundle(AnnotationView.class).getString("TP_X_error(s)"), new Object[] {Integer.valueOf(errors)}); // NOI18N + if (errors == 0 && warnings != 0) { + text.append(NbBundle.getMessage(AnnotationView.class, "TP_X_warning(s)", warnings)); // NOI18N + } else if (errors != 0 && warnings == 0) { + text.append(NbBundle.getMessage(AnnotationView.class, "TP_X_error(s)", errors)); // NOI18N + } else { + text.append(NbBundle.getMessage(AnnotationView.class, "TP_X_error(s)_Y_warning(s)", errors, warnings)); // NOI18N } - - return MessageFormat.format(NbBundle.getBundle(AnnotationView.class).getString("TP_X_error(s)_Y_warning(s)"), new Object[] {Integer.valueOf(errors), Integer.valueOf(warnings)}); // NOI18N + return text.toString(); } - + + // annotation bar Mark mark = getMarkForPoint(y); - if (mark != null) { String description = mark.getShortDescription(); - if (description != null) { - if (description != null) { - // #122422 - some descriptions are intentionaly a valid HTML and don't want to be escaped - if (description.startsWith(HTML_PREFIX_LOWERCASE) || description.startsWith(HTML_PREFIX_UPPERCASE)) { - return description; - } else { - return "" + StringEscapeUtils.escapeHtml(description); // NOI18N - } + // #122422 - some descriptions are intentionaly a valid HTML and don't want to be escaped + if (description.startsWith(HTML_PREFIX_LOWERCASE) || description.startsWith(HTML_PREFIX_UPPERCASE)) { + return description; + } else { + return "" + StringEscapeUtils.escapeHtml(description); // NOI18N } } } return null; } + + private static void appendHistogram(StringBuilder sb, Map histogram, String fg, String bg) { + if (histogram.isEmpty()) { + return; + } + int n = 0; + sb.append("
"); + for (Map.Entry annotation : histogram.entrySet()) { + sb.append(annotation.getValue()).append(" ") + .append(StringEscapeUtils.escapeHtml(toShortLine(annotation.getKey()))).append("
"); + if (n++ > 20) { + sb.append("(...)
"); + break; + } + } + sb.append("
"); + } + + @SuppressWarnings("AssignmentToMethodParameter") + private static String toShortLine(String desc) { + int cut = desc.indexOf("\n"); + if (cut != -1) { + desc = desc.substring(0, cut); + } + int max = 200; + if (desc.length() > max) { + desc = desc.substring(0, max) + "..."; + } + return desc; + } private static final String HTML_PREFIX_LOWERCASE = " err_histogram, int errors, Map war_histogram, int warnings) {} } diff --git a/ide/editor.errorstripe/src/org/netbeans/modules/editor/errorstripe/AnnotationViewDataImpl.java b/ide/editor.errorstripe/src/org/netbeans/modules/editor/errorstripe/AnnotationViewDataImpl.java index 3310ceabc814..a91fdea31dcd 100644 --- a/ide/editor.errorstripe/src/org/netbeans/modules/editor/errorstripe/AnnotationViewDataImpl.java +++ b/ide/editor.errorstripe/src/org/netbeans/modules/editor/errorstripe/AnnotationViewDataImpl.java @@ -27,15 +27,20 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.SortedMap; import java.util.TreeMap; import java.util.WeakHashMap; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import javax.swing.text.JTextComponent; import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.api.editor.mimelookup.MimePath; @@ -59,6 +64,8 @@ import org.openide.cookies.InstanceCookie; import org.openide.filesystems.FileObject; import org.openide.loaders.DataObject; +import org.openide.text.Annotation; +import org.openide.util.Lookup; import org.openide.util.NbCollections; import org.openide.util.WeakListeners; @@ -77,8 +84,8 @@ final class AnnotationViewDataImpl implements PropertyChangeListener, Annotation private Reference paneRef; private BaseDocument document; - private List markProviders = new ArrayList(); - private List statusProviders = new ArrayList(); + private List markProviders = new ArrayList<>(); + private List statusProviders = new ArrayList<>(); private List markProvidersWeakLs = new ArrayList<>(); private List statusProvidersWeakLs = new ArrayList<>(); @@ -86,8 +93,8 @@ final class AnnotationViewDataImpl implements PropertyChangeListener, Annotation private Collection currentMarks = null; private SortedMap> marksMap = null; - private static WeakHashMap> mime2Creators = new WeakHashMap>(); - private static WeakHashMap> mime2StatusProviders = new WeakHashMap>(); + private static WeakHashMap> mime2Creators = new WeakHashMap<>(); + private static WeakHashMap> mime2StatusProviders = new WeakHashMap<>(); private static LegacyCrapProvider legacyCrap; @@ -100,6 +107,7 @@ public AnnotationViewDataImpl(AnnotationView view, JTextComponent pane) { this.document = null; } + @Override public void register(BaseDocument document) { this.document = document; @@ -118,6 +126,7 @@ public void register(BaseDocument document) { clear(); } + @Override public void unregister() { if (document != null && weakL != null) { document.getAnnotations().removeAnnotationsListener(weakL); @@ -149,7 +158,7 @@ private void gatherProviders(JTextComponent pane) { long start = System.currentTimeMillis(); // Collect legacy mark providers - List newMarkProviders = new ArrayList(); + List newMarkProviders = new ArrayList<>(); if (legacyCrap != null) { createMarkProviders(legacyCrap.getMarkProviderCreators(), newMarkProviders, pane); } @@ -177,7 +186,7 @@ private void gatherProviders(JTextComponent pane) { // Collect legacy status providers - List newStatusProviders = new ArrayList(); + List newStatusProviders = new ArrayList<>(); if (legacyCrap != null) { createStatusProviders(legacyCrap.getUpToDateStatusProviderFactories(), newStatusProviders, pane); } @@ -269,7 +278,7 @@ private void removeListenersFromMarkProviders() { } /*package private*/ static Collection createMergedMarks(List providers) { - Collection result = new LinkedHashSet(); + Collection result = new LinkedHashSet<>(); for(MarkProvider provider : providers) { result.addAll(provider.getMarks()); @@ -283,7 +292,7 @@ private void removeListenersFromMarkProviders() { currentMarks = createMergedMarks(markProviders); } - return new ArrayList(currentMarks); + return new ArrayList<>(currentMarks); } /*package private*/ static List getStatusesForLineImpl(int line, SortedMap> marks) { @@ -291,6 +300,7 @@ private void removeListenersFromMarkProviders() { return inside == null ? Collections.emptyList() : inside; } + @Override public Mark getMainMarkForBlock(int startLine, int endLine) { Mark m1; synchronized(this) { @@ -364,6 +374,7 @@ private Mark getMainMarkForBlockAnnotations(int startLine, int endLine) { return null; } + @Override public int findNextUsedLine(int from) { int line1; synchronized (this) { @@ -389,7 +400,7 @@ public int findNextUsedLine(int from) { return Integer.MAX_VALUE; } - return next.firstKey().intValue(); + return next.firstKey(); } private void registerMark(Mark mark) { @@ -400,14 +411,8 @@ private void registerMark(Mark mark) { } for (int line = span[0]; line <= span[1]; line++) { - List inside = marksMap.get(line); - - if (inside == null) { - inside = new ArrayList(); - marksMap.put(line, inside); - } - - inside.add(mark); + marksMap.computeIfAbsent(line, k -> new ArrayList<>()) + .add(mark); } } @@ -424,7 +429,7 @@ private void unregisterMark(Mark mark) { if (inside != null) { inside.remove(mark); - if (inside.size() == 0) { + if (inside.isEmpty()) { marksMap.remove(line); } } @@ -434,7 +439,7 @@ private void unregisterMark(Mark mark) { /*package private for tests*/synchronized SortedMap> getMarkMap() { if (marksMap == null) { Collection marks = getMergedMarks(); - marksMap = new TreeMap>(); + marksMap = new TreeMap<>(); for (Mark mark : marks) { registerMark(mark); @@ -495,8 +500,8 @@ public void propertyChange(PropertyChangeEvent evt) { nue = ((MarkProvider) evt.getSource()).getMarks(); if (old != null && nue != null) { - Collection added = new LinkedHashSet(nue); - Collection removed = new LinkedHashSet(old); + Collection added = new LinkedHashSet<>(nue); + Collection removed = new LinkedHashSet<>(old); // own removeAll since indexof on HashSet is faster than on ArrayList and AbstractSet call indexof on smaller collection for (Iterator old_it = old.iterator(); old_it.hasNext();) { @@ -517,7 +522,7 @@ public void propertyChange(PropertyChangeEvent evt) { } if (currentMarks != null) { - LinkedHashSet copy = new LinkedHashSet(currentMarks); + LinkedHashSet copy = new LinkedHashSet<>(currentMarks); copy.removeAll(removed); copy.addAll(added); currentMarks = copy; @@ -535,25 +540,32 @@ public void propertyChange(PropertyChangeEvent evt) { if (UpToDateStatusProvider.PROP_UP_TO_DATE.equals(evt.getPropertyName())) { view.fullRepaint(false); - return ; } } + @Override public synchronized void clear() { currentMarks = null; marksMap = null; } - public int[] computeErrorsAndWarnings() { + @Override + public Stats computeAnnotationStatistics() { int errors = 0; int warnings = 0; - Collection marks = getMergedMarks(); + Map err_histogram = new HashMap<>(); + Map war_histogram = new HashMap<>(); - for(Mark mark : marks) { + for (Mark mark : getMergedMarks()) { Status s = mark.getStatus(); errors += s == Status.STATUS_ERROR ? 1 : 0; warnings += s == Status.STATUS_WARNING ? 1 : 0; + if (s == Status.STATUS_ERROR || s == Status.STATUS_WARNING) { + increment(err_histogram, mark.getShortDescription()); + } else if (s == Status.STATUS_WARNING) { + increment(war_histogram, mark.getShortDescription()); + } } for (AnnotationDesc desc : NbCollections.iterable(listAnnotations(-1, Integer.MAX_VALUE))) { @@ -562,16 +574,40 @@ public int[] computeErrorsAndWarnings() { if (s != null) { errors += s == Status.STATUS_ERROR ? 1 : 0; warnings += s == Status.STATUS_WARNING ? 1 : 0; + if (s == Status.STATUS_ERROR || s == Status.STATUS_WARNING) { + if (desc instanceof Lookup.Provider lp) { + Annotation an = lp.getLookup().lookup(Annotation.class); + if (an != null) { + if (s == Status.STATUS_ERROR) { + increment(err_histogram, an.getShortDescription()); + } else if (s == Status.STATUS_WARNING) { + increment(war_histogram, an.getShortDescription()); + } + } + } + } } } - - return new int[] {errors, warnings}; + return new Stats(sortByValue(err_histogram), errors, + sortByValue(war_histogram), warnings); + } + + private void increment(Map histogram, String desc) { + histogram.compute(desc, (k, v) -> v == null ? 1 : v + 1); + } + + private static LinkedHashMap sortByValue(Map map) { + return map.entrySet().stream() + .sorted(Comparator.comparing(Map.Entry::getValue, Comparator.reverseOrder())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); } + @Override public void changedLine(int Line) { changedAll(); } + @Override public void changedAll() { view.fullRepaint(false); } @@ -595,7 +631,7 @@ private Iterator listAnnotations(final int startLine, final Annotations annotations = document.getAnnotations(); return new Iterator() { - private final List remaining = new ArrayList(); + private final List remaining = new ArrayList<>(); private int line = startLine; private int last = (-1); private int unchagedLoops = 0; @@ -658,7 +694,7 @@ private Iterator listAnnotations(final int startLine, // that registered stuff in text/base. The artificial text/base // mime type is deprecated and should not be used anymore. @MimeLocation(subfolderName=UP_TO_DATE_STATUS_PROVIDER_FOLDER_NAME, instanceProviderClass=LegacyCrapProvider.class) - public static final class LegacyCrapProvider implements InstanceProvider { + public static final class LegacyCrapProvider implements InstanceProvider { private final List instanceFiles; private List creators; @@ -686,18 +722,11 @@ public Collection getUpToDateStatusProv return factories; } - public Object createInstance(List fileObjectList) { - ArrayList textBaseFilesList = new ArrayList(); - - for(Object o : fileObjectList) { - FileObject fileObject = null; - - if (o instanceof FileObject) { - fileObject = (FileObject) o; - } else { - continue; - } + @Override + public LegacyCrapProvider createInstance(List fileObjectList) { + ArrayList textBaseFilesList = new ArrayList<>(); + for (FileObject fileObject : fileObjectList) { String fullPath = fileObject.getPath(); int idx = fullPath.lastIndexOf(UP_TO_DATE_STATUS_PROVIDER_FOLDER_NAME); assert idx != -1 : "Expecting files with '" + UP_TO_DATE_STATUS_PROVIDER_FOLDER_NAME + "' in the path: " + fullPath; //NOI18N @@ -715,8 +744,8 @@ public Object createInstance(List fileObjectList) { } private void computeInstances() { - ArrayList newCreators = new ArrayList(); - ArrayList newFactories = new ArrayList(); + ArrayList newCreators = new ArrayList<>(); + ArrayList newFactories = new ArrayList<>(); for(FileObject f : instanceFiles) { if (!f.isValid() || !f.isData()) {