| /* |
| * 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.editor; |
| |
| import java.awt.AlphaComposite; |
| import java.awt.BasicStroke; |
| import java.awt.Composite; |
| import java.awt.Container; |
| import java.awt.Dimension; |
| import java.awt.Rectangle; |
| import java.awt.Graphics; |
| import java.awt.Color; |
| import java.awt.Font; |
| import java.awt.Point; |
| import java.awt.Component; |
| import java.awt.Cursor; |
| import java.awt.Graphics2D; |
| import java.awt.Stroke; |
| import java.awt.datatransfer.Clipboard; |
| import java.awt.datatransfer.DataFlavor; |
| import java.awt.datatransfer.Transferable; |
| import java.awt.datatransfer.UnsupportedFlavorException; |
| import java.awt.event.ComponentEvent; |
| import java.awt.event.ComponentAdapter; |
| import java.awt.event.ComponentListener; |
| import java.awt.event.MouseListener; |
| import java.awt.event.MouseEvent; |
| import java.awt.event.MouseMotionListener; |
| import java.awt.event.ActionListener; |
| import java.awt.event.ActionEvent; |
| import java.awt.event.FocusListener; |
| import java.awt.event.FocusEvent; |
| import java.awt.event.InputEvent; |
| import java.beans.PropertyChangeListener; |
| import java.beans.PropertyChangeEvent; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.Callable; |
| import java.util.function.IntUnaryOperator; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import java.util.prefs.PreferenceChangeEvent; |
| import java.util.prefs.PreferenceChangeListener; |
| import java.util.prefs.Preferences; |
| |
| import javax.swing.Action; |
| import javax.swing.JComponent; |
| import javax.swing.JScrollBar; |
| import javax.swing.JScrollPane; |
| import javax.swing.JViewport; |
| import javax.swing.Timer; |
| import javax.swing.SwingUtilities; |
| import javax.swing.TransferHandler; |
| import javax.swing.text.BadLocationException; |
| import javax.swing.text.Document; |
| import javax.swing.text.Element; |
| import javax.swing.text.JTextComponent; |
| import javax.swing.text.Caret; |
| import javax.swing.event.ChangeListener; |
| import javax.swing.event.ChangeEvent; |
| import javax.swing.event.DocumentListener; |
| import javax.swing.event.DocumentEvent; |
| import javax.swing.event.EventListenerList; |
| import javax.swing.text.AttributeSet; |
| import javax.swing.text.Position; |
| import javax.swing.text.StyleConstants; |
| |
| import org.netbeans.api.editor.fold.FoldHierarchyEvent; |
| import org.netbeans.api.editor.fold.FoldHierarchyListener; |
| import org.netbeans.api.editor.mimelookup.MimeLookup; |
| import org.netbeans.api.editor.settings.FontColorNames; |
| import org.netbeans.api.editor.settings.FontColorSettings; |
| import org.netbeans.api.editor.settings.SimpleValueNames; |
| import org.netbeans.lib.editor.util.swing.DocumentUtilities; |
| import org.netbeans.lib.editor.util.swing.DocumentListenerPriority; |
| import org.netbeans.modules.editor.lib2.EditorPreferencesDefaults; |
| import org.netbeans.modules.editor.lib.SettingsConversions; |
| import org.netbeans.modules.editor.lib2.RectangularSelectionTransferHandler; |
| import org.netbeans.modules.editor.lib2.RectangularSelectionUtils; |
| import org.netbeans.modules.editor.lib2.view.*; |
| import org.openide.util.Exceptions; |
| import org.openide.util.WeakListeners; |
| |
| /** |
| * Caret implementation |
| * |
| * @author Miloslav Metelka |
| * @version 1.00 |
| */ |
| |
| @SuppressWarnings("ClassWithMultipleLoggers") |
| public class BaseCaret implements Caret, |
| MouseListener, MouseMotionListener, PropertyChangeListener, |
| DocumentListener, ActionListener, |
| AtomicLockListener, FoldHierarchyListener { |
| |
| /** Caret type representing block covering current character */ |
| public static final String BLOCK_CARET = EditorPreferencesDefaults.BLOCK_CARET; // NOI18N |
| |
| /** One dot thin line compatible with Swing default caret */ |
| public static final String THIN_LINE_CARET = EditorPreferencesDefaults.THIN_LINE_CARET; // NOI18N |
| |
| /** @since 1.23 */ |
| public static final String THICK_LINE_CARET = EditorPreferencesDefaults.THICK_LINE_CARET; // NOI18N |
| |
| /** Default caret type */ |
| public static final String LINE_CARET = "line-caret"; // NOI18N |
| |
| /** Boolean property defining whether selection is being rectangular in a particular text component. */ |
| private static final String RECTANGULAR_SELECTION_PROPERTY = "rectangular-selection"; // NOI18N |
| |
| /** List of positions (with even size) defining regions of rectangular selection. Maintained by BaseCaret. */ |
| private static final String RECTANGULAR_SELECTION_REGIONS_PROPERTY = "rectangular-selection-regions"; // NOI18N |
| |
| // -J-Dorg.netbeans.editor.BaseCaret.level=FINEST |
| private static final Logger LOG = Logger.getLogger(BaseCaret.class.getName()); |
| |
| // -J-Dorg.netbeans.editor.BaseCaret.EDT.level=FINE - check that setDot() and other operations in EDT only |
| private static final Logger LOG_EDT = Logger.getLogger(BaseCaret.class.getName() + ".EDT"); |
| |
| static { |
| // Compatibility debugging flags mapping to logger levels |
| if (Boolean.getBoolean("netbeans.debug.editor.caret.focus") && LOG.getLevel().intValue() < Level.FINE.intValue()) |
| LOG.setLevel(Level.FINE); |
| if (Boolean.getBoolean("netbeans.debug.editor.caret.focus.extra") && LOG.getLevel().intValue() < Level.FINER.intValue()) |
| LOG.setLevel(Level.FINER); |
| } |
| |
| /** |
| * Implementation of various listeners. |
| */ |
| private final ListenerImpl listenerImpl; |
| |
| /** |
| * Present bounds of the caret. This rectangle needs to be repainted |
| * prior the caret gets repainted elsewhere. |
| */ |
| private volatile Rectangle caretBounds; |
| |
| /** Component this caret is bound to */ |
| protected JTextComponent component; |
| |
| /** Position of the caret on the screen. This helps to compute |
| * caret position on the next after jump. |
| */ |
| Point magicCaretPosition; |
| |
| /** Position of caret. */ |
| Position caretPos; |
| |
| /** Position of selection mark. */ |
| Position markPos; |
| |
| /** Is the caret visible */ |
| boolean caretVisible; |
| |
| /** Whether blinking caret is currently visible. |
| * <code>caretVisible</code> must be also true in order to paint the caret. |
| */ |
| boolean blinkVisible; |
| |
| /** Is the selection currently visible? */ |
| boolean selectionVisible; |
| |
| /** Listeners */ |
| protected EventListenerList listenerList = new EventListenerList(); |
| |
| /** Timer used for blinking the caret */ |
| protected Timer flasher; |
| |
| /** Type of the caret */ |
| String type; |
| |
| /** Width of thick caret */ |
| int width; |
| |
| /** Is the caret italic for italic fonts */ |
| boolean italic; |
| |
| private int xPoints[] = new int[4]; |
| private int yPoints[] = new int[4]; |
| private Action selectWordAction; |
| private Action selectLineAction; |
| |
| /** Change event. Only one instance needed because it has only source property */ |
| protected ChangeEvent changeEvent; |
| |
| /** Dot array of one character under caret */ |
| protected char dotChar[] = {' '}; |
| |
| private boolean overwriteMode; |
| |
| /** Remembering document on which caret listens avoids |
| * duplicate listener addition to SwingPropertyChangeSupport |
| * due to the bug 4200280 |
| */ |
| private BaseDocument listenDoc; |
| |
| /** Font of the text underlying the caret. It can be used |
| * in caret painting. |
| */ |
| protected Font afterCaretFont; |
| |
| /** Font of the text right before the caret */ |
| protected Font beforeCaretFont; |
| |
| /** Foreground color of the text underlying the caret. It can be used |
| * in caret painting. |
| */ |
| protected Color textForeColor; |
| |
| /** Background color of the text underlying the caret. It can be used |
| * in caret painting. |
| */ |
| protected Color textBackColor; |
| |
| /** Whether the text is being modified under atomic lock. |
| * If so just one caret change is fired at the end of all modifications. |
| */ |
| private transient boolean inAtomicLock = false; |
| private transient boolean inAtomicUnlock = false; |
| |
| /** Helps to check whether there was modification performed |
| * and so the caret change needs to be fired. |
| */ |
| private transient boolean modified; |
| |
| /** Whether there was an undo done in the modification and the offset of the modification */ |
| private transient int undoOffset = -1; |
| |
| static final long serialVersionUID =-9113841520331402768L; |
| |
| /** |
| * Set to true once the folds have changed. The caret should retain |
| * its relative visual position on the screen. |
| */ |
| private boolean updateAfterFoldHierarchyChange; |
| |
| /** |
| * Whether at least one typing change occurred during possibly several atomic operations. |
| */ |
| private boolean typingModificationOccurred; |
| |
| private Preferences prefs = null; |
| private final PreferenceChangeListener prefsListener = new PreferenceChangeListener() { |
| public @Override void preferenceChange(PreferenceChangeEvent evt) { |
| String setingName = evt == null ? null : evt.getKey(); |
| if (setingName == null || SimpleValueNames.CARET_BLINK_RATE.equals(setingName)) { |
| SettingsConversions.callSettingsChange(BaseCaret.this); |
| int rate = prefs.getInt(SimpleValueNames.CARET_BLINK_RATE, -1); |
| if (rate == -1) { |
| JTextComponent c = component; |
| Integer rateI = c == null ? null : (Integer) c.getClientProperty(BaseTextUI.PROP_DEFAULT_CARET_BLINK_RATE); |
| rate = rateI != null ? rateI : EditorPreferencesDefaults.defaultCaretBlinkRate; |
| } |
| setBlinkRate(rate); |
| refresh(); |
| } |
| } |
| }; |
| private PreferenceChangeListener weakPrefsListener = null; |
| |
| private boolean caretUpdatePending; |
| |
| private MouseState mouseState = MouseState.DEFAULT; |
| |
| /** |
| * Minimum selection start for word and line selections. |
| * This helps to ensure that when extending word (or line) selections |
| * the selection will always include at least the initially selected word (or line). |
| */ |
| private int minSelectionStartOffset; |
| |
| private int minSelectionEndOffset; |
| |
| private boolean rectangularSelection; |
| |
| /** |
| * Rectangle that corresponds to model2View of current point of selection. |
| */ |
| private Rectangle rsDotRect; |
| |
| /** |
| * Rectangle that corresponds to model2View of beginning of selection. |
| */ |
| private Rectangle rsMarkRect; |
| |
| /** |
| * Rectangle marking rectangular selection. |
| */ |
| private Rectangle rsPaintRect; |
| |
| /** |
| * List of start-pos and end-pos pairs that denote rectangular selection |
| * on the selected lines. |
| */ |
| private List<Position> rsRegions; |
| |
| /** |
| * Used for showing the default cursor instead of the text cursor when the |
| * mouse is over a block of selected text. |
| * This field is used to prevent repeated calls to component.setCursor() |
| * with the same cursor. |
| */ |
| private boolean showingTextCursor = true; |
| |
| public BaseCaret() { |
| listenerImpl = new ListenerImpl(); |
| } |
| |
| void updateType() { |
| JTextComponent c = component; |
| if (c != null && prefs != null && !Boolean.TRUE.equals(c.getClientProperty("AsTextField"))) { |
| |
| String newType; |
| int newWidth = 0; |
| boolean newItalic; |
| Color caretColor = Color.black; |
| |
| if (overwriteMode) { |
| newType = prefs.get(SimpleValueNames.CARET_TYPE_OVERWRITE_MODE, EditorPreferencesDefaults.defaultCaretTypeOverwriteMode); |
| newItalic = prefs.getBoolean(SimpleValueNames.CARET_ITALIC_OVERWRITE_MODE, EditorPreferencesDefaults.defaultCaretItalicOverwriteMode); |
| } else { // insert mode |
| newType = prefs.get(SimpleValueNames.CARET_TYPE_INSERT_MODE, EditorPreferencesDefaults.defaultCaretTypeInsertMode); |
| newItalic = prefs.getBoolean(SimpleValueNames.CARET_ITALIC_INSERT_MODE, EditorPreferencesDefaults.defaultCaretItalicInsertMode); |
| newWidth = prefs.getInt(SimpleValueNames.THICK_CARET_WIDTH, EditorPreferencesDefaults.defaultThickCaretWidth); |
| } |
| |
| FontColorSettings fcs = MimeLookup.getLookup(DocumentUtilities.getMimeType(c)).lookup(FontColorSettings.class); |
| if (fcs != null) { |
| if (overwriteMode) { |
| AttributeSet attribs = fcs.getFontColors(FontColorNames.CARET_COLOR_OVERWRITE_MODE); //NOI18N |
| if (attribs != null) { |
| caretColor = (Color) attribs.getAttribute(StyleConstants.Foreground); |
| } |
| } else { |
| AttributeSet attribs = fcs.getFontColors(FontColorNames.CARET_COLOR_INSERT_MODE); //NOI18N |
| if (attribs != null) { |
| caretColor = (Color) attribs.getAttribute(StyleConstants.Foreground); |
| } |
| } |
| } |
| |
| this.type = newType; |
| this.italic = newItalic; |
| this.width = newWidth; |
| c.setCaretColor(caretColor); |
| if (LOG.isLoggable(Level.FINER)) { |
| LOG.finer("Updating caret color:" + caretColor + '\n'); // NOI18N |
| } |
| |
| resetBlink(); |
| dispatchUpdate(false); |
| } |
| } |
| |
| /** |
| * Assign new caret bounds into <code>caretBounds</code> variable. |
| * |
| * @return true if the new caret bounds were successfully computed |
| * and assigned or false otherwise. |
| */ |
| private boolean updateCaretBounds() { |
| final JTextComponent c = component; |
| final boolean[] ret = { false }; |
| if (c != null) { |
| final Document doc = c.getDocument(); |
| doc.render(new Runnable() { |
| @Override |
| public void run() { |
| int offset = getDot(); |
| if (offset > doc.getLength()) { |
| offset = doc.getLength(); |
| } |
| if (doc != null) { |
| CharSequence docText = DocumentUtilities.getText(doc); |
| dotChar[0] = docText.charAt(offset); |
| } |
| 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) { |
| int minwidth = 2; |
| // [NETBEANS-4940] Caret drawing problems over a TAB |
| Object o = component.getClientProperty("CARET_MIN_WIDTH"); |
| if(o instanceof IntUnaryOperator) { |
| minwidth = ((IntUnaryOperator)o).applyAsInt(offset); |
| } |
| newCaretBounds.width = Math.max(newCaretBounds.width, minwidth); |
| } |
| } catch (BadLocationException e) { |
| newCaretBounds = null; |
| Utilities.annotateLoggable(e); |
| } |
| if (newCaretBounds != null) { |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.log(Level.FINE, "updateCaretBounds: old={0}, new={1}, offset={2}", |
| new Object[]{caretBounds, newCaretBounds, offset}); //NOI18N |
| } |
| caretBounds = newCaretBounds; |
| ret[0] = true; |
| } |
| } |
| }); |
| } |
| LOG.log(Level.FINE, "updateCaretBounds: no change, old={0}", caretBounds); //NOI18N |
| return ret[0]; |
| } |
| |
| /** Called when UI is being installed into JTextComponent */ |
| public @Override void install(JTextComponent c) { |
| assert (SwingUtilities.isEventDispatchThread()); // must be done in AWT |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("Installing to " + s2s(c)); //NOI18N |
| } |
| |
| component = c; |
| blinkVisible = true; |
| |
| // Assign dot and mark positions |
| BaseDocument doc = Utilities.getDocument(c); |
| if (doc != null) { |
| modelChanged(null, doc); |
| } |
| |
| // Attempt to assign initial bounds - usually here the component |
| // is not yet added to the component hierarchy. |
| updateCaretBounds(); |
| |
| if (caretBounds == null) { |
| // For null bounds wait for the component to get resized |
| // and attempt to recompute bounds then |
| component.addComponentListener(listenerImpl); |
| } |
| |
| component.addPropertyChangeListener(this); |
| component.addFocusListener(listenerImpl); |
| component.addMouseListener(this); |
| component.addMouseMotionListener(this); |
| ViewHierarchy.get(component).addViewHierarchyListener(listenerImpl); |
| |
| EditorUI editorUI = Utilities.getEditorUI(component); |
| editorUI.addPropertyChangeListener( this ); |
| |
| if (component.hasFocus()) { |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("Component has focus, calling BaseCaret.focusGained(); doc=" // NOI18N |
| + component.getDocument().getProperty(Document.TitleProperty) + '\n'); |
| } |
| listenerImpl.focusGained(null); // emulate focus gained |
| } |
| |
| dispatchUpdate(false); |
| } |
| |
| /** Called when UI is being removed from JTextComponent */ |
| @Override |
| @SuppressWarnings("NestedSynchronizedStatement") |
| public void deinstall(JTextComponent c) { |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("Deinstalling from " + s2s(c)); //NOI18N |
| } |
| |
| component = null; // invalidate |
| caretBounds = null; |
| |
| // No idea why the sync is done the way how it is, but the locks must |
| // always be acquired in the same order otherwise the code will deadlock |
| // sooner or later. See #100734 |
| synchronized (this) { |
| synchronized (listenerImpl) { |
| if (flasher != null) { |
| setBlinkRate(0); |
| } |
| } |
| } |
| |
| c.removeComponentListener(listenerImpl); |
| c.removePropertyChangeListener(this); |
| c.removeFocusListener(listenerImpl); |
| c.removeMouseListener(this); |
| c.removeMouseMotionListener(this); |
| ViewHierarchy.get(c).removeViewHierarchyListener(listenerImpl); |
| |
| |
| EditorUI editorUI = Utilities.getEditorUI(c); |
| editorUI.removePropertyChangeListener(this); |
| |
| modelChanged(listenDoc, null); |
| } |
| |
| protected void modelChanged(BaseDocument oldDoc, BaseDocument newDoc) { |
| if (oldDoc != null) { |
| // ideally the oldDoc param shouldn't exist and only listenDoc should be used |
| assert (oldDoc == listenDoc); |
| |
| DocumentUtilities.removeDocumentListener( |
| oldDoc, this, DocumentListenerPriority.CARET_UPDATE); |
| oldDoc.removeAtomicLockListener(this); |
| |
| caretPos = null; |
| markPos = null; |
| |
| listenDoc = null; |
| if (prefs != null && weakPrefsListener != null) { |
| prefs.removePreferenceChangeListener(weakPrefsListener); |
| } |
| } |
| |
| |
| if (newDoc != null) { |
| |
| DocumentUtilities.addDocumentListener( |
| newDoc, this, DocumentListenerPriority.CARET_UPDATE); |
| listenDoc = newDoc; |
| newDoc.addAtomicLockListener(this); |
| |
| // Leave caretPos and markPos null => offset==0 |
| prefs = MimeLookup.getLookup(DocumentUtilities.getMimeType(newDoc)).lookup(Preferences.class); |
| if (prefs != null) { |
| weakPrefsListener = WeakListeners.create(PreferenceChangeListener.class, prefsListener, prefs); |
| prefs.addPreferenceChangeListener(weakPrefsListener); |
| } |
| |
| Utilities.runInEventDispatchThread( |
| new Runnable() { |
| public @Override void run() { |
| updateType(); |
| } |
| } |
| ); |
| } |
| } |
| |
| /** Renders the caret */ |
| public @Override void paint(Graphics g) { |
| JTextComponent c = component; |
| if (c == null) return; |
| |
| // #70915 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. |
| if (getDot() != 0 && caretBounds == null) { |
| update(true); |
| } |
| if (LOG.isLoggable(Level.FINEST)) { |
| LOG.finest("BaseCaret.paint(): caretBounds=" + caretBounds + dumpVisibility() + '\n'); |
| } |
| if (caretBounds != null && isVisible() && blinkVisible) { |
| paintCustomCaret(g); |
| } |
| 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(); |
| g2d.setColor(selColor); |
| Composite origComposite = g2d.getComposite(); |
| try { |
| g2d.setComposite(AlphaComposite.SrcOver.derive(0.2f)); |
| g2d.fill(rsPaintRect); |
| } finally { |
| g2d.setComposite(origComposite); |
| } |
| // Paint stroked line around rectangular selection rectangle |
| g.setColor(c.getCaretColor()); |
| g2d.setStroke(stroke); |
| Rectangle onePointSmallerRect = new Rectangle(rsPaintRect); |
| onePointSmallerRect.width--; |
| onePointSmallerRect.height--; |
| g2d.draw(onePointSmallerRect); |
| |
| } finally { |
| g2d.setStroke(origStroke); |
| g2d.setColor(origColor); |
| } |
| } |
| } |
| |
| protected void paintCustomCaret(Graphics g) { |
| JTextComponent c = component; |
| if (c != null) { |
| EditorUI editorUI = Utilities.getEditorUI(c); |
| g.setColor(c.getCaretColor()); |
| if (THIN_LINE_CARET.equals(type)) { // thin line caret |
| int upperX = caretBounds.x; |
| if (beforeCaretFont != null && beforeCaretFont.isItalic() && italic) { |
| upperX += Math.tan(beforeCaretFont.getItalicAngle()) * caretBounds.height; |
| } |
| g.drawLine((int)upperX, caretBounds.y, caretBounds.x, |
| (caretBounds.y + caretBounds.height - 1)); |
| } else if (THICK_LINE_CARET.equals(type)) { // thick caret |
| int blkWidth = this.width; |
| if (blkWidth <= 0) blkWidth = 5; // sanity check |
| if (afterCaretFont != null) g.setFont(afterCaretFont); |
| Color textBackgroundColor = c.getBackground(); |
| if (textBackgroundColor != null) { |
| g.setXORMode( textBackgroundColor); |
| } |
| g.fillRect(caretBounds.x, caretBounds.y, blkWidth, caretBounds.height - 1); |
| } else if (BLOCK_CARET.equals(type)) { // block caret |
| if (afterCaretFont != null) g.setFont(afterCaretFont); |
| if (afterCaretFont != null && afterCaretFont.isItalic() && italic) { // paint italic caret |
| int upperX = (int)(caretBounds.x |
| + Math.tan(afterCaretFont.getItalicAngle()) * caretBounds.height); |
| xPoints[0] = upperX; |
| yPoints[0] = caretBounds.y; |
| xPoints[1] = upperX + caretBounds.width; |
| yPoints[1] = caretBounds.y; |
| xPoints[2] = caretBounds.x + caretBounds.width; |
| yPoints[2] = caretBounds.y + caretBounds.height - 1; |
| xPoints[3] = caretBounds.x; |
| yPoints[3] = caretBounds.y + caretBounds.height - 1; |
| g.fillPolygon(xPoints, yPoints, 4); |
| |
| } else { // paint non-italic caret |
| g.fillRect(caretBounds.x, caretBounds.y, caretBounds.width, caretBounds.height); |
| } |
| |
| if (!Character.isWhitespace(dotChar[0])) { |
| Color textBackgroundColor = c.getBackground(); |
| if (textBackgroundColor != null) |
| g.setColor(textBackgroundColor); |
| // int ascent = FontMetricsCache.getFontMetrics(afterCaretFont, c).getAscent(); |
| g.drawChars(dotChar, 0, 1, caretBounds.x, |
| caretBounds.y + editorUI.getLineAscent()); |
| } |
| |
| } else { // two dot line caret |
| int blkWidth = 2; |
| if (beforeCaretFont != null && beforeCaretFont.isItalic() && italic) { |
| int upperX = (int)(caretBounds.x |
| + Math.tan(beforeCaretFont.getItalicAngle()) * caretBounds.height); |
| xPoints[0] = upperX; |
| yPoints[0] = caretBounds.y; |
| xPoints[1] = upperX + blkWidth; |
| yPoints[1] = caretBounds.y; |
| xPoints[2] = caretBounds.x + blkWidth; |
| yPoints[2] = caretBounds.y + caretBounds.height - 1; |
| xPoints[3] = caretBounds.x; |
| yPoints[3] = caretBounds.y + caretBounds.height - 1; |
| g.fillPolygon(xPoints, yPoints, 4); |
| |
| } else { // paint non-italic caret |
| g.fillRect(caretBounds.x, caretBounds.y, blkWidth, caretBounds.height - 1); |
| } |
| } |
| } |
| } |
| |
| /** Update the caret's visual position */ |
| void dispatchUpdate(final boolean scrollViewToCaret) { |
| /* After using SwingUtilities.invokeLater() due to fix of #18860 |
| * there is another fix of #35034 which ensures that the caret's |
| * document listener will be added AFTER the views hierarchy's |
| * document listener so the code can run synchronously again |
| * which should eliminate the problem with caret lag. |
| * However the document can be modified from non-AWT thread |
| * which is the case in #57316 and in that case the code |
| * must run asynchronously in AWT thread. |
| */ |
| Utilities.runInEventDispatchThread( |
| new Runnable() { |
| public @Override void run() { |
| JTextComponent c = component; |
| if (c != null) { |
| BaseDocument doc = Utilities.getDocument(c); |
| if (doc != null) { |
| doc.readLock(); |
| try { |
| update(scrollViewToCaret); |
| } finally { |
| doc.readUnlock(); |
| } |
| } |
| } |
| } |
| } |
| ); |
| } |
| |
| /** |
| * Update the caret's visual position. |
| * <br/> |
| * The document is read-locked while calling this method. |
| * |
| * @param scrollViewToCaret whether the view of the text component should be |
| * scrolled to the position of the caret. |
| */ |
| protected void update(boolean scrollViewToCaret) { |
| caretUpdatePending = false; |
| JTextComponent c = component; |
| if (c != null) { |
| if (!c.isValid()) { |
| c.validate(); |
| } |
| BaseTextUI ui = (BaseTextUI)c.getUI(); |
| BaseDocument doc = Utilities.getDocument(c); |
| if (doc != null) { |
| Rectangle oldCaretBounds = caretBounds; // no need to deep copy |
| if (oldCaretBounds != null) { |
| if (italic) { // caret is italic - add char height to the width of the rect |
| oldCaretBounds.width += oldCaretBounds.height; |
| } |
| c.repaint(oldCaretBounds); |
| } |
| |
| // note - the order is important ! caret bounds must be updated even if the fold flag is true. |
| if (updateCaretBounds() || updateAfterFoldHierarchyChange) { |
| Rectangle scrollBounds = new Rectangle(caretBounds); |
| |
| // 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 >= caretBounds.height + hScrollBarHeight) { |
| scrollBounds.height += hScrollBarHeight; |
| } |
| } |
| } |
| } |
| } |
| |
| 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, caretBounds }); |
| } |
| if (oldRelY >= 0 && oldRelY < visibleBounds.height && |
| (oldCaretBounds.y != caretBounds.y || oldCaretBounds.x != caretBounds.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(caretBounds.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 && */ |
| (caretBounds.y > visibleBounds.y + visibleBounds.height + caretBounds.height |
| || caretBounds.y + caretBounds.height < visibleBounds.y - caretBounds.height) |
| ) { |
| // Scroll into the middle |
| scrollBounds.y -= (visibleBounds.height - caretBounds.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(caretBounds); |
| } |
| } |
| } |
| } |
| |
| private void updateSystemSelection() { |
| if (getDot() != getMark() && component != null) { |
| Clipboard clip = getSystemSelection(); |
| |
| if (clip != null) { |
| clip.setContents(new java.awt.datatransfer.StringSelection(component.getSelectedText()), null); |
| } |
| } |
| } |
| |
| private Clipboard getSystemSelection() { |
| return component.getToolkit().getSystemSelection(); |
| } |
| |
| private void updateRectangularSelectionPositionBlocks() { |
| JTextComponent c = component; |
| if (rectangularSelection) { |
| if (listenDoc != null) { |
| listenDoc.readLock(); |
| try { |
| if (rsRegions == null) { |
| rsRegions = new ArrayList<Position>(); |
| c.putClientProperty(RECTANGULAR_SELECTION_REGIONS_PROPERTY, rsRegions); |
| } |
| synchronized (rsRegions) { |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("Rectangular-selection position regions:\n"); |
| } |
| rsRegions.clear(); |
| if (rsPaintRect != null) { |
| LockedViewHierarchy lvh = ViewHierarchy.get(c).lock(); |
| try { |
| float rowHeight = lvh.getDefaultRowHeight(); |
| double y = rsPaintRect.y; |
| double maxY = y + rsPaintRect.height; |
| double minX = rsPaintRect.getMinX(); |
| double maxX = rsPaintRect.getMaxX(); |
| do { |
| int startOffset = lvh.viewToModel(minX, y, null); |
| int endOffset = lvh.viewToModel(maxX, y, null); |
| // They could be swapped due to RTL text |
| if (startOffset > endOffset) { |
| int tmp = startOffset; startOffset = endOffset; endOffset = tmp; |
| } |
| Position startPos = listenDoc.createPosition(startOffset); |
| Position endPos = listenDoc.createPosition(endOffset); |
| rsRegions.add(startPos); |
| rsRegions.add(endPos); |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine(" <" + startOffset + "," + endOffset + ">\n"); |
| } |
| y += rowHeight; |
| } while (y < maxY); |
| c.putClientProperty(RECTANGULAR_SELECTION_REGIONS_PROPERTY, rsRegions); |
| } finally { |
| lvh.unlock(); |
| } |
| } |
| } |
| } catch (BadLocationException ex) { |
| Exceptions.printStackTrace(ex); |
| } finally { |
| listenDoc.readUnlock(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Redefine to Object.equals() to prevent defaulting to Rectangle.equals() |
| * which would cause incorrect firing |
| */ |
| @Override@SuppressWarnings("EqualsWhichDoesntCheckParameterClass") |
| public boolean equals(Object o) { |
| return (this == o); |
| } |
| |
| public @Override int hashCode() { |
| return System.identityHashCode(this); |
| } |
| |
| /** Adds listener to track when caret position was changed */ |
| public @Override void addChangeListener(ChangeListener l) { |
| listenerList.add(ChangeListener.class, l); |
| } |
| |
| /** Removes listeners to caret position changes */ |
| public @Override void removeChangeListener(ChangeListener l) { |
| listenerList.remove(ChangeListener.class, l); |
| } |
| |
| /** Notifies listeners that caret position has changed */ |
| protected void fireStateChanged() { |
| Runnable runnable = new Runnable() { |
| public @Override void run() { |
| // #252441: uninstallUI might detached the caret from component while this |
| // event was queued. |
| if (component == null || component.getCaret() != BaseCaret.this) { |
| return; |
| } |
| Object listeners[] = listenerList.getListenerList(); |
| for (int i = listeners.length - 2; i >= 0 ; i -= 2) { |
| if (listeners[i] == ChangeListener.class) { |
| if (changeEvent == null) { |
| changeEvent = new ChangeEvent(BaseCaret.this); |
| } |
| ((ChangeListener)listeners[i + 1]).stateChanged(changeEvent); |
| } |
| } |
| } |
| }; |
| |
| // Fix of #24336 - always do in AWT thread |
| // Fix of #114649 - when under document's lock repost asynchronously |
| if (inAtomicUnlock) { |
| SwingUtilities.invokeLater(runnable); |
| } else { |
| Utilities.runInEventDispatchThread(runnable); |
| } |
| updateSystemSelection(); |
| } |
| |
| /** |
| * Whether the caret currently visible. |
| * <br> |
| * Although the caret is visible it may be in a state when it's |
| * not physically showing on screen in case when it's blinking. |
| */ |
| public final @Override boolean isVisible() { |
| return caretVisible; |
| } |
| |
| protected void setVisibleImpl(boolean v) { |
| if (LOG.isLoggable(Level.FINER)) { |
| LOG.finer("BaseCaret.setVisible(" + v + ")\n"); |
| if (LOG.isLoggable(Level.FINEST)) { |
| LOG.log(Level.INFO, "", new Exception()); |
| } |
| } |
| boolean visible = isVisible(); |
| synchronized (this) { |
| synchronized (listenerImpl) { |
| if (flasher != null) { |
| if (visible) { |
| flasher.stop(); |
| } |
| if (LOG.isLoggable(Level.FINER)) { |
| LOG.finer((v ? "Starting" : "Stopping") + // NOI18N |
| " the caret blinking timer: " + dumpVisibility() + '\n'); // NOI18N |
| } |
| if (v) { |
| flasher.start(); |
| } else { |
| flasher.stop(); |
| } |
| } |
| } |
| |
| caretVisible = v; |
| } |
| JTextComponent c = component; |
| if (c != null && caretBounds != null) { |
| Rectangle repaintRect = caretBounds; |
| if (italic) { |
| repaintRect = new Rectangle(repaintRect); // copy |
| repaintRect.width += repaintRect.height; // ensure enough horizontally |
| } |
| c.repaint(repaintRect); |
| } |
| } |
| |
| private String dumpVisibility() { |
| return "visible=" + isVisible() + ", blinkVisible=" + blinkVisible; |
| } |
| |
| @SuppressWarnings("NestedSynchronizedStatement") |
| void resetBlink() { |
| synchronized (this) { |
| boolean visible = isVisible(); |
| synchronized (listenerImpl) { |
| if (flasher != null) { |
| flasher.stop(); |
| blinkVisible = true; |
| 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(); |
| } else { |
| if (LOG.isLoggable(Level.FINER)){ |
| LOG.finer("Reset blinking (caret not visible)" + // NOI18N |
| " - caret blinking timer not started: " + dumpVisibility() + '\n'); // NOI18N |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /** Sets the caret visibility */ |
| public @Override void setVisible(final boolean v) { |
| Utilities.runInEventDispatchThread( |
| new Runnable() { |
| public @Override void run() { |
| setVisibleImpl(v); |
| } |
| } |
| ); |
| } |
| |
| /** Is the selection visible? */ |
| public final @Override boolean isSelectionVisible() { |
| return selectionVisible; |
| } |
| |
| /** Sets the selection visibility */ |
| public @Override void setSelectionVisible(boolean v) { |
| if (selectionVisible == v) { |
| return; |
| } |
| JTextComponent c = component; |
| Document doc; |
| if (c != null && (doc = c.getDocument()) != null) { |
| selectionVisible = v; |
| |
| // repaint the block |
| final BaseTextUI ui = (BaseTextUI)c.getUI(); |
| doc.render(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| ui.getEditorUI().repaintBlock(getDot(), getMark()); |
| } catch (BadLocationException e) { |
| Utilities.annotateLoggable(e); |
| } |
| } |
| }); |
| } |
| } |
| |
| /** Saves the current caret position. This is used when |
| * caret up or down actions occur, moving between lines |
| * that have uneven end positions. |
| * |
| * @param p the Point to use for the saved position |
| */ |
| public @Override void setMagicCaretPosition(Point p) { |
| magicCaretPosition = p; |
| } |
| |
| /** Get position used to mark begining of the selected block */ |
| public @Override final Point getMagicCaretPosition() { |
| return magicCaretPosition; |
| } |
| |
| /** Sets the caret blink rate. |
| * @param rate blink rate in milliseconds, 0 means no blink |
| */ |
| public @Override synchronized void setBlinkRate(int rate) { |
| if (LOG.isLoggable(Level.FINER)) { |
| LOG.finer("setBlinkRate(" + rate + ")" + dumpVisibility() + '\n'); // NOI18N |
| } |
| synchronized (listenerImpl) { |
| if (flasher == null && rate > 0) { |
| flasher = new Timer(rate, new WeakTimerListener(this)); |
| } |
| if (flasher != null) { |
| if (rate > 0) { |
| if (flasher.getDelay() != rate) { |
| flasher.setDelay(rate); |
| } |
| } else { // zero rate - don't blink |
| flasher.stop(); |
| flasher.removeActionListener(this); |
| flasher = null; |
| blinkVisible = true; |
| if (LOG.isLoggable(Level.FINER)){ |
| LOG.finer("Zero blink rate - no blinking. flasher=null; blinkVisible=true"); // NOI18N |
| } |
| } |
| } |
| } |
| } |
| |
| /** Returns blink rate of the caret or 0 if caret doesn't blink */ |
| @Override |
| @SuppressWarnings("NestedSynchronizedStatement") |
| public int getBlinkRate() { |
| synchronized (this) { |
| synchronized (listenerImpl) { |
| return (flasher != null) ? flasher.getDelay() : 0; |
| } |
| } |
| } |
| |
| /** Gets the current position of the caret */ |
| public @Override int getDot() { |
| if (component != null) { |
| return (caretPos != null) ? caretPos.getOffset() : 0; |
| } |
| return 0; |
| } |
| |
| /** Gets the current position of the selection mark. |
| * If there's a selection this position will be different |
| * from the caret position. |
| */ |
| public @Override int getMark() { |
| if (component != null) { |
| return (markPos != null) ? markPos.getOffset() : 0; |
| } |
| return 0; |
| } |
| |
| /** |
| * Assign the caret a new offset in the underlying document. |
| * <br/> |
| * This method implicitly sets the selection range to zero. |
| */ |
| public @Override void setDot(int offset) { |
| // The first call to this method in NB is done when the component |
| // is already connected to the component hierarchy but its size |
| // is still (0,0,0,0) (although its preferred size is already non-empty). |
| // This causes the TextUI.modelToView() to return null |
| // because BasicTextUI.getVisibleEditorRect() returns null. |
| // Thus caretBounds will be null in such case although |
| // the offset in setDot() is already non-zero. |
| // In such case the component listener listens for resizing |
| // of the editor component and reassigns the caretBounds |
| // once the component gets resized. |
| setDot(offset, caretBounds, EditorUI.SCROLL_DEFAULT); |
| } |
| |
| public void setDot(int offset, boolean expandFold) { |
| setDot(offset, caretBounds, EditorUI.SCROLL_DEFAULT, expandFold); |
| } |
| |
| |
| /** Sets the caret position to some position. This |
| * causes removal of the active selection. If expandFold set to true |
| * fold containing offset position will be expanded. |
| * |
| * <p> |
| * <b>Note:</b> This method is deprecated and the present implementation |
| * ignores values of scrollRect and scrollPolicy parameters. |
| * |
| * @param offset offset in the document to which the caret should be positioned. |
| * @param scrollRect rectangle to which the editor window should be scrolled. |
| * @param scrollPolicy the way how scrolling should be done. |
| * One of <code>EditorUI.SCROLL_*</code> constants. |
| * @param expandFold whether possible fold at the caret position should be expanded. |
| * |
| * @deprecated use #setDot(int, boolean) preceded by <code>JComponent.scrollRectToVisible()</code>. |
| */ |
| |
| public void setDot(int offset, Rectangle scrollRect, int scrollPolicy, boolean expandFold) { |
| if (LOG_EDT.isLoggable(Level.FINE)) { // Only permit operations in EDT |
| if (!SwingUtilities.isEventDispatchThread()) { |
| throw new IllegalStateException("BaseCaret.setDot() not in EDT: offset=" + offset); // NOI18N |
| } |
| } |
| |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("setDot: offset=" + offset); //NOI18N |
| if (LOG.isLoggable(Level.FINEST)) { |
| LOG.log(Level.INFO, "setDot call stack", new Exception()); |
| } |
| } |
| |
| JTextComponent c = component; |
| if (c != null) { |
| BaseDocument doc = (BaseDocument)c.getDocument(); |
| boolean dotChanged = false; |
| doc.readLock(); |
| try { |
| if (doc != null && offset >= 0 && offset <= doc.getLength()) { |
| dotChanged = true; |
| try { |
| caretPos = doc.createPosition(offset); |
| markPos = doc.createPosition(offset); |
| |
| Callable<Boolean> cc = (Callable<Boolean>)c.getClientProperty("org.netbeans.api.fold.expander"); |
| if (cc != null && expandFold) { |
| // the caretPos/markPos were already called. |
| // nothing except the document is locked at this moment. |
| try { |
| cc.call(); |
| } catch (Exception ex) { |
| Exceptions.printStackTrace(ex); |
| } |
| } |
| if (rectangularSelection) { |
| setRectangularSelectionToDotAndMark(); |
| } |
| |
| } catch (BadLocationException e) { |
| throw new IllegalStateException(e.toString()); |
| // setting the caret to wrong position leaves it at current position |
| } |
| } |
| } finally { |
| doc.readUnlock(); |
| } |
| |
| if (dotChanged) { |
| fireStateChanged(); |
| dispatchUpdate(true); |
| } |
| } |
| } |
| |
| /** Sets the caret position to some position. This |
| * causes removal of the active selection. |
| * |
| * <p> |
| * <b>Note:</b> This method is deprecated and the present implementation |
| * ignores values of scrollRect and scrollPolicy parameters. |
| * |
| * @param offset offset in the document to which the caret should be positioned. |
| * @param scrollRect rectangle to which the editor window should be scrolled. |
| * @param scrollPolicy the way how scrolling should be done. |
| * One of <code>EditorUI.SCROLL_*</code> constants. |
| * |
| * @deprecated use #setDot(int) preceded by <code>JComponent.scrollRectToVisible()</code>. |
| */ |
| public void setDot(int offset, Rectangle scrollRect, int scrollPolicy) { |
| setDot(offset, scrollRect, scrollPolicy, true); |
| } |
| |
| public @Override void moveDot(int offset) { |
| moveDot(offset, caretBounds, EditorUI.SCROLL_MOVE); |
| } |
| |
| /** Makes selection by moving dot but leaving mark. |
| * |
| * <p> |
| * <b>Note:</b> This method is deprecated and the present implementation |
| * ignores values of scrollRect and scrollPolicy parameters. |
| * |
| * @param offset offset in the document to which the caret should be positioned. |
| * @param scrollRect rectangle to which the editor window should be scrolled. |
| * @param scrollPolicy the way how scrolling should be done. |
| * One of <code>EditorUI.SCROLL_*</code> constants. |
| * |
| * @deprecated use #setDot(int) preceded by <code>JComponent.scrollRectToVisible()</code>. |
| */ |
| public void moveDot(int offset, Rectangle scrollRect, int scrollPolicy) { |
| if (LOG_EDT.isLoggable(Level.FINE)) { // Only permit operations in EDT |
| if (!SwingUtilities.isEventDispatchThread()) { |
| throw new IllegalStateException("BaseCaret.moveDot() not in EDT: offset=" + offset); // NOI18N |
| } |
| } |
| |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("moveDot: offset=" + offset); //NOI18N |
| } |
| |
| JTextComponent c = component; |
| if (c != null) { |
| BaseDocument doc = (BaseDocument)c.getDocument(); |
| if (doc != null && offset >= 0 && offset <= doc.getLength()) { |
| doc.readLock(); |
| try { |
| int oldCaretPos = getDot(); |
| if (offset == oldCaretPos) { // no change |
| return; |
| } |
| caretPos = doc.createPosition(offset); |
| if (selectionVisible) { // selection already visible |
| Utilities.getEditorUI(c).repaintBlock(oldCaretPos, offset); |
| } |
| 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(); |
| } |
| } |
| fireStateChanged(); |
| dispatchUpdate(true); |
| } |
| } |
| |
| // DocumentListener methods |
| public @Override void insertUpdate(DocumentEvent evt) { |
| JTextComponent c = component; |
| if (c != null) { |
| int offset = evt.getOffset(); |
| int endOffset = offset + evt.getLength(); |
| if (evt.getOffset() == 0) { |
| // Insert at offset 0 the marks would stay at offset == 0 |
| if (getMark() == 0) { |
| try { |
| markPos = listenDoc.createPosition(endOffset); |
| } catch (BadLocationException ex) { |
| Exceptions.printStackTrace(ex); |
| } |
| } |
| if (getDot() == 0) { |
| try { |
| caretPos = listenDoc.createPosition(endOffset); |
| } catch (BadLocationException ex) { |
| Exceptions.printStackTrace(ex); |
| } |
| } |
| } |
| BaseDocumentEvent bevt = (BaseDocumentEvent)evt; |
| boolean typingModification; |
| if ((bevt.isInUndo() || bevt.isInRedo()) |
| && component == Utilities.getLastActiveComponent() |
| && !Boolean.TRUE.equals(DocumentUtilities.getEventProperty(evt, "caretIgnore")) |
| ) { |
| // in undo mode and current component |
| undoOffset = evt.getOffset() + evt.getLength(); |
| // Undo operations now cause the caret to move to the place where the undo occurs. |
| // In future we should put additional info into the document event whether it was |
| // a typing modification and if not the caret should not be relocated and scrolled. |
| typingModification = true; |
| } else { |
| undoOffset = -1; |
| typingModification = DocumentUtilities.isTypingModification(component.getDocument()); |
| } |
| |
| modified = true; |
| |
| modifiedUpdate(typingModification); |
| } |
| } |
| |
| public @Override void removeUpdate(DocumentEvent evt) { |
| JTextComponent c = component; |
| if (c != null) { |
| // make selection invisible if removal shrinked block to zero size |
| BaseDocumentEvent bevt = (BaseDocumentEvent)evt; |
| boolean typingModification; |
| if ((bevt.isInUndo() || bevt.isInRedo()) |
| && c == Utilities.getLastActiveComponent() |
| && !Boolean.TRUE.equals(DocumentUtilities.getEventProperty(evt, "caretIgnore")) |
| ) { |
| // in undo mode and current component |
| undoOffset = evt.getOffset(); |
| // Undo operations now cause the caret to move to the place where the undo occurs. |
| // In future we should put additional info into the document event whether it was |
| // a typing modification and if not the caret should not be relocated and scrolled. |
| typingModification = true; |
| } else { // Not undo or redo |
| undoOffset = -1; |
| typingModification = DocumentUtilities.isTypingModification(component.getDocument()); |
| |
| } |
| |
| modified = true; |
| |
| modifiedUpdate(typingModification); |
| } |
| } |
| |
| private void modifiedUpdate(boolean typingModification) { |
| if (!inAtomicLock) { |
| JTextComponent c = component; |
| if (modified && c != null) { |
| if (undoOffset >= 0) { // last modification was undo => set the dot to undoOffset |
| setDot(undoOffset); |
| } else { // last modification was not undo |
| BaseDocument doc = listenDoc; |
| if (doc != null) { |
| doc.readLock(); |
| try { |
| updateRectangularSelectionPaintRect(); |
| } finally { |
| doc.readUnlock(); |
| } |
| } |
| fireStateChanged(); |
| // Scroll to caret only for component with focus |
| dispatchUpdate(c.hasFocus() && typingModification); |
| } |
| modified = false; |
| } |
| } else { |
| typingModificationOccurred |= typingModification; |
| } |
| } |
| |
| public @Override void atomicLock(AtomicLockEvent evt) { |
| inAtomicLock = true; |
| } |
| |
| public @Override void atomicUnlock(AtomicLockEvent evt) { |
| inAtomicLock = false; |
| inAtomicUnlock = true; |
| try { |
| modifiedUpdate(typingModificationOccurred); |
| } finally { |
| inAtomicUnlock = false; |
| typingModificationOccurred = false; |
| } |
| } |
| |
| public @Override void changedUpdate(DocumentEvent evt) { |
| // XXX: used as a backdoor from HighlightingDrawLayer |
| if (evt == null) { |
| dispatchUpdate(false); |
| } |
| } |
| |
| // MouseListener methods |
| @Override |
| public void mousePressed(MouseEvent evt) { |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("mousePressed: " + logMouseEvent(evt) + ", state=" + mouseState + '\n'); // NOI18N |
| } |
| |
| JTextComponent c = component; |
| if (c != null && isLeftMouseButtonExt(evt)) { |
| // Expand fold if offset is in collapsed fold |
| int offset = mouse2Offset(evt); |
| switch (evt.getClickCount()) { |
| case 1: // Single press |
| if (c.isEnabled() && !c.hasFocus()) { |
| c.requestFocus(); |
| } |
| c.setDragEnabled(true); |
| if (evt.isShiftDown()) { // Select till offset |
| moveDot(offset); |
| adjustRectangularSelectionMouseX(evt.getX(), evt.getY()); // also fires state change |
| mouseState = MouseState.CHAR_SELECTION; |
| } else { // Regular press |
| // check whether selection drag is possible |
| if (isDragPossible(evt) && mapDragOperationFromModifiers(evt) != TransferHandler.NONE) { |
| mouseState = MouseState.DRAG_SELECTION_POSSIBLE; |
| } else { // Drag not possible |
| mouseState = MouseState.CHAR_SELECTION; |
| setDot(offset); |
| } |
| } |
| break; |
| |
| case 2: // double-click => word selection |
| mouseState = MouseState.WORD_SELECTION; |
| // Disable drag which would otherwise occur when mouse would be over text |
| c.setDragEnabled(false); |
| // Check possible fold expansion |
| try { |
| // hack, to get knowledge of possible expansion. Editor depends on Folding, so it's not really possible |
| // to have Folding depend on BaseCaret (= a cycle). If BaseCaret moves to editor.lib2, this contract |
| // can be formalized as an interface. |
| Callable<Boolean> cc = (Callable<Boolean>)c.getClientProperty("org.netbeans.api.fold.expander"); |
| if (cc == null || !cc.equals(this)) { |
| if (selectWordAction == null) { |
| selectWordAction = ((BaseKit) c.getUI().getEditorKit( |
| c)).getActionByName(BaseKit.selectWordAction); |
| } |
| if (selectWordAction != null) { |
| selectWordAction.actionPerformed(null); |
| } |
| // Select word action selects forward i.e. dot > mark |
| minSelectionStartOffset = getMark(); |
| minSelectionEndOffset = getDot(); |
| } |
| } catch (Exception ex) { |
| Exceptions.printStackTrace(ex); |
| } |
| break; |
| |
| case 3: // triple-click => line selection |
| mouseState = MouseState.LINE_SELECTION; |
| // Disable drag which would otherwise occur when mouse would be over text |
| c.setDragEnabled(false); |
| if (selectLineAction == null) { |
| selectLineAction = ((BaseKit) c.getUI().getEditorKit( |
| c)).getActionByName(BaseKit.selectLineAction); |
| } |
| if (selectLineAction != null) { |
| selectLineAction.actionPerformed(null); |
| // Select word action selects forward i.e. dot > mark |
| minSelectionStartOffset = getMark(); |
| minSelectionEndOffset = getDot(); |
| } |
| break; |
| |
| default: // multi-click |
| } |
| } |
| } |
| |
| @Override |
| public void mouseReleased(MouseEvent evt) { |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("mouseReleased: " + logMouseEvent(evt) + ", state=" + mouseState + '\n'); // NOI18N |
| } |
| |
| int offset = mouse2Offset(evt); |
| switch (mouseState) { |
| case DRAG_SELECTION_POSSIBLE: |
| setDot(offset); |
| adjustRectangularSelectionMouseX(evt.getX(), evt.getY()); // also fires state change |
| break; |
| |
| case CHAR_SELECTION: |
| moveDot(offset); // Will do setDot() if no selection |
| adjustRectangularSelectionMouseX(evt.getX(), evt.getY()); // also fires state change |
| break; |
| } |
| // Set DEFAULT state; after next mouse press the state may change |
| // to another state according to particular click count |
| mouseState = MouseState.DEFAULT; |
| component.setDragEnabled(true); |
| } |
| |
| /** |
| * Translates mouse event to text offset |
| */ |
| int mouse2Offset(MouseEvent evt) { |
| JTextComponent c = component; |
| int offset = 0; |
| if (c != null) { |
| int y = evt.getY(); |
| if (y < 0) { |
| offset = 0; |
| } else if (y > c.getSize().getHeight()) { |
| offset = c.getDocument().getLength(); |
| } else { |
| offset = c.viewToModel(new Point(evt.getX(), evt.getY())); |
| } |
| } |
| return offset; |
| } |
| |
| @Override |
| public void mouseClicked(MouseEvent evt) { |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("mouseClicked: " + logMouseEvent(evt) + ", state=" + mouseState + '\n'); // NOI18N |
| } |
| |
| JTextComponent c = component; |
| if (c != null) { |
| if (isMiddleMouseButtonExt(evt)) { |
| if (evt.getClickCount() == 1) { |
| if (c == null) { |
| return; |
| } |
| Clipboard buffer = getSystemSelection(); |
| |
| if (buffer == null) { |
| return; |
| } |
| |
| Transferable trans = buffer.getContents(null); |
| if (trans == null) { |
| return; |
| } |
| |
| final BaseDocument doc = (BaseDocument) c.getDocument(); |
| if (doc == null) { |
| return; |
| } |
| |
| final int offset = ((BaseTextUI) c.getUI()).viewToModel(c, |
| evt.getX(), evt.getY()); |
| |
| try { |
| final String pastingString = (String) trans.getTransferData(DataFlavor.stringFlavor); |
| if (pastingString == null) { |
| return; |
| } |
| doc.runAtomicAsUser(new Runnable() { |
| public @Override |
| void run() { |
| try { |
| doc.insertString(offset, pastingString, null); |
| setDot(offset + pastingString.length()); |
| } catch (BadLocationException exc) { |
| } |
| } |
| }); |
| } catch (UnsupportedFlavorException ufe) { |
| } catch (IOException ioe) { |
| } |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void mouseEntered(MouseEvent evt) { |
| } |
| |
| @Override |
| public void mouseExited(MouseEvent evt) { |
| } |
| |
| // MouseMotionListener methods |
| @Override |
| public void mouseMoved(MouseEvent evt) { |
| if (mouseState == MouseState.DEFAULT) { |
| boolean textCursor = true; |
| int position = component.viewToModel(evt.getPoint()); |
| if (RectangularSelectionUtils.isRectangularSelection(component)) { |
| List<Position> positions = RectangularSelectionUtils.regionsCopy(component); |
| for (int i = 0; textCursor && i < positions.size(); i += 2) { |
| int a = positions.get(i).getOffset(); |
| int b = positions.get(i + 1).getOffset(); |
| if (a == b) { |
| continue; |
| } |
| |
| textCursor &= !(position >= a && position <= b || position >= b && position <= a); |
| } |
| } else { |
| // stream selection |
| if (getDot() == getMark()) { |
| // empty selection |
| textCursor = true; |
| } else { |
| int dot = getDot(); |
| int mark = getMark(); |
| if (position >= dot && position <= mark || position >= mark && position <= dot) { |
| textCursor = false; |
| } else { |
| textCursor = true; |
| } |
| } |
| } |
| |
| if (textCursor != showingTextCursor) { |
| int cursorType = textCursor ? Cursor.TEXT_CURSOR : Cursor.DEFAULT_CURSOR; |
| component.setCursor(Cursor.getPredefinedCursor(cursorType)); |
| showingTextCursor = textCursor; |
| } |
| } |
| } |
| |
| @Override |
| public void mouseDragged(MouseEvent evt) { |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("mouseDragged: " + logMouseEvent(evt) + ", state=" + mouseState + '\n'); //NOI18N |
| } |
| |
| if (isLeftMouseButtonExt(evt)) { |
| JTextComponent c = component; |
| int offset = mouse2Offset(evt); |
| int dot = getDot(); |
| int mark = getMark(); |
| try { |
| switch (mouseState) { |
| case DEFAULT: |
| case DRAG_SELECTION: |
| break; |
| |
| case DRAG_SELECTION_POSSIBLE: |
| mouseState = MouseState.DRAG_SELECTION; |
| break; |
| |
| case CHAR_SELECTION: |
| moveDot(offset); |
| adjustRectangularSelectionMouseX(evt.getX(), evt.getY()); |
| break; // Use the offset under mouse pointer |
| |
| |
| case WORD_SELECTION: |
| // Increase selection if at least in the middle of a word. |
| // It depends whether selection direction is from lower offsets upward or back. |
| if (offset >= mark) { // Selection extends forward. |
| offset = Utilities.getWordEnd(c, offset); |
| } else { // Selection extends backward. |
| offset = Utilities.getWordStart(c, offset); |
| } |
| selectEnsureMinSelection(mark, dot, offset); |
| break; |
| |
| case LINE_SELECTION: |
| if (offset >= mark) { // Selection extends forward |
| offset = Math.min(Utilities.getRowEnd(c, offset) + 1, c.getDocument().getLength()); |
| } else { // Selection extends backward |
| offset = Utilities.getRowStart(c, offset); |
| } |
| selectEnsureMinSelection(mark, dot, offset); |
| break; |
| |
| default: |
| throw new AssertionError("Invalid state " + mouseState); // NOI18N |
| } |
| } catch (BadLocationException ex) { |
| Exceptions.printStackTrace(ex); |
| } |
| } |
| } |
| |
| private void adjustRectangularSelectionMouseX(int x, int y) { |
| if (!rectangularSelection) { |
| return; |
| } |
| JTextComponent c = component; |
| int offset = c.viewToModel(new Point(x, y)); |
| Rectangle r = null;; |
| if (offset >= 0) { |
| try { |
| r = c.modelToView(offset); |
| } catch (BadLocationException ex) { |
| r = null; |
| } |
| } |
| if (r != null) { |
| float xDiff = x - r.x; |
| if (xDiff > 0) { |
| float charWidth; |
| LockedViewHierarchy lvh = ViewHierarchy.get(c).lock(); |
| try { |
| charWidth = lvh.getDefaultCharWidth(); |
| } finally { |
| lvh.unlock(); |
| } |
| int n = (int) (xDiff / charWidth); |
| r.x += n * charWidth; |
| r.width = (int) charWidth; |
| } |
| rsDotRect.x = r.x; |
| rsDotRect.width = r.width; |
| updateRectangularSelectionPaintRect(); |
| fireStateChanged(); |
| } |
| } |
| |
| void setRectangularSelectionToDotAndMark() { |
| int dotOffset = getDot(); |
| int markOffset = getMark(); |
| try { |
| rsDotRect = component.modelToView(dotOffset); |
| rsMarkRect = component.modelToView(markOffset); |
| } catch (BadLocationException ex) { |
| rsDotRect = rsMarkRect = null; |
| } |
| updateRectangularSelectionPaintRect(); |
| } |
| |
| public void updateRectangularUpDownSelection() { |
| JTextComponent c = component; |
| int dotOffset = getDot(); |
| try { |
| Rectangle r = c.modelToView(dotOffset); |
| rsDotRect.y = r.y; |
| rsDotRect.height = r.height; |
| } catch (BadLocationException ex) { |
| // Leave rsDotRect unchanged |
| } |
| } |
| |
| /** |
| * Extend rectangular selection either by char in a specified selection |
| * or by word (if ctrl is pressed). |
| * |
| * @param toRight true for right or false for left. |
| * @param ctrl |
| */ |
| public void extendRectangularSelection(boolean toRight, boolean ctrl) { |
| JTextComponent c = component; |
| Document doc = c.getDocument(); |
| int dotOffset = getDot(); |
| Element lineRoot = doc.getDefaultRootElement(); |
| int lineIndex = lineRoot.getElementIndex(dotOffset); |
| Element lineElement = lineRoot.getElement(lineIndex); |
| float charWidth; |
| LockedViewHierarchy lvh = ViewHierarchy.get(c).lock(); |
| try { |
| charWidth = lvh.getDefaultCharWidth(); |
| } finally { |
| lvh.unlock(); |
| } |
| int newDotOffset = -1; |
| try { |
| int newlineOffset = lineElement.getEndOffset() - 1; |
| Rectangle newlineRect = c.modelToView(newlineOffset); |
| if (!ctrl) { |
| if (toRight) { |
| if (rsDotRect.x < newlineRect.x) { |
| newDotOffset = dotOffset + 1; |
| } else { |
| rsDotRect.x += charWidth; |
| } |
| } else { // toLeft |
| if (rsDotRect.x > newlineRect.x) { |
| rsDotRect.x -= charWidth; |
| if (rsDotRect.x < newlineRect.x) { // Fix on rsDotRect |
| newDotOffset = newlineOffset; |
| } |
| } else { |
| newDotOffset = Math.max(dotOffset - 1, lineElement.getStartOffset()); |
| } |
| } |
| |
| } else { // With Ctrl |
| int numVirtualChars = 8; // Number of virtual characters per one Ctrl+Shift+Arrow press |
| if (toRight) { |
| if (rsDotRect.x < newlineRect.x) { |
| newDotOffset = Math.min(Utilities.getNextWord(c, dotOffset), lineElement.getEndOffset() - 1); |
| } else { // Extend virtually |
| rsDotRect.x += numVirtualChars * charWidth; |
| } |
| } else { // toLeft |
| if (rsDotRect.x > newlineRect.x) { // Virtually extended |
| rsDotRect.x -= numVirtualChars * charWidth; |
| if (rsDotRect.x < newlineRect.x) { |
| newDotOffset = newlineOffset; |
| } |
| } else { |
| newDotOffset = Math.max(Utilities.getPreviousWord(c, dotOffset), lineElement.getStartOffset()); |
| } |
| } |
| } |
| |
| if (newDotOffset != -1) { |
| rsDotRect = c.modelToView(newDotOffset); |
| moveDot(newDotOffset); // updates rs and fires state change |
| } else { |
| updateRectangularSelectionPaintRect(); |
| fireStateChanged(); |
| } |
| } catch (BadLocationException ex) { |
| // Leave selection as is |
| } |
| } |
| |
| private void updateRectangularSelectionPaintRect() { |
| // Repaint current rect |
| JTextComponent c = component; |
| Rectangle repaintRect = rsPaintRect; |
| if (rsDotRect == null || rsMarkRect == null) { |
| return; |
| } |
| Rectangle newRect = new Rectangle(); |
| if (rsDotRect.x < rsMarkRect.x) { // Swap selection to left |
| newRect.x = rsDotRect.x; // -1 to make the visual selection non-empty |
| newRect.width = rsMarkRect.x - newRect.x; |
| } else { // Extend or shrink on right |
| newRect.x = rsMarkRect.x; |
| newRect.width = rsDotRect.x - newRect.x; |
| } |
| if (rsDotRect.y < rsMarkRect.y) { |
| newRect.y = rsDotRect.y; |
| newRect.height = (rsMarkRect.y + rsMarkRect.height) - newRect.y; |
| } else { |
| newRect.y = rsMarkRect.y; |
| newRect.height = (rsDotRect.y + rsDotRect.height) - newRect.y; |
| } |
| if (newRect.width < 2) { |
| newRect.width = 2; |
| } |
| rsPaintRect = newRect; |
| |
| // Repaint merged region with original rect |
| if (repaintRect == null) { |
| repaintRect = rsPaintRect; |
| } else { |
| repaintRect = repaintRect.union(rsPaintRect); |
| } |
| c.repaint(repaintRect); |
| |
| updateRectangularSelectionPositionBlocks(); |
| } |
| |
| private void selectEnsureMinSelection(int mark, int dot, int newDot) { |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("selectEnsureMinSelection: mark=" + mark + ", dot=" + dot + ", newDot=" + newDot); // NOI18N |
| } |
| if (dot >= mark) { // Existing forward selection |
| if (newDot >= mark) { |
| moveDot(Math.max(newDot, minSelectionEndOffset)); |
| } else { // newDot < mark => swap mark and dot |
| setDot(minSelectionEndOffset); |
| moveDot(Math.min(newDot, minSelectionStartOffset)); |
| } |
| |
| } else { // Existing backward selection |
| if (newDot <= mark) { |
| moveDot(Math.min(newDot, minSelectionStartOffset)); |
| } else { // newDot > mark => swap mark and dot |
| setDot(minSelectionStartOffset); |
| moveDot(Math.max(newDot, minSelectionEndOffset)); |
| } |
| } |
| } |
| |
| private boolean isLeftMouseButtonExt(MouseEvent evt) { |
| return (SwingUtilities.isLeftMouseButton(evt) |
| && !(evt.isPopupTrigger()) |
| && (evt.getModifiers() & (InputEvent.META_MASK | InputEvent.ALT_MASK)) == 0); |
| } |
| |
| private boolean isMiddleMouseButtonExt(MouseEvent evt) { |
| return (evt.getButton() == MouseEvent.BUTTON2) && |
| (evt.getModifiersEx() & (InputEvent.CTRL_DOWN_MASK | InputEvent.META_DOWN_MASK | /* cannot be tested bcs of bug in JDK InputEvent.ALT_DOWN_MASK | */ InputEvent.ALT_GRAPH_DOWN_MASK)) == 0; |
| } |
| |
| protected int mapDragOperationFromModifiers(MouseEvent e) { |
| int mods = e.getModifiersEx(); |
| |
| if ((mods & InputEvent.BUTTON1_DOWN_MASK) == 0) { |
| return TransferHandler.NONE; |
| } |
| |
| return TransferHandler.COPY_OR_MOVE; |
| } |
| |
| /** |
| * Determines if the following are true: |
| * <ul> |
| * <li>the press event is located over a selection |
| * <li>the dragEnabled property is true |
| * <li>A TranferHandler is installed |
| * </ul> |
| * <p> |
| * This is implemented to check for a TransferHandler. |
| * Subclasses should perform the remaining conditions. |
| */ |
| protected boolean isDragPossible(MouseEvent e) { |
| JComponent comp = getEventComponent(e); |
| boolean possible = (comp == null) ? false : (comp.getTransferHandler() != null); |
| if (possible) { |
| JTextComponent c = (JTextComponent) getEventComponent(e); |
| if (c.getDragEnabled()) { |
| Caret caret = c.getCaret(); |
| int dot = caret.getDot(); |
| int mark = caret.getMark(); |
| if (dot != mark) { |
| Point p = new Point(e.getX(), e.getY()); |
| int pos = c.viewToModel(p); |
| |
| int p0 = Math.min(dot, mark); |
| int p1 = Math.max(dot, mark); |
| if ((pos >= p0) && (pos < p1)) { |
| return true; |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| protected JComponent getEventComponent(MouseEvent e) { |
| Object src = e.getSource(); |
| if (src instanceof JComponent) { |
| JComponent c = (JComponent) src; |
| return c; |
| } |
| return null; |
| } |
| |
| private static String logMouseEvent(MouseEvent evt) { |
| return "x=" + evt.getX() + ", y=" + evt.getY() + ", clicks=" + evt.getClickCount() //NOI18N |
| + ", component=" + s2s(evt.getComponent()) //NOI18N |
| + ", source=" + s2s(evt.getSource()) + ", button=" + evt.getButton() + ", mods=" + evt.getModifiers() + ", modsEx=" + evt.getModifiersEx(); //NOI18N |
| } |
| |
| private static String s2s(Object o) { |
| return o == null ? "null" : o.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(o)); //NOI18N |
| } |
| |
| // PropertyChangeListener methods |
| public @Override void propertyChange(PropertyChangeEvent evt) { |
| String propName = evt.getPropertyName(); |
| if ("document".equals(propName)) { // NOI18N |
| BaseDocument newDoc = (evt.getNewValue() instanceof BaseDocument) |
| ? (BaseDocument)evt.getNewValue() : null; |
| modelChanged(listenDoc, newDoc); |
| |
| } else if (EditorUI.OVERWRITE_MODE_PROPERTY.equals(propName)) { |
| Boolean b = (Boolean)evt.getNewValue(); |
| overwriteMode = (b != null) ? b.booleanValue() : false; |
| updateType(); |
| |
| } else if ("ancestor".equals(propName) && evt.getSource() == component) { // NOI18N |
| // The following code ensures that when the width of the line views |
| // gets computed on background after the file gets opened |
| // (so the horizontal scrollbar gets added after several seconds |
| // for larger files) that the suddenly added horizontal scrollbar |
| // will not hide the caret laying on the last line of the viewport. |
| // A component listener gets installed into horizontal scrollbar |
| // and if it's fired the caret's bounds will be checked whether |
| // they intersect with the horizontal scrollbar |
| // and if so the view will be scrolled. |
| Container parent = component.getParent(); |
| if (parent instanceof JViewport) { |
| parent = parent.getParent(); // parent of viewport |
| if (parent instanceof JScrollPane) { |
| JScrollPane scrollPane = (JScrollPane)parent; |
| JScrollBar hScrollBar = scrollPane.getHorizontalScrollBar(); |
| if (hScrollBar != null) { |
| // Add weak listener so that editor pane could be removed |
| // from scrollpane without being held by scrollbar |
| hScrollBar.addComponentListener( |
| (ComponentListener)WeakListeners.create( |
| ComponentListener.class, listenerImpl, hScrollBar)); |
| } |
| } |
| } |
| } else if ("enabled".equals(propName)) { |
| Boolean enabled = (Boolean) evt.getNewValue(); |
| if(component.isFocusOwner()) { |
| if(enabled == Boolean.TRUE) { |
| if(component.isEditable()) { |
| setVisible(true); |
| } |
| setSelectionVisible(true); |
| } else { |
| setVisible(false); |
| setSelectionVisible(false); |
| } |
| } |
| } else if (RECTANGULAR_SELECTION_PROPERTY.equals(propName)) { |
| boolean origRectangularSelection = rectangularSelection; |
| rectangularSelection = Boolean.TRUE.equals(component.getClientProperty(RECTANGULAR_SELECTION_PROPERTY)); |
| if (rectangularSelection != origRectangularSelection) { |
| if (rectangularSelection) { |
| setRectangularSelectionToDotAndMark(); |
| RectangularSelectionTransferHandler.install(component); |
| |
| } else { // No rectangular selection |
| RectangularSelectionTransferHandler.uninstall(component); |
| } |
| fireStateChanged(); |
| } |
| } |
| } |
| |
| // ActionListener methods |
| /** Fired when blink timer fires */ |
| public @Override void actionPerformed(ActionEvent evt) { |
| JTextComponent c = component; |
| if (c != null) { |
| blinkVisible = !blinkVisible; |
| if (caretBounds != null) { |
| Rectangle repaintRect = caretBounds; |
| if (italic) { |
| repaintRect = new Rectangle(repaintRect); // clone |
| repaintRect.width += repaintRect.height; |
| } |
| c.repaint(repaintRect); |
| } |
| } |
| } |
| |
| /** |
| * This method is an implementation detail. |
| * Please do not use it. |
| * |
| * @param evt |
| * @deprecated |
| */ |
| @Deprecated |
| public @Override void foldHierarchyChanged(FoldHierarchyEvent evt) { |
| |
| } |
| |
| void scheduleCaretUpdate() { |
| if (!caretUpdatePending) { |
| caretUpdatePending = true; |
| SwingUtilities.invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| update(false); |
| } |
| }); |
| } |
| } |
| |
| private class ListenerImpl extends ComponentAdapter |
| implements FocusListener, ViewHierarchyListener { |
| |
| ListenerImpl() { |
| } |
| |
| // FocusListener methods |
| public @Override void focusGained(FocusEvent evt) { |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine( |
| "BaseCaret.focusGained(); doc=" + // NOI18N |
| component.getDocument().getProperty(Document.TitleProperty) + '\n' |
| ); |
| } |
| |
| JTextComponent c = component; |
| if (c != null) { |
| updateType(); |
| if (component.isEnabled()) { |
| if (component.isEditable()) { |
| setVisible(true); |
| } |
| setSelectionVisible(true); |
| } |
| if (LOG.isLoggable(Level.FINER)) { |
| LOG.finer("Caret visibility: " + isVisible() + '\n'); // NOI18N |
| } |
| } else { |
| if (LOG.isLoggable(Level.FINER)) { |
| LOG.finer("Text component is null, caret will not be visible" + '\n'); // NOI18N |
| } |
| } |
| } |
| |
| public @Override void focusLost(FocusEvent evt) { |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("BaseCaret.focusLost(); doc=" + // NOI18N |
| component.getDocument().getProperty(Document.TitleProperty) + |
| "\nFOCUS GAINER: " + evt.getOppositeComponent() + '\n' // NOI18N |
| ); |
| if (LOG.isLoggable(Level.FINER)) { |
| LOG.finer("FOCUS EVENT: " + evt + '\n'); // NOI18N |
| } |
| } |
| setVisible(false); |
| setSelectionVisible(evt.isTemporary()); |
| } |
| |
| // ComponentListener methods |
| /** |
| * May be called for either component or horizontal scrollbar. |
| */ |
| public @Override void componentShown(ComponentEvent e) { |
| // Called when horizontal scrollbar gets visible |
| // (but the same listener added to component as well so must check first) |
| // Check whether present caret position will not get hidden |
| // under horizontal scrollbar and if so scroll the view |
| Component hScrollBar = e.getComponent(); |
| if (hScrollBar != component) { // really called for horizontal scrollbar |
| Component scrollPane = hScrollBar.getParent(); |
| if (caretBounds != null && scrollPane instanceof JScrollPane) { |
| Rectangle viewRect = ((JScrollPane)scrollPane).getViewport().getViewRect(); |
| Rectangle hScrollBarRect = new Rectangle( |
| viewRect.x, |
| viewRect.y + viewRect.height, |
| hScrollBar.getWidth(), |
| hScrollBar.getHeight() |
| ); |
| if (hScrollBarRect.intersects(caretBounds)) { |
| // Update caret's position |
| dispatchUpdate(true); // should be visible so scroll the view |
| } |
| } |
| } |
| } |
| |
| |
| /** |
| * May be called for either component or horizontal scrollbar. |
| */ |
| public @Override void componentResized(ComponentEvent e) { |
| Component c = e.getComponent(); |
| if (c == component) { // called for component |
| // In case the caretBounds are still null |
| // (component not connected to hierarchy yet or it has zero size |
| // so the modelToView() returned null) re-attempt to compute the bounds. |
| if (caretBounds == null) { |
| dispatchUpdate(true); |
| if (caretBounds != null) { // detach the listener - no longer necessary |
| c.removeComponentListener(this); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void viewHierarchyChanged(ViewHierarchyEvent evt) { |
| scheduleCaretUpdate(); |
| } |
| |
| } // End of ListenerImpl class |
| |
| public final void refresh() { |
| updateType(); |
| SwingUtilities.invokeLater(new Runnable() { |
| public @Override void run() { |
| updateCaretBounds(); // the line height etc. may have change |
| } |
| }); |
| } |
| |
| private static enum MouseState { |
| |
| DEFAULT, // Mouse released; not extending any selection |
| CHAR_SELECTION, // Extending character selection after single mouse press |
| WORD_SELECTION, // Extending word selection after double-click when mouse button still pressed |
| LINE_SELECTION, // Extending line selection after triple-click when mouse button still pressed |
| DRAG_SELECTION_POSSIBLE, // There was a selected text when mouse press arrived so drag is possible |
| DRAG_SELECTION // Drag is being done (text selection existed at the mouse press) |
| |
| } |
| |
| /** |
| * Refreshes caret display on the screen. |
| * Some height or view changes may result in the caret going off the screen. In some cases, this is not desirable, |
| * as the user's work may be interrupted by e.g. an automatic refresh. This method repositions the view so the |
| * caret remains visible. |
| * <p/> |
| * The method has two modes: it can reposition the view just if it originally displayed the caret and the caret became |
| * invisible, and it can scroll the caret into view unconditionally. |
| * @param retainInView true to scroll only if the caret was visible. False to refresh regardless of visibility. |
| */ |
| public void refresh(boolean retainInView) { |
| Rectangle b = caretBounds; |
| updateAfterFoldHierarchyChange = b != null; |
| boolean wasInView = b != null && component.getVisibleRect().intersects(b); |
| |
| update(!retainInView || wasInView); |
| } |
| |
| } |