--- a/editor.lib2/src/org/netbeans/api/editor/caret/CaretItem.java +++ a/editor.lib2/src/org/netbeans/api/editor/caret/CaretItem.java @@ -44,6 +44,7 @@ import java.awt.Point; import java.awt.Rectangle; import java.util.logging.Logger; +import javax.swing.text.JTextComponent; import javax.swing.text.Position; import org.netbeans.api.annotations.common.CheckForNull; @@ -62,12 +63,14 @@ // -J-Dorg.netbeans.modules.editor.lib2.CaretItem.level=FINEST private static final Logger LOG = Logger.getLogger(CaretItem.class.getName()); - private static final int TRANSACTION_MARK_REMOVED = 1; + private static final int REMOVED_IN_TRANSACTION = 1; private static final int INFO_OBSOLETE = 2; - private static final int UPDATE_VISUAL_BOUNDS = 4; + private static final int UPDATE_CARET_BOUNDS = 4; + private static final int CARET_PAINTED = 8; + private final EditorCaret editorCaret; private Position dotPos; @@ -97,7 +100,7 @@ this.editorCaret = editorCaret; this.dotPos = dotPos; this.markPos = markPos; - this.statusBits = UPDATE_VISUAL_BOUNDS; // Request visual bounds updating automatically + this.statusBits = UPDATE_CARET_BOUNDS; // Request visual bounds updating automatically } EditorCaret editorCaret() { @@ -203,11 +206,26 @@ this.magicCaretPosition = newMagicCaretPosition; } - void setCaretBounds(Rectangle newCaretBounds) { + /** + * Set new caret bounds while repainting both original and new bounds. + * + * @param newCaretBounds non-null new bounds + */ + synchronized Rectangle setCaretBoundsWithRepaint(Rectangle newCaretBounds, JTextComponent c) { + Rectangle oldCaretBounds = this.caretBounds; + boolean repaintOld = (oldCaretBounds != null && (this.statusBits & CARET_PAINTED) != 0); + this.statusBits &= ~CARET_PAINTED; + if (repaintOld) { + c.repaint(oldCaretBounds); // First schedule repaint of the original bounds (even if new bounds will possibly be the same) + } + if (!repaintOld || !newCaretBounds.equals(oldCaretBounds)) { + c.repaint(newCaretBounds); + } this.caretBounds = newCaretBounds; + return oldCaretBounds; } - Rectangle getCaretBounds() { + synchronized Rectangle getCaretBounds() { return this.caretBounds; } @@ -219,43 +237,40 @@ this.transactionIndexHint = transactionIndexHint; } - void markTransactionMarkRemoved() { - this.statusBits |= TRANSACTION_MARK_REMOVED; + synchronized void markRemovedInTransaction() { + this.statusBits |= REMOVED_IN_TRANSACTION; } - boolean isTransactionMarkRemoved() { - return (this.statusBits & TRANSACTION_MARK_REMOVED) != 0; + synchronized boolean getAndClearRemovedInTransaction() { + boolean ret = (this.statusBits & REMOVED_IN_TRANSACTION) != 0; + this.statusBits &= ~REMOVED_IN_TRANSACTION; + return ret; } - void clearTransactionMarkRemoved() { - this.statusBits &= ~TRANSACTION_MARK_REMOVED; + synchronized void markInfoObsolete() { + this.statusBits |= INFO_OBSOLETE; + } + + synchronized boolean getAndClearInfoObsolete() { + boolean ret = (this.statusBits & INFO_OBSOLETE) != 0; + this.statusBits &= ~INFO_OBSOLETE; + return ret; + } + + synchronized void markUpdateCaretBounds() { + this.statusBits |= UPDATE_CARET_BOUNDS; } - void markUpdateVisualBounds() { - this.statusBits |= UPDATE_VISUAL_BOUNDS; + synchronized boolean getAndClearUpdateCaretBounds() { + boolean ret = (this.statusBits & UPDATE_CARET_BOUNDS) != 0; + this.statusBits &= ~UPDATE_CARET_BOUNDS; + return ret; } - boolean isUpdateVisualBounds() { - return (this.statusBits & UPDATE_VISUAL_BOUNDS) != 0; + synchronized void markCaretPainted() { + this.statusBits |= CARET_PAINTED; } - void clearUpdateVisualBounds() { - this.statusBits &= ~UPDATE_VISUAL_BOUNDS; - } - - void markInfoObsolete() { - this.statusBits |= INFO_OBSOLETE; - } - - boolean isInfoObsolete() { - return (this.statusBits & INFO_OBSOLETE) != 0; - } - - void clearInfoObsolete() { - this.statusBits &= ~INFO_OBSOLETE; - } - - @Override public int compareTo(Object o) { return getDot() - ((CaretItem)o).getDot(); --- a/editor.lib2/src/org/netbeans/api/editor/caret/CaretTransaction.java +++ a/editor.lib2/src/org/netbeans/api/editor/caret/CaretTransaction.java @@ -99,6 +99,8 @@ private GapList extraRemovedItems; + private GapList allRemovedItems; + private int[] indexes; private int indexesLength; @@ -157,7 +159,7 @@ caretItem.setMarkPos(markPos); } updateAffectedIndexes(index, index + 1); - caretItem.markUpdateVisualBounds(); + caretItem.markUpdateCaretBounds(); caretItem.markInfoObsolete(); dotOrMarkChanged = true; return true; @@ -172,7 +174,7 @@ int index = findCaretItemIndex(origCaretItems, caretItem); if (index != -1) { caretItem.setMagicCaretPosition(p); - caretItem.clearInfo(); + caretItem.markInfoObsolete(); updateAffectedIndexes(index, index + 1); return true; } @@ -346,7 +348,7 @@ setSelectionStartEnd(itemInfo, lastInfo.startPos, true); } // Remove lastInfo's getCaret item - lastInfo.caretItem.markTransactionMarkRemoved(); + lastInfo.caretItem.markRemovedInTransaction(); origSortedItems.copyElements(copyStartIndex, i - 1, nonOverlappingItems); copyStartIndex = i; @@ -356,7 +358,7 @@ setSelectionStartEnd(lastInfo, itemInfo.endPos, false); } // Remove itemInfo's getCaret item - itemInfo.caretItem.markTransactionMarkRemoved(); + itemInfo.caretItem.markRemovedInTransaction(); origSortedItems.copyElements(copyStartIndex, i, nonOverlappingItems); copyStartIndex = i + 1; } @@ -380,8 +382,7 @@ replaceItems = new GapList<>(origItemsSize); for (i = 0; i < origItemsSize; i++) { CaretItem caretItem = origItems.get(i); - if (caretItem.isTransactionMarkRemoved()) { - caretItem.clearTransactionMarkRemoved(); + if (caretItem.getAndClearRemovedInTransaction()) { if (extraRemovedItems == null) { extraRemovedItems = new GapList<>(); } @@ -394,21 +395,21 @@ } } - GapList addRemovedItems(GapList toItems) { + GapList allRemovedItems() { int removeSize = modEndIndex - modIndex; int extraRemovedSize = (extraRemovedItems != null) ? extraRemovedItems.size() : 0; if (removeSize + extraRemovedSize > 0) { - if (toItems == null) { - toItems = new GapList<>(removeSize + extraRemovedSize); - } - if (removeSize > 0) { - toItems.addAll(origCaretItems, modIndex, removeSize); - } - if (extraRemovedSize > 0) { - toItems.addAll(extraRemovedItems); + if (allRemovedItems == null) { + allRemovedItems = new GapList<>(removeSize + extraRemovedSize); + if (removeSize > 0) { + allRemovedItems.addAll(origCaretItems, modIndex, removeSize); + } + if (extraRemovedSize > 0) { + allRemovedItems.addAll(extraRemovedItems); + } } } - return toItems; + return allRemovedItems; } GapList addUpdateVisualBoundsItems(GapList toItems) { @@ -416,8 +417,7 @@ int size = items.size(); for (int i = 0; i < size; i++) { CaretItem caretItem = items.get(i); - if (caretItem.isUpdateVisualBounds()) { - caretItem.clearUpdateVisualBounds(); + if (caretItem.getAndClearUpdateCaretBounds()) { if (toItems == null) { toItems = new GapList<>(); } --- a/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaret.java +++ a/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaret.java @@ -54,6 +54,7 @@ import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; +import java.awt.Shape; import java.awt.Stroke; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.DataFlavor; @@ -128,7 +129,6 @@ import org.netbeans.modules.editor.lib2.RectangularSelectionUtils; import org.netbeans.modules.editor.lib2.actions.EditorActionUtilities; import org.netbeans.modules.editor.lib2.highlighting.CaretOverwriteModeHighlighting; -import org.netbeans.modules.editor.lib2.view.DocumentView; import org.netbeans.modules.editor.lib2.view.LockedViewHierarchy; import org.netbeans.modules.editor.lib2.view.ViewHierarchy; import org.netbeans.modules.editor.lib2.view.ViewHierarchyEvent; @@ -164,6 +164,8 @@ // -J-Dorg.netbeans.editor.BaseCaret.level=FINEST private static final Logger LOG = Logger.getLogger(EditorCaret.class.getName()); + static final long serialVersionUID = 0L; + static { RectangularSelectionCaretAccessor.register(new RectangularSelectionCaretAccessor() { @Override @@ -183,8 +185,6 @@ }); } - static final long serialVersionUID = 0L; - /** * Non-empty list of individual carets in the order they were created. * At least one item is always present. @@ -236,10 +236,11 @@ /** * Whether blinking caret is currently visible on the screen. *
- * This changes from true to false after each tick of a timer - * (assuming visible == true). + * This changes from true to false and back as caret(s) blink. + *
+ * If false then original locations do not need to be repainted. */ - private boolean blinkVisible; + private boolean showing; /** * Determine if a possible selection would be displayed or not. @@ -254,8 +255,32 @@ private MouseState mouseState = MouseState.DEFAULT; - /** Timer used for blinking the caret */ - private Timer flasher; + /** + * Default delay for caret blinking if the system is responsive enough. + * Zero if caret blinking is disabled. + */ + private int blinkDefaultDelay; + + /** + * Currently used delay according to system responsiveness. + */ + private int blinkCurrentDelay; + + /** + * Last time when blink timer action was invoked or zero if the blinking delay + * should not be examined upon next timer action invocation. + * If there are too many carets being painted and the system becomes busy + * the blinkCurrentDelay may be increased to lower the blinking frequency + * and allow the system to be responsive. + */ + private long lastBlinkTime; + + /** + * Carets blinking timer. + */ + private Timer blinkTimer; + + private ActionListener weakTimerListener; private Action selectWordAction; private Action selectLineAction; @@ -269,18 +294,6 @@ private CaretTransaction activeTransaction; /** - * Items from previous transaction(s) that need their visual rectangle - * to get repainted to clear the previous caret representation visually. - */ - private GapList pendingRepaintRemovedItemsList; - - /** - * Items from previous transaction(s) that need their visual bounds - * to be recomputed and caret to be repainted then. - */ - private GapList pendingUpdateVisualBoundsItemsList; - - /** * Caret item to which the view should scroll or null for no scrolling. */ private CaretItem scrollToItem; @@ -381,6 +394,7 @@ * If there is a selection, the mark will not be the same as the dot. * @return mark offset >=0 */ + @Override public int getMark() { return getLastCaret().getMark(); @@ -606,7 +620,7 @@ * or no document installed in the text component. *
* Note that adding a new caret to offset where another caret is already located may lead - * to its immediate removal. + * or no document installed in the text component. */ public int addCaret(@NonNull Position dotPos, @NonNull Position markPos) { return runTransaction(CaretTransaction.RemoveType.NO_REMOVE, 0, @@ -749,9 +763,9 @@ } } synchronized (listenerList) { - if (flasher != null) { + if (blinkTimer != null) { if (this.visible) { - flasher.stop(); + blinkTimer.stop(); } if (LOG.isLoggable(Level.FINER)) { LOG.finer((visible ? "Starting" : "Stopping") + // NOI18N @@ -759,9 +773,9 @@ } this.visible = visible; if (visible) { - flasher.start(); + blinkTimer.start(); } else { - flasher.stop(); + blinkTimer.stop(); } } } @@ -811,11 +825,11 @@ Boolean b = (Boolean) c.getClientProperty(EditorUtilities.CARET_OVERWRITE_MODE_PROPERTY); overwriteMode = (b != null) ? b : false; updateOverwriteModeLayer(true); - setBlinkVisible(true); + setShowing(true); // Attempt to assign initial bounds - usually here the component // is not yet added to the component hierarchy. - updateAllCaretsBounds(); + requestUpdateAllCaretsBounds(); if(getLastCaretItem().getCaretBounds() == null) { // For null bounds wait for the component to get resized @@ -848,7 +862,7 @@ } synchronized (listenerList) { - if (flasher != null) { + if (blinkTimer != null) { setBlinkRate(0); } } @@ -867,31 +881,66 @@ @Override public void paint(Graphics g) { JTextComponent c = component; - if (c == null) return; - - // Check whether the caret was moved but the component was not - // validated yet and therefore the caret bounds are still null - // and if so compute the bounds and scroll the view if necessary. - // TODO - could this be done by an extra flag rather than bounds checking?? - CaretItem lastCaret = getLastCaretItem(); - if (getDot() != 0 && lastCaret.getCaretBounds() == null) { - update(true); + if (c == null || !isShowing()) { + return; } - + + // Only paint carets that are part of the clip - use sorted carets + Rectangle clipBounds = g.getClipBounds(); List carets = getSortedCarets(); - for (CaretInfo caretInfo : carets) { // TODO only paint the items in the clipped area - use binary search to located first item - CaretItem caretItem = caretInfo.getCaretItem(); - if (LOG.isLoggable(Level.FINEST)) { - LOG.finest("BaseCaret.paint(): caretBounds=" + caretItem.getCaretBounds() + dumpVisibility() + '\n'); + int low = 0; + int caretsSize = carets.size(); + int high = caretsSize - 1; + while (low <= high) { + int mid = (low + high) >>> 1; + CaretInfo caretInfo = carets.get(mid); + Rectangle midBounds = caretInfo.getCaretItem().getCaretBounds(); + + if (midBounds.y + midBounds.height <= clipBounds.y) { + low = mid + 1; + } else { // No exact match (may be multiple carets on sinle line) + high = mid - 1; } - if (caretItem.getCaretBounds() != null && isVisible() && blinkVisible) { - paintCaret(g, caretItem); + } + + Color origColor = g.getColor(); + try { + g.setColor(c.getCaretColor()); + // Use "low" index (which is higher than "high" at end of binary-search) + for (int i = low; i < caretsSize; i++) { + CaretInfo caretInfo = carets.get(i); + CaretItem caretItem = caretInfo.getCaretItem(); + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("BaseCaret.paint(): caretBounds=" + caretItem.getCaretBounds() + dumpVisibility() + '\n'); + } + if (caretItem.getCaretBounds() != null) { + Rectangle caretBounds = caretItem.getCaretBounds(); + switch (type) { + case THICK_LINE_CARET: + g.fillRect(caretBounds.x, caretBounds.y, this.thickCaretWidth, caretBounds.height - 1); + break; + + case THIN_LINE_CARET: + int upperX = caretItem.getCaretBounds().x; + g.drawLine((int) upperX, caretItem.getCaretBounds().y, caretItem.getCaretBounds().x, + (caretItem.getCaretBounds().y + caretItem.getCaretBounds().height - 1)); + break; + + case BLOCK_CARET: + // Use a CaretOverwriteModeHighlighting layer to paint the caret + break; + + default: + throw new IllegalStateException("Invalid caret type=" + type); + } + } } + + // Possibly service rectangular selection if (rectangularSelection && rsPaintRect != null && g instanceof Graphics2D) { Graphics2D g2d = (Graphics2D) g; Stroke stroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[] {4, 2}, 0); Stroke origStroke = g2d.getStroke(); - Color origColor = g2d.getColor(); try { // Render translucent rectangle Color selColor = c.getSelectionColor(); @@ -913,9 +962,10 @@ } finally { g2d.setStroke(origStroke); - g2d.setColor(origColor); } } + } finally { + g.setColor(origColor); } } @@ -932,21 +982,27 @@ LOG.finer("setBlinkRate(" + rate + ")" + dumpVisibility() + '\n'); // NOI18N } synchronized (listenerList) { - if (flasher == null && rate > 0) { - flasher = new Timer(rate, listenerImpl); + this.blinkDefaultDelay = rate; + this.blinkCurrentDelay = rate; + if (blinkTimer == null && rate > 0) { + blinkTimer = new Timer(rate, null); + blinkTimer.addActionListener(weakTimerListener = WeakListeners.create( + ActionListener.class, listenerImpl, blinkTimer)); } - if (flasher != null) { + if (blinkTimer != null) { if (rate > 0) { - if (flasher.getDelay() != rate) { - flasher.setDelay(rate); + if (blinkTimer.getDelay() != rate) { + blinkTimer.setDelay(rate); } } else { // zero rate - don't blink - flasher.stop(); - flasher.removeActionListener(listenerImpl); - flasher = null; - setBlinkVisible(true); + blinkTimer.stop(); + if (weakTimerListener != null) { + blinkTimer.removeActionListener(weakTimerListener); + } + blinkTimer = null; + setShowing(true); if (LOG.isLoggable(Level.FINER)){ - LOG.finer("Zero blink rate - no blinking. flasher=null; blinkVisible=true"); // NOI18N + LOG.finer("Zero blink rate - no blinking. blinkTimer=null; showing=true"); // NOI18N } } } @@ -956,7 +1012,7 @@ @Override public int getBlinkRate() { synchronized (listenerList) { - return (flasher != null) ? flasher.getDelay() : 0; + return blinkDefaultDelay; } } @@ -1122,32 +1178,34 @@ synchronized (listenerList) { GapList replaceItems = activeTransaction.getReplaceItems(); if (replaceItems != null) { + caretItems = replaceItems; diffCount = replaceItems.size() - caretItems.size(); - caretItems = replaceItems; - sortedCaretItems = activeTransaction.getSortedCaretItems(); for (CaretItem caretItem : caretItems) { - if (caretItem.isInfoObsolete()) { - caretItem.clearInfoObsolete(); + if (caretItem.getAndClearInfoObsolete()) { caretItem.clearInfo(); } } - assert (sortedCaretItems != null) : "Null sortedCaretItems! removeType=" + removeType; // NOI18N - } - if (activeTransaction.isAnyChange()) { - caretInfos = null; - sortedCaretInfos = null; + sortedCaretItems = activeTransaction.getSortedCaretItems(); + assert (sortedCaretItems != null) : "Null sortedCaretItems! removeType=" + removeType; // NOI18N } } - pendingRepaintRemovedItemsList = activeTransaction. - addRemovedItems(pendingRepaintRemovedItemsList); - pendingUpdateVisualBoundsItemsList = activeTransaction. - addUpdateVisualBoundsItems(pendingUpdateVisualBoundsItemsList); - if (pendingUpdateVisualBoundsItemsList != null || pendingRepaintRemovedItemsList != null) { - // For now clear the lists and use old way TODO update to selective updating and rendering + if (activeTransaction.isAnyChange()) { + caretInfos = null; + sortedCaretInfos = null; + } + // Repaint bounds of removed items + GapList removedItems = activeTransaction.allRemovedItems(); + if (removedItems != null) { + for (CaretItem removedItem : removedItems) { + Rectangle caretBounds = removedItem.getCaretBounds(); + if (caretBounds != null) { + c.repaint(caretBounds); + } + } + } + if (activeTransaction.isAnyChange()) { fireStateChanged(); dispatchUpdate(true); - pendingRepaintRemovedItemsList = null; - pendingUpdateVisualBoundsItemsList = null; } return diffCount; } finally { @@ -1174,38 +1232,18 @@ } } - private void moveDotCaret(int offset, CaretItem caret) throws IllegalStateException { - JTextComponent c = component; - AbstractDocument doc; - if (c != null && (doc = activeDoc) != null) { - if (offset >= 0 && offset <= doc.getLength()) { - doc.readLock(); - try { - int oldCaretPos = caret.getDot(); - if (offset == oldCaretPos) { // no change - return; - } - caret.setDotPos(doc.createPosition(offset)); - // Selection highlighting should be handled automatically by highlighting layers - if (rectangularSelection) { - Rectangle r = c.modelToView(offset); - if (rsDotRect != null) { - rsDotRect.y = r.y; - rsDotRect.height = r.height; - } else { - rsDotRect = r; - } - updateRectangularSelectionPaintRect(); - } - } catch (BadLocationException e) { - throw new IllegalStateException(e.toString()); - // position is incorrect - } finally { - doc.readUnlock(); + private void updateRectangularSelectionDotRect() { // Assumes update caret bounds of getLastCaretItem() + if (rectangularSelection) { + Rectangle caretBounds = getLastCaretItem().getCaretBounds(); + if (caretBounds != null) { + if (rsDotRect != null) { + rsDotRect.y = caretBounds.y; + rsDotRect.height = caretBounds.height; + } else { + rsDotRect = caretBounds; } } - fireStateChanged(); - dispatchUpdate(true); + updateRectangularSelectionPaintRect(); } } @@ -1235,7 +1273,7 @@ }; // Always fire in EDT - if (inAtomicUnlock) { // Cannot fire within atomic lock9 + if (inAtomicUnlock) { // Cannot fire within atomic lock SwingUtilities.invokeLater(runnable); } else { ViewUtils.runInEDT(runnable); @@ -1333,79 +1371,24 @@ } /** - * Assign new caret bounds into caretBounds variable. - * - * @return true if the new caret bounds were successfully computed - * and assigned or false otherwise. + * Schedule recomputation of visual bounds of all carets. */ - private boolean updateAllCaretsBounds() { + private void requestUpdateAllCaretsBounds() { JTextComponent c = component; AbstractDocument doc; - boolean ret = false; if (c != null && (doc = activeDoc) != null) { doc.readLock(); try { List sortedCarets = getSortedCarets(); for (CaretInfo caret : sortedCarets) { - ret |= updateRealCaretBounds(caret.getCaretItem(), doc, c); + caret.getCaretItem().markUpdateCaretBounds(); } } finally { doc.readUnlock(); } } - return ret; } - private boolean updateCaretBounds(CaretItem caret) { - JTextComponent c = component; - boolean ret = false; - AbstractDocument doc; - if (c != null && (doc = activeDoc) != null) { - doc.readLock(); - try { - ret = updateRealCaretBounds(caret, doc, c); - } finally { - doc.readUnlock(); - } - } - return ret; - } - - private boolean updateRealCaretBounds(CaretItem caret, Document doc, JTextComponent c) { - Position dotPos = caret.getDotPosition(); - int offset = dotPos == null? 0 : dotPos.getOffset(); - if (offset > doc.getLength()) { - offset = doc.getLength(); - } - Rectangle newCaretBounds; - try { - DocumentView docView = DocumentView.get(c); - if (docView != null) { - // docView.syncViewsRebuild(); // Make sure pending views changes are resolved - } - newCaretBounds = c.getUI().modelToView( - c, offset, Position.Bias.Forward); - // [TODO] Temporary fix - impl should remember real bounds computed by paintCustomCaret() - if (newCaretBounds != null) { - newCaretBounds.width = Math.max(newCaretBounds.width, 2); - } - - } catch (BadLocationException e) { - - newCaretBounds = null; - } - if (newCaretBounds != null) { - if (LOG.isLoggable(Level.FINE)) { - LOG.log(Level.FINE, "updateCaretBounds: old={0}, new={1}, offset={2}", - new Object[]{caret.getCaretBounds(), newCaretBounds, offset}); //NOI18N - } - caret.setCaretBounds(newCaretBounds); - return true; - } else { - return false; - } - } - private void modelChanged(Document oldDoc, Document newDoc) { if (oldDoc != null) { // ideally the oldDoc param shouldn't exist and only listenDoc should be used @@ -1450,32 +1433,6 @@ } } - private void paintCaret(Graphics g, CaretItem caret) { - JTextComponent c = component; - if (c != null) { - g.setColor(c.getCaretColor()); - Rectangle caretBounds = caret.getCaretBounds(); - switch (type) { - case THICK_LINE_CARET: - g.fillRect(caretBounds.x, caretBounds.y, this.thickCaretWidth, caretBounds.height - 1); - break; - - case THIN_LINE_CARET: - int upperX = caret.getCaretBounds().x; - g.drawLine((int) upperX, caret.getCaretBounds().y, caret.getCaretBounds().x, - (caret.getCaretBounds().y + caret.getCaretBounds().height - 1)); - break; - - case BLOCK_CARET: - // Use a CaretOverwriteModeHighlighting layer to paint the caret - break; - - default: - throw new IllegalStateException("Invalid caret type=" + type); - } - } - } - void dispatchUpdate() { JTextComponent c = component; if (c != null) { @@ -1525,111 +1482,53 @@ } Document doc = c.getDocument(); if (doc != null) { - List sortedCarets = getSortedCarets(); - for (CaretInfo caret : sortedCarets) { - CaretItem caretItem = caret.getCaretItem(); - Rectangle oldCaretBounds = caretItem.getCaretBounds(); // no need to deep copy - if (oldCaretBounds != null) { - c.repaint(oldCaretBounds); - } - - // note - the order is important ! caret bounds must be updated even if the fold flag is true. - if (updateCaretBounds(caretItem) || updateAfterFoldHierarchyChange) { - Rectangle scrollBounds = new Rectangle(caretItem.getCaretBounds()); - - // Optimization to avoid extra repaint: - // If the caret bounds were not yet assigned then attempt - // to scroll the window so that there is an extra vertical space - // for the possible horizontal scrollbar that may appear - // if the line-view creation process finds line-view that - // is too wide and so the horizontal scrollbar will appear - // consuming an extra vertical space at the bottom. - if (oldCaretBounds == null) { - Component viewport = c.getParent(); - if (viewport instanceof JViewport) { - Component scrollPane = viewport.getParent(); - if (scrollPane instanceof JScrollPane) { - JScrollBar hScrollBar = ((JScrollPane) scrollPane).getHorizontalScrollBar(); - if (hScrollBar != null) { - int hScrollBarHeight = hScrollBar.getPreferredSize().height; - Dimension extentSize = ((JViewport) viewport).getExtentSize(); - // If the extent size is high enough then extend - // the scroll region by extra vertical space - if (extentSize.height >= caretItem.getCaretBounds().height + hScrollBarHeight) { - scrollBounds.height += hScrollBarHeight; + LockedViewHierarchy lvh = ViewHierarchy.get(c).lock(); + try { + CaretItem lastCaretItem = getLastCaretItem(); + List sortedCarets = getSortedCarets(); + for (CaretInfo caret : sortedCarets) { + CaretItem caretItem = caret.getCaretItem(); + if (caretItem.getAndClearUpdateCaretBounds()) { + Shape caretShape = lvh.modelToView(caretItem.getDot(), Position.Bias.Forward); + if (caretShape != null) { + Rectangle newCaretBounds = caretShape.getBounds(); + Rectangle oldCaretBounds = caretItem.setCaretBoundsWithRepaint(newCaretBounds, c); + // Only scroll the view for the LAST caret to be visible + if (scrollViewToCaret && caretItem == lastCaretItem) { + Rectangle scrollBounds = newCaretBounds; // Must possibly be cloned upon change + + // For null old bounds (likely at begining of component displayment) ensure that a possible + // horizontal scrollbar would hide the caret so enlarge the scroll bounds by hscrollbar height. + if (oldCaretBounds == null) { + Component viewport = c.getParent(); + if (viewport instanceof JViewport) { + Component scrollPane = viewport.getParent(); + if (scrollPane instanceof JScrollPane) { + JScrollBar hScrollBar = ((JScrollPane) scrollPane).getHorizontalScrollBar(); + if (hScrollBar != null) { + int hScrollBarHeight = hScrollBar.getPreferredSize().height; + Dimension extentSize = ((JViewport) viewport).getExtentSize(); + // If the extent size is high enough then extend + // the scroll region by extra vertical space + if (extentSize.height >= caretItem.getCaretBounds().height + hScrollBarHeight) { + scrollBounds = new Rectangle(scrollBounds); // Clone + scrollBounds.height += hScrollBarHeight; + } + } + } } } + + if (LOG.isLoggable(Level.FINER)) { + LOG.finer("Scrolling to: " + scrollBounds); + } + c.scrollRectToVisible(scrollBounds); } } } - - Rectangle visibleBounds = c.getVisibleRect(); - - // If folds have changed attempt to scroll the view so that - // relative caret's visual position gets retained - // (the absolute position will change because of collapsed/expanded folds). - boolean doScroll = scrollViewToCaret; - boolean explicit = false; - if (oldCaretBounds != null && (!scrollViewToCaret || updateAfterFoldHierarchyChange)) { - int oldRelY = oldCaretBounds.y - visibleBounds.y; - // Only fix if the caret is within visible bounds and the new x or y coord differs from the old one - if (LOG.isLoggable(Level.FINER)) { - LOG.log(Level.FINER, "oldCaretBounds: {0}, visibleBounds: {1}, caretBounds: {2}", - new Object[]{oldCaretBounds, visibleBounds, caretItem.getCaretBounds()}); - } - if (oldRelY >= 0 && oldRelY < visibleBounds.height - && (oldCaretBounds.y != caretItem.getCaretBounds().y || oldCaretBounds.x != caretItem.getCaretBounds().x)) { - doScroll = true; // Perform explicit scrolling - explicit = true; - int oldRelX = oldCaretBounds.x - visibleBounds.x; - // Do not retain the horizontal caret bounds by scrolling - // since many modifications do not explicitly say that they are typing modifications - // and this would cause problems like #176268 -// scrollBounds.x = Math.max(caretBounds.x - oldRelX, 0); - scrollBounds.y = Math.max(caretItem.getCaretBounds().y - oldRelY, 0); -// scrollBounds.width = visibleBounds.width; - scrollBounds.height = visibleBounds.height; - } - } - - // Historically the caret is expected to appear - // in the middle of the window if setDot() gets called - // e.g. by double-clicking in Navigator. - // If the caret bounds are more than a caret height below the present - // visible view bounds (or above the view bounds) - // then scroll the window so that the caret is in the middle - // of the visible window to see the context around the caret. - // This should work fine with PgUp/Down because these - // scroll the view explicitly. - if (scrollViewToCaret - && !explicit - && // #219580: if the preceding if-block computed new scrollBounds, it cannot be offset yet more - /* # 70915 !updateAfterFoldHierarchyChange && */ (caretItem.getCaretBounds().y > visibleBounds.y + visibleBounds.height + caretItem.getCaretBounds().height - || caretItem.getCaretBounds().y + caretItem.getCaretBounds().height < visibleBounds.y - caretItem.getCaretBounds().height)) { - // Scroll into the middle - scrollBounds.y -= (visibleBounds.height - caretItem.getCaretBounds().height) / 2; - scrollBounds.height = visibleBounds.height; - } - if (LOG.isLoggable(Level.FINER)) { - LOG.finer("Resetting fold flag, current: " + updateAfterFoldHierarchyChange); - } - updateAfterFoldHierarchyChange = false; - - // Ensure that the viewport will be scrolled either to make the caret visible - // or to retain cart's relative visual position against the begining of the viewport's visible rectangle. - if (doScroll) { - if (LOG.isLoggable(Level.FINER)) { - LOG.finer("Scrolling to: " + scrollBounds); - } - c.scrollRectToVisible(scrollBounds); - if (!c.getVisibleRect().intersects(scrollBounds)) { - // HACK: see #219580: for some reason, the scrollRectToVisible may fail. - c.scrollRectToVisible(scrollBounds); - } - } - resetBlink(); - c.repaint(caretItem.getCaretBounds()); } + } finally { + lvh.unlock(); } } } @@ -1747,21 +1646,22 @@ } private String dumpVisibility() { - return "visible=" + isVisible() + ", blinkVisible=" + blinkVisible; + return "visible=" + isVisible() + ", showing=" + showing; } /*private*/ void resetBlink() { boolean visible = isVisible(); synchronized (listenerList) { - if (flasher != null) { - flasher.stop(); - setBlinkVisible(true); + if (blinkTimer != null) { + blinkTimer.stop(); + setShowing(true); + lastBlinkTime = System.currentTimeMillis(); if (visible) { if (LOG.isLoggable(Level.FINER)){ LOG.finer("Reset blinking (caret already visible)" + // NOI18N " - starting the caret blinking timer: " + dumpVisibility() + '\n'); // NOI18N } - flasher.start(); + blinkTimer.start(); } else { if (LOG.isLoggable(Level.FINER)){ LOG.finer("Reset blinking (caret not visible)" + // NOI18N @@ -1771,10 +1671,27 @@ } } } - - /*private*/ void setBlinkVisible(boolean blinkVisible) { + + /** + * Return true if the caret is visible and it should currently be painted on the screen. + * This flag is being toggled by caret blinking timer. + * + * @return true if caret is currently painted on the screen. + */ + boolean isShowing() { + return showing; + } + + /** + * Set whether the carets should physically be showing on screen. + *
+ * It's set to from true to false and vice versa by caret blinking timer. + * + * @param showing true if the carets should be showing on screen or false if not. + */ + /*private*/ void setShowing(boolean showing) { synchronized (listenerList) { - this.blinkVisible = blinkVisible; + this.showing = showing; } updateOverwriteModeLayer(false); } @@ -1785,7 +1702,7 @@ CaretOverwriteModeHighlighting overwriteModeHighlighting = (CaretOverwriteModeHighlighting) c.getClientProperty(CaretOverwriteModeHighlighting.class); if (overwriteModeHighlighting != null) { - overwriteModeHighlighting.setVisible(visible && blinkVisible); + overwriteModeHighlighting.setVisible(visible && showing); } } } @@ -1944,15 +1861,6 @@ return false; } - private void refresh() { - updateType(); - SwingUtilities.invokeLater(new Runnable() { - public @Override void run() { - updateAllCaretsBounds(); // the line height etc. may have change - } - }); - } - private static String logMouseEvent(MouseEvent evt) { return "x=" + evt.getX() + ", y=" + evt.getY() + ", clicks=" + evt.getClickCount() //NOI18N + ", component=" + s2s(evt.getComponent()) //NOI18N @@ -1964,13 +1872,35 @@ } private final class ListenerImpl extends ComponentAdapter - implements DocumentListener, AtomicLockListener, MouseListener, MouseMotionListener, FocusListener, ViewHierarchyListener, - PropertyChangeListener, ActionListener, PreferenceChangeListener, KeyListener + implements ActionListener, DocumentListener, AtomicLockListener, MouseListener, + MouseMotionListener, FocusListener, ViewHierarchyListener, + PropertyChangeListener, PreferenceChangeListener, KeyListener { ListenerImpl() { } + @Override + public void actionPerformed(ActionEvent e) { + JTextComponent c = component; + if (c != null) { + setShowing(!showing); + List sortedCarets = getSortedCarets(); // TODO only repaint carets showing on screen + for (CaretInfo caret : sortedCarets) { + CaretItem caretItem = caret.getCaretItem(); + if (caretItem.getCaretBounds() != null) { + Rectangle repaintRect = caretItem.getCaretBounds(); + c.repaint(repaintRect); + } + } + // Check if the system is responsive enough to blink at the current blink rate + long tm = System.currentTimeMillis(); + if (tm - (blinkCurrentDelay + (blinkCurrentDelay >> 2)) > 0L) { + + } + } + } + public @Override void preferenceChange(PreferenceChangeEvent evt) { String setingName = evt == null ? null : evt.getKey(); if (setingName == null || SimpleValueNames.CARET_BLINK_RATE.equals(setingName)) { @@ -1979,7 +1909,6 @@ rate = EditorPreferencesDefaults.defaultCaretBlinkRate; } setBlinkRate(rate); - refresh(); } } @@ -2051,25 +1980,6 @@ } } - // ActionListener methods - /** - * Fired when blink timer fires - */ - public @Override void actionPerformed(ActionEvent evt) { - JTextComponent c = component; - if (c != null) { - setBlinkVisible(!blinkVisible); - List sortedCarets = getSortedCarets(); // TODO only repaint carets showing on screen - for (CaretInfo caret : sortedCarets) { - CaretItem caretItem = caret.getCaretItem(); - if (caretItem.getCaretBounds() != null) { - Rectangle repaintRect = caretItem.getCaretBounds(); - c.repaint(repaintRect); - } - } - } - } - // DocumentListener methods public @Override void insertUpdate(DocumentEvent evt) { JTextComponent c = component; @@ -2078,8 +1988,8 @@ int offset = evt.getOffset(); final int endOffset = offset + evt.getLength(); if (offset == 0) { - // Manually shift carets at offset zero - runTransaction(CaretTransaction.RemoveType.DOCUMENT_INSERT_ZERO_OFFSET, endOffset, null, null); + // Manually shift carets at offset zero - do this always even when inside atomic lock + runTransaction(CaretTransaction.RemoveType.DOCUMENT_INSERT_ZERO_OFFSET, 0, null, null); } // [TODO] proper undo solution modified = true; @@ -2241,7 +2151,7 @@ case CHAR_SELECTION: if (evt.isAltDown() && evt.isShiftDown()) { - moveDotCaret(offset, getLastCaretItem()); + moveDot(offset); } else { moveDot(offset); // Will do setDot() if no selection adjustRectangularSelectionMouseX(evt.getX(), evt.getY()); // also fires state change @@ -2405,7 +2315,7 @@ case CHAR_SELECTION: if (evt.isAltDown() && evt.isShiftDown()) { - moveDotCaret(offset, getLastCaretItem()); + moveDot(offset); } else { moveDot(offset); adjustRectangularSelectionMouseX(evt.getX(), evt.getY()); @@ -2566,8 +2476,34 @@ @Override public void keyReleased(KeyEvent e) { } + + } // End of ListenerImpl class + + private final class Blinker implements Runnable { - } // End of ListenerImpl class + /** + * Fired when blink task gets executed. + */ + @Override + public void run() { + JTextComponent c = component; + if (c != null) { + setShowing(!showing); + // Repaint all carets + List sortedCarets = getSortedCarets(); // TODO only repaint carets showing on screen + for (CaretInfo caret : sortedCarets) { + CaretItem caretItem = caret.getCaretItem(); + if (caretItem.getCaretBounds() != null) { + Rectangle repaintRect = caretItem.getCaretBounds(); + if (repaintRect != null) { + c.repaint(repaintRect); + } + } + } + } + } + + } private enum CaretType {