Line 0
Link Here
|
|
|
1 |
/* |
2 |
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. |
3 |
* |
4 |
* Copyright 2015 Oracle and/or its affiliates. All rights reserved. |
5 |
* |
6 |
* Oracle and Java are registered trademarks of Oracle and/or its affiliates. |
7 |
* Other names may be trademarks of their respective owners. |
8 |
* |
9 |
* The contents of this file are subject to the terms of either the GNU |
10 |
* General Public License Version 2 only ("GPL") or the Common |
11 |
* Development and Distribution License("CDDL") (collectively, the |
12 |
* "License"). You may not use this file except in compliance with the |
13 |
* License. You can obtain a copy of the License at |
14 |
* http://www.netbeans.org/cddl-gplv2.html |
15 |
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the |
16 |
* specific language governing permissions and limitations under the |
17 |
* License. When distributing the software, include this License Header |
18 |
* Notice in each file and include the License file at |
19 |
* nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this |
20 |
* particular file as subject to the "Classpath" exception as provided |
21 |
* by Oracle in the GPL Version 2 section of the License file that |
22 |
* accompanied this code. If applicable, add the following below the |
23 |
* License Header, with the fields enclosed by brackets [] replaced by |
24 |
* your own identifying information: |
25 |
* "Portions Copyrighted [year] [name of copyright owner]" |
26 |
* |
27 |
* If you wish your version of this file to be governed by only the CDDL |
28 |
* or only the GPL Version 2, indicate your decision by adding |
29 |
* "[Contributor] elects to include this software in this distribution |
30 |
* under the [CDDL or GPL Version 2] license." If you do not indicate a |
31 |
* single choice of license, a recipient has the option to distribute |
32 |
* your version of this file under either the CDDL, the GPL Version 2 or |
33 |
* to extend the choice of license to its licensees as provided above. |
34 |
* However, if you add GPL Version 2 code and therefore, elected the GPL |
35 |
* Version 2 license, then the option applies only if the new code is |
36 |
* made subject to such option by the copyright holder. |
37 |
* |
38 |
* Contributor(s): |
39 |
* |
40 |
* Portions Copyrighted 2015 Sun Microsystems, Inc. |
41 |
*/ |
42 |
package org.netbeans.api.editor.caret; |
43 |
|
44 |
import org.netbeans.spi.editor.caret.CaretMoveHandler; |
45 |
import java.awt.AlphaComposite; |
46 |
import java.awt.BasicStroke; |
47 |
import java.awt.Color; |
48 |
import java.awt.Component; |
49 |
import java.awt.Composite; |
50 |
import java.awt.Container; |
51 |
import java.awt.Cursor; |
52 |
import java.awt.Dimension; |
53 |
import java.awt.Graphics; |
54 |
import java.awt.Graphics2D; |
55 |
import java.awt.Point; |
56 |
import java.awt.Rectangle; |
57 |
import java.awt.Stroke; |
58 |
import java.awt.datatransfer.Clipboard; |
59 |
import java.awt.datatransfer.DataFlavor; |
60 |
import java.awt.datatransfer.Transferable; |
61 |
import java.awt.datatransfer.UnsupportedFlavorException; |
62 |
import java.awt.event.ActionEvent; |
63 |
import java.awt.event.ActionListener; |
64 |
import java.awt.event.ComponentAdapter; |
65 |
import java.awt.event.ComponentEvent; |
66 |
import java.awt.event.ComponentListener; |
67 |
import java.awt.event.FocusEvent; |
68 |
import java.awt.event.FocusListener; |
69 |
import java.awt.event.InputEvent; |
70 |
import java.awt.event.KeyEvent; |
71 |
import java.awt.event.KeyListener; |
72 |
import java.awt.event.MouseEvent; |
73 |
import java.awt.event.MouseListener; |
74 |
import java.awt.event.MouseMotionListener; |
75 |
import java.beans.PropertyChangeEvent; |
76 |
import java.beans.PropertyChangeListener; |
77 |
import java.io.IOException; |
78 |
import java.util.ArrayList; |
79 |
import java.util.List; |
80 |
import java.util.concurrent.Callable; |
81 |
import java.util.logging.Level; |
82 |
import java.util.logging.Logger; |
83 |
import java.util.prefs.PreferenceChangeEvent; |
84 |
import java.util.prefs.PreferenceChangeListener; |
85 |
import java.util.prefs.Preferences; |
86 |
import javax.swing.Action; |
87 |
import javax.swing.JComponent; |
88 |
import javax.swing.JScrollBar; |
89 |
import javax.swing.JScrollPane; |
90 |
import javax.swing.JViewport; |
91 |
import javax.swing.SwingUtilities; |
92 |
import javax.swing.Timer; |
93 |
import javax.swing.TransferHandler; |
94 |
import javax.swing.event.ChangeEvent; |
95 |
import javax.swing.event.ChangeListener; |
96 |
import javax.swing.event.DocumentEvent; |
97 |
import javax.swing.event.DocumentListener; |
98 |
import javax.swing.text.AbstractDocument; |
99 |
import javax.swing.text.AttributeSet; |
100 |
import javax.swing.text.BadLocationException; |
101 |
import javax.swing.text.Caret; |
102 |
import javax.swing.text.DefaultEditorKit; |
103 |
import javax.swing.text.Document; |
104 |
import javax.swing.text.Element; |
105 |
import javax.swing.text.JTextComponent; |
106 |
import javax.swing.text.Position; |
107 |
import javax.swing.text.StyleConstants; |
108 |
import org.netbeans.api.annotations.common.CheckForNull; |
109 |
import org.netbeans.api.annotations.common.NonNull; |
110 |
import org.netbeans.api.editor.EditorUtilities; |
111 |
import org.netbeans.api.editor.document.AtomicLockDocument; |
112 |
import org.netbeans.api.editor.document.AtomicLockEvent; |
113 |
import org.netbeans.api.editor.document.AtomicLockListener; |
114 |
import org.netbeans.api.editor.document.LineDocument; |
115 |
import org.netbeans.api.editor.document.LineDocumentUtils; |
116 |
import org.netbeans.api.editor.mimelookup.MimeLookup; |
117 |
import org.netbeans.api.editor.settings.FontColorNames; |
118 |
import org.netbeans.api.editor.settings.FontColorSettings; |
119 |
import org.netbeans.api.editor.settings.SimpleValueNames; |
120 |
import org.netbeans.lib.editor.util.ArrayUtilities; |
121 |
import org.netbeans.lib.editor.util.GapList; |
122 |
import org.netbeans.lib.editor.util.ListenerList; |
123 |
import org.netbeans.lib.editor.util.swing.DocumentListenerPriority; |
124 |
import org.netbeans.lib.editor.util.swing.DocumentUtilities; |
125 |
import org.netbeans.modules.editor.lib2.EditorPreferencesDefaults; |
126 |
import org.netbeans.modules.editor.lib2.RectangularSelectionCaretAccessor; |
127 |
import org.netbeans.modules.editor.lib2.RectangularSelectionTransferHandler; |
128 |
import org.netbeans.modules.editor.lib2.RectangularSelectionUtils; |
129 |
import org.netbeans.modules.editor.lib2.actions.EditorActionUtilities; |
130 |
import org.netbeans.modules.editor.lib2.highlighting.CaretOverwriteModeHighlighting; |
131 |
import org.netbeans.modules.editor.lib2.view.DocumentView; |
132 |
import org.netbeans.modules.editor.lib2.view.LockedViewHierarchy; |
133 |
import org.netbeans.modules.editor.lib2.view.ViewHierarchy; |
134 |
import org.netbeans.modules.editor.lib2.view.ViewHierarchyEvent; |
135 |
import org.netbeans.modules.editor.lib2.view.ViewHierarchyListener; |
136 |
import org.netbeans.modules.editor.lib2.view.ViewUtils; |
137 |
import org.openide.util.Exceptions; |
138 |
import org.openide.util.WeakListeners; |
139 |
|
140 |
/** |
141 |
* Extension to standard Swing caret used by all NetBeans editors. |
142 |
* <br> |
143 |
* It supports multi-caret editing mode where an arbitrary number of carets |
144 |
* is placed at arbitrary positions throughout a document. |
145 |
* In this mode each caret is described by its <code>CaretInfo</code> object. |
146 |
* <br> |
147 |
* The caret works over text components having {@link AbstractDocument} based document. |
148 |
* |
149 |
* @author Miloslav Metelka |
150 |
* @author Ralph Ruijs |
151 |
* @since 2.6 |
152 |
*/ |
153 |
public final class EditorCaret implements Caret { |
154 |
|
155 |
// Temporary until rectangular selection gets ported to multi-caret support |
156 |
private static final String RECTANGULAR_SELECTION_PROPERTY = "rectangular-selection"; // NOI18N |
157 |
private static final String RECTANGULAR_SELECTION_REGIONS_PROPERTY = "rectangular-selection-regions"; // NOI18N |
158 |
|
159 |
// -J-Dorg.netbeans.editor.BaseCaret.level=FINEST |
160 |
private static final Logger LOG = Logger.getLogger(EditorCaret.class.getName()); |
161 |
|
162 |
static { |
163 |
RectangularSelectionCaretAccessor.register(new RectangularSelectionCaretAccessor() { |
164 |
@Override |
165 |
public void setRectangularSelectionToDotAndMark(EditorCaret editorCaret) { |
166 |
editorCaret.setRectangularSelectionToDotAndMark(); |
167 |
} |
168 |
|
169 |
@Override |
170 |
public void updateRectangularUpDownSelection(EditorCaret editorCaret) { |
171 |
editorCaret.updateRectangularUpDownSelection(); |
172 |
} |
173 |
|
174 |
@Override |
175 |
public void extendRectangularSelection(EditorCaret editorCaret, boolean toRight, boolean ctrl) { |
176 |
editorCaret.extendRectangularSelection(toRight, ctrl); |
177 |
} |
178 |
}); |
179 |
} |
180 |
|
181 |
static final long serialVersionUID = 0L; |
182 |
|
183 |
/** |
184 |
* Non-empty list of individual carets in the order they were created. |
185 |
* At least one item is always present. |
186 |
*/ |
187 |
private @NonNull GapList<CaretItem> caretItems; |
188 |
|
189 |
/** |
190 |
* Non-empty list of individual carets in the order they were created. |
191 |
* At least one item is always present. |
192 |
*/ |
193 |
private @NonNull GapList<CaretItem> sortedCaretItems; |
194 |
|
195 |
/** |
196 |
* Cached infos corresponding to caret items or null if any of the items were changed |
197 |
* so another copy of the caret items- (translated into caret infos) will get created |
198 |
* upon query to {@link #getCarets() }. |
199 |
* <br> |
200 |
* Once the list gets created both the list content and information in each caret info are immutable. |
201 |
*/ |
202 |
private List<CaretInfo> caretInfos; |
203 |
|
204 |
/** |
205 |
* Cached infos corresponding to sorted caret items or null if any of the sorted items were changed |
206 |
* so another copy of the sorted caret items (translated into caret infos) will get created |
207 |
* upon query to {@link #getSortedCarets() }. |
208 |
* <br> |
209 |
* Once the list gets created both the list content and information in each caret info are immutable. |
210 |
*/ |
211 |
private List<CaretInfo> sortedCaretInfos; |
212 |
|
213 |
/** Component this caret is bound to */ |
214 |
private JTextComponent component; |
215 |
|
216 |
/** List of individual carets */ |
217 |
private boolean overwriteMode; |
218 |
|
219 |
private final ListenerList<EditorCaretListener> listenerList; |
220 |
|
221 |
private final ListenerList<ChangeListener> changeListenerList; |
222 |
|
223 |
/** |
224 |
* Implementor of various listeners. |
225 |
*/ |
226 |
private final ListenerImpl listenerImpl; |
227 |
|
228 |
/** Is the caret visible (after <code>setVisible(true)</code> call? */ |
229 |
private boolean visible; |
230 |
|
231 |
/** |
232 |
* Whether blinking caret is currently visible on the screen. |
233 |
* <br> |
234 |
* This changes from true to false after each tick of a timer |
235 |
* (assuming <code>visible == true</code>). |
236 |
*/ |
237 |
private boolean blinkVisible; |
238 |
|
239 |
/** |
240 |
* Determine if a possible selection would be displayed or not. |
241 |
*/ |
242 |
private boolean selectionVisible; |
243 |
|
244 |
/** Type of the caret */ |
245 |
private CaretType type = CaretType.THICK_LINE_CARET; |
246 |
|
247 |
/** Width of caret */ |
248 |
private int thickCaretWidth = EditorPreferencesDefaults.defaultThickCaretWidth; |
249 |
|
250 |
private MouseState mouseState = MouseState.DEFAULT; |
251 |
|
252 |
/** Timer used for blinking the caret */ |
253 |
private Timer flasher; |
254 |
|
255 |
private Action selectWordAction; |
256 |
private Action selectLineAction; |
257 |
|
258 |
private AbstractDocument activeDoc; |
259 |
|
260 |
private Thread lockThread; |
261 |
|
262 |
private int lockDepth; |
263 |
|
264 |
private CaretTransaction activeTransaction; |
265 |
|
266 |
/** |
267 |
* Items from previous transaction(s) that need their visual rectangle |
268 |
* to get repainted to clear the previous caret representation visually. |
269 |
*/ |
270 |
private GapList<CaretItem> pendingRepaintRemovedItemsList; |
271 |
|
272 |
/** |
273 |
* Items from previous transaction(s) that need their visual bounds |
274 |
* to be recomputed and caret to be repainted then. |
275 |
*/ |
276 |
private GapList<CaretItem> pendingUpdateVisualBoundsItemsList; |
277 |
|
278 |
/** |
279 |
* Caret item to which the view should scroll or null for no scrolling. |
280 |
*/ |
281 |
private CaretItem scrollToItem; |
282 |
|
283 |
/** |
284 |
* Whether the text is being modified under atomic lock. |
285 |
* If so just one caret change is fired at the end of all modifications. |
286 |
*/ |
287 |
private transient boolean inAtomicLock = false; |
288 |
private transient boolean inAtomicUnlock = false; |
289 |
|
290 |
/** |
291 |
* Helps to check whether there was modification performed |
292 |
* and so the caret change needs to be fired. |
293 |
*/ |
294 |
private transient boolean modified; |
295 |
|
296 |
/** |
297 |
* Set to true once the folds have changed. The caret should retain |
298 |
* its relative visual position on the screen. |
299 |
*/ |
300 |
private boolean updateAfterFoldHierarchyChange; |
301 |
|
302 |
/** |
303 |
* Whether at least one typing change occurred during possibly several atomic operations. |
304 |
*/ |
305 |
private boolean typingModificationOccurred; |
306 |
|
307 |
private Preferences prefs = null; |
308 |
|
309 |
private PreferenceChangeListener weakPrefsListener = null; |
310 |
|
311 |
private boolean caretUpdatePending; |
312 |
|
313 |
/** |
314 |
* Minimum selection start for word and line selections. |
315 |
* This helps to ensure that when extending word (or line) selections |
316 |
* the selection will always include at least the initially selected word (or line). |
317 |
*/ |
318 |
private int minSelectionStartOffset; |
319 |
|
320 |
private int minSelectionEndOffset; |
321 |
|
322 |
private boolean rectangularSelection; |
323 |
|
324 |
/** |
325 |
* Rectangle that corresponds to model2View of current point of selection. |
326 |
*/ |
327 |
private Rectangle rsDotRect; |
328 |
|
329 |
/** |
330 |
* Rectangle that corresponds to model2View of beginning of selection. |
331 |
*/ |
332 |
private Rectangle rsMarkRect; |
333 |
|
334 |
/** |
335 |
* Rectangle marking rectangular selection. |
336 |
*/ |
337 |
private Rectangle rsPaintRect; |
338 |
|
339 |
/** |
340 |
* List of start-pos and end-pos pairs that denote rectangular selection |
341 |
* on the selected lines. |
342 |
*/ |
343 |
private List<Position> rsRegions; |
344 |
|
345 |
/** |
346 |
* Used for showing the default cursor instead of the text cursor when the |
347 |
* mouse is over a block of selected text. |
348 |
* This field is used to prevent repeated calls to component.setCursor() |
349 |
* with the same cursor. |
350 |
*/ |
351 |
private boolean showingTextCursor = true; |
352 |
|
353 |
public EditorCaret() { |
354 |
caretItems = new GapList<>(); |
355 |
sortedCaretItems = new GapList<>(); |
356 |
CaretItem singleCaret = new CaretItem(this, null, null); |
357 |
caretItems.add(singleCaret); |
358 |
sortedCaretItems.add(singleCaret); |
359 |
|
360 |
listenerList = new ListenerList<>(); |
361 |
changeListenerList = new ListenerList<>(); |
362 |
listenerImpl = new ListenerImpl(); |
363 |
} |
364 |
|
365 |
@Override |
366 |
public int getDot() { |
367 |
return getLastCaret().getDot(); |
368 |
} |
369 |
|
370 |
@Override |
371 |
public int getMark() { |
372 |
return getLastCaret().getMark(); |
373 |
} |
374 |
|
375 |
/** |
376 |
* Get information about all existing carets in the order they were created. |
377 |
* <br> |
378 |
* The list always has at least one item. The last caret (last item of the list) |
379 |
* is the most recent caret. |
380 |
* <br> |
381 |
* The list is a snapshot of the current state of the carets. The list content itself and its contained |
382 |
* caret infos are guaranteed not change after subsequent calls to caret API or document modifications. |
383 |
* <br> |
384 |
* The list is nonmodifiable. |
385 |
* <br> |
386 |
* This method should be called with document's read-lock acquired which will guarantee |
387 |
* stability of {@link CaretInfo#getDot() } and {@link CaretInfo#getMark() } and prevent |
388 |
* caret merging as a possible effect of document modifications. |
389 |
* |
390 |
* @return copy of caret list with size >= 1 containing information about all carets. |
391 |
*/ |
392 |
public @NonNull List<CaretInfo> getCarets() { |
393 |
synchronized (listenerList) { |
394 |
if (caretInfos == null) { |
395 |
int i = caretItems.size(); |
396 |
CaretInfo[] infos = new CaretInfo[i--]; |
397 |
for (; i >= 0; i--) { |
398 |
infos[i] = caretItems.get(i).getValidInfo(); |
399 |
} |
400 |
caretInfos = ArrayUtilities.unmodifiableList(infos); |
401 |
} |
402 |
return caretInfos; |
403 |
} |
404 |
} |
405 |
|
406 |
/** |
407 |
* Get information about all existing carets sorted by dot positions in ascending order. |
408 |
* <br> |
409 |
* The list is a snapshot of the current state of the carets. The list content itself and its contained |
410 |
* caret infos are guaranteed not change after subsequent calls to caret API or document modifications. |
411 |
* <br> |
412 |
* The list is nonmodifiable. |
413 |
* <br> |
414 |
* This method should be called with document's read-lock acquired which will guarantee |
415 |
* stability of {@link CaretInfo#getDot() } and {@link CaretInfo#getMark() } and prevent |
416 |
* caret merging as a possible effect of document modifications. |
417 |
* |
418 |
* @return copy of caret list with size >= 1 sorted by dot positions in ascending order. |
419 |
*/ |
420 |
public @NonNull List<CaretInfo> getSortedCarets() { |
421 |
synchronized (listenerList) { |
422 |
if (sortedCaretInfos == null) { |
423 |
int i = sortedCaretItems.size(); |
424 |
CaretInfo[] sortedInfos = new CaretInfo[i--]; |
425 |
for (; i >= 0; i--) { |
426 |
sortedInfos[i] = sortedCaretItems.get(i).getValidInfo(); |
427 |
} |
428 |
sortedCaretInfos = ArrayUtilities.unmodifiableList(sortedInfos); |
429 |
} |
430 |
return sortedCaretInfos; |
431 |
} |
432 |
} |
433 |
|
434 |
/** |
435 |
* Get info about the most recently created caret. |
436 |
* <br> |
437 |
* For normal mode this is the only caret returned by {@link #getCarets() }. |
438 |
* <br> |
439 |
* For multi-caret mode this is the last item in the list returned by {@link #getCarets() }. |
440 |
* |
441 |
* @return last caret (the most recently added caret). |
442 |
*/ |
443 |
public @NonNull CaretInfo getLastCaret() { |
444 |
synchronized (listenerList) { |
445 |
return caretItems.get(caretItems.size() - 1).getValidInfo(); |
446 |
} |
447 |
} |
448 |
|
449 |
/** |
450 |
* Get information about the caret at the specified offset. |
451 |
* |
452 |
* @param offset the offset of the caret |
453 |
* @return CaretInfo for the caret at offset, null if there is no caret or |
454 |
* the offset is invalid |
455 |
*/ |
456 |
public @CheckForNull CaretInfo getCaretAt(int offset) { |
457 |
return null; // TBD |
458 |
} |
459 |
|
460 |
/** |
461 |
* Assign a new offset to the caret in the underlying document. |
462 |
* <br> |
463 |
* This method implicitly sets the selection range to zero. |
464 |
* <br> |
465 |
* If multiple carets are present this method retains only last caret |
466 |
* which then moves to the given offset. |
467 |
* |
468 |
* @param offset {@inheritDoc} |
469 |
* @see Caret#setDot(int) |
470 |
*/ |
471 |
public @Override void setDot(final int offset) { |
472 |
if (LOG.isLoggable(Level.FINE)) { |
473 |
LOG.fine("setDot: offset=" + offset); //NOI18N |
474 |
if (LOG.isLoggable(Level.FINEST)) { |
475 |
LOG.log(Level.INFO, "setDot call stack", new Exception()); |
476 |
} |
477 |
} |
478 |
runTransaction(CaretTransaction.RemoveType.RETAIN_LAST_CARET, 0, null, new CaretMoveHandler() { |
479 |
@Override |
480 |
public void moveCarets(CaretMoveContext context) { |
481 |
Document doc = context.getComponent().getDocument(); |
482 |
if (doc != null) { |
483 |
try { |
484 |
Position pos = doc.createPosition(offset); |
485 |
context.setDot(context.getOriginalLastCaret(), pos); |
486 |
} catch (BadLocationException ex) { |
487 |
// Ignore the setDot() request |
488 |
} |
489 |
} |
490 |
} |
491 |
}); |
492 |
} |
493 |
|
494 |
/** |
495 |
* Moves the caret position (dot) to some other position, leaving behind the |
496 |
* mark. This is useful for making selections. |
497 |
* <br> |
498 |
* If multiple carets are present this method retains all carets |
499 |
* and moves the dot of the last caret to the given offset. |
500 |
* |
501 |
* @param offset {@inheritDoc} |
502 |
* @see Caret#moveDot(int) |
503 |
*/ |
504 |
public @Override void moveDot(final int offset) { |
505 |
if (LOG.isLoggable(Level.FINE)) { |
506 |
LOG.fine("moveDot: offset=" + offset); //NOI18N |
507 |
} |
508 |
|
509 |
runTransaction(CaretTransaction.RemoveType.NO_REMOVE, 0, null, new CaretMoveHandler() { |
510 |
@Override |
511 |
public void moveCarets(CaretMoveContext context) { |
512 |
Document doc = context.getComponent().getDocument(); |
513 |
if (doc != null) { |
514 |
try { |
515 |
Position pos = doc.createPosition(offset); |
516 |
context.moveDot(context.getOriginalLastCaret(), pos); |
517 |
} catch (BadLocationException ex) { |
518 |
// Ignore the setDot() request |
519 |
} |
520 |
} |
521 |
} |
522 |
}); |
523 |
} |
524 |
|
525 |
/** |
526 |
* Move multiple carets or create/modify selections. |
527 |
* <br> |
528 |
* For performance reasons this is made as a single transaction over the caret |
529 |
* with only one change event being fired. |
530 |
* <br> |
531 |
* Note that the move handler does not permit to add or remove carets - this has to be performed |
532 |
* by other methods present in this class (as another transaction over the editor caret). |
533 |
* <br> |
534 |
* <pre> |
535 |
* <code> |
536 |
* // Go one line up with all carets |
537 |
* editorCaret.moveCarets(new CaretMoveHandler() { |
538 |
* @Override public void moveCarets(CaretMoveContext context) { |
539 |
* for (CaretInfo caretInfo : context.getOriginalSortedCarets()) { |
540 |
* try { |
541 |
* int dot = caretInfo.getDot(); |
542 |
* dot = Utilities.getPositionAbove(target, dot, p.x); |
543 |
* Position dotPos = doc.createPosition(dot); |
544 |
* context.setDot(caretInfo, dotPos); |
545 |
* } catch (BadLocationException e) { |
546 |
* // the position stays the same |
547 |
* } |
548 |
* } |
549 |
* } |
550 |
* }); |
551 |
* </code> |
552 |
* </pre> |
553 |
* |
554 |
* @param moveHandler non-null move handler to perform the changes. The handler's methods |
555 |
* will be given a context to operate on. |
556 |
* @return difference between current count of carets and the number of carets when the operation started. |
557 |
* Returns Integer.MIN_VALUE if the operation was cancelled due to the caret not being installed in any text component |
558 |
* or no document installed in the text component. |
559 |
*/ |
560 |
public int moveCarets(@NonNull CaretMoveHandler moveHandler) { |
561 |
return runTransaction(CaretTransaction.RemoveType.NO_REMOVE, 0, null, moveHandler); |
562 |
} |
563 |
|
564 |
/** |
565 |
* Create a new caret at the given position with a possible selection. |
566 |
* <br> |
567 |
* The caret will become the last caret of the list returned by {@link #getCarets() }. |
568 |
* <br> |
569 |
* This method requires the caller to have either read lock or write lock acquired |
570 |
* over the underlying document. |
571 |
* <br> |
572 |
* <pre> |
573 |
* <code> |
574 |
* editorCaret.addCaret(pos, pos); // Add a new caret at pos.getOffset() |
575 |
* |
576 |
* Position pos2 = doc.createPosition(pos.getOffset() + 2); |
577 |
* // Add a new caret with selection starting at pos and extending to pos2 with caret located at pos2 |
578 |
* editorCaret.addCaret(pos2, pos); |
579 |
* // Add a new caret with selection starting at pos and extending to pos2 with caret located at pos |
580 |
* editorCaret.addCaret(pos, pos2); |
581 |
* </code> |
582 |
* </pre> |
583 |
* |
584 |
* @param dotPos position of the newly created caret. |
585 |
* @param markPos beginning of the selection (the other end is dotPos) or the same position like dotPos for no selection. |
586 |
* The markPos may have higher offset than dotPos to select in a backward direction. |
587 |
* @return difference between current count of carets and the number of carets when the operation started. |
588 |
* Returns Integer.MIN_VALUE if the operation was cancelled due to the caret not being installed in any text component |
589 |
* or no document installed in the text component. |
590 |
* <br> |
591 |
* Note that adding a new caret to offset where another caret is already located may lead |
592 |
* to its immediate removal. |
593 |
*/ |
594 |
public int addCaret(@NonNull Position dotPos, @NonNull Position markPos) { |
595 |
return runTransaction(CaretTransaction.RemoveType.NO_REMOVE, 0, |
596 |
new CaretItem[] { new CaretItem(this, dotPos, markPos) }, null); |
597 |
} |
598 |
|
599 |
/** |
600 |
* Add multiple carets at once. |
601 |
* <br> |
602 |
* It is similar to calling {@link #addCaret(javax.swing.text.Position, javax.swing.text.Position) } |
603 |
* multiple times but this method is more efficient (it only fires caret change once). |
604 |
* <br> |
605 |
* This method requires the caller to have either read lock or write lock acquired |
606 |
* over the underlying document. |
607 |
* <br> |
608 |
* <pre> |
609 |
* <code> |
610 |
* List<Position> pairs = new ArrayList<>(); |
611 |
* pairs.add(dotPos); |
612 |
* pairs.add(dotPos); |
613 |
* pairs.add(dot2Pos); |
614 |
* pairs.add(mark2Pos); |
615 |
* // Add caret located at dotPos.getOffset() and another one with selection |
616 |
* // starting at mark2Pos and extending to dot2Pos with caret located at dot2Pos |
617 |
* editorCaret.addCaret(pairs); |
618 |
* </code> |
619 |
* </pre> |
620 |
* |
621 |
* @param dotAndMarkPosPairs list of position pairs consisting of dot position |
622 |
* and mark position (selection start position) which may be the same position like the dot |
623 |
* if the particular caret has no selection. The list must have even size. |
624 |
* @return difference between current count of carets and the number of carets when the operation started. |
625 |
* Returns Integer.MIN_VALUE if the operation was cancelled due to the caret not being installed in any text component |
626 |
* or no document installed in the text component. |
627 |
*/ |
628 |
public int addCarets(@NonNull List<Position> dotAndMarkPosPairs) { |
629 |
return runTransaction(CaretTransaction.RemoveType.NO_REMOVE, 0, |
630 |
CaretTransaction.asCaretItems(this, dotAndMarkPosPairs), null); |
631 |
} |
632 |
|
633 |
/** |
634 |
* Replace all current carets with the new ones. |
635 |
* <br> |
636 |
* This method requires the caller to have either read lock or write lock acquired |
637 |
* over the underlying document. |
638 |
* <br> |
639 |
* @param dotAndMarkPosPairs list of position pairs consisting of dot position |
640 |
* and mark position (selection start position) which may be the same position like dot |
641 |
* if the particular caret has no selection. The list must have even size. |
642 |
* @return difference between current count of carets and the number of carets when the operation started. |
643 |
* Returns Integer.MIN_VALUE if the operation was cancelled due to the caret not being installed in any text component |
644 |
* or no document installed in the text component. |
645 |
*/ |
646 |
public int replaceCarets(@NonNull List<Position> dotAndMarkPosPairs) { |
647 |
if (dotAndMarkPosPairs.isEmpty()) { |
648 |
throw new IllegalArgumentException("dotAndSelectionStartPosPairs list must not be empty"); |
649 |
} |
650 |
CaretItem[] addedItems = CaretTransaction.asCaretItems(this, dotAndMarkPosPairs); |
651 |
return runTransaction(CaretTransaction.RemoveType.REMOVE_ALL_CARETS, 0, addedItems, null); |
652 |
} |
653 |
|
654 |
/** |
655 |
* Remove last added caret (determined by {@link #getLastCaret() }). |
656 |
* <br> |
657 |
* If there is just one caret the method has no effect. |
658 |
* |
659 |
* @return difference between current count of carets and the number of carets when the operation started. |
660 |
* Returns Integer.MIN_VALUE if the operation was cancelled due to the caret not being installed in any text component |
661 |
* or no document installed in the text component. |
662 |
*/ |
663 |
public int removeLastCaret() { |
664 |
return runTransaction(CaretTransaction.RemoveType.REMOVE_LAST_CARET, 0, null, null); |
665 |
} |
666 |
|
667 |
/** |
668 |
* Switch to single caret mode by removing all carets except the last caret. |
669 |
* @return difference between current count of carets and the number of carets when the operation started. |
670 |
* Returns Integer.MIN_VALUE if the operation was cancelled due to the caret not being installed in any text component |
671 |
* or no document installed in the text component. |
672 |
*/ |
673 |
public int retainLastCaretOnly() { |
674 |
return runTransaction(CaretTransaction.RemoveType.RETAIN_LAST_CARET, 0, null, null); |
675 |
} |
676 |
|
677 |
/** |
678 |
* Adds listener to track caret changes in detail. |
679 |
* |
680 |
* @param listener non-null listener. |
681 |
*/ |
682 |
public void addEditorCaretListener(@NonNull EditorCaretListener listener) { |
683 |
listenerList.add(listener); |
684 |
} |
685 |
|
686 |
/** |
687 |
* Adds listener to track caret position changes (to fulfil {@link Caret} interface). |
688 |
*/ |
689 |
@Override |
690 |
public void addChangeListener(@NonNull ChangeListener l) { |
691 |
changeListenerList.add(l); |
692 |
} |
693 |
|
694 |
public void removeEditorCaretListener(@NonNull EditorCaretListener listener) { |
695 |
listenerList.remove(listener); |
696 |
} |
697 |
|
698 |
/** |
699 |
* Removes listener to track caret position changes (to fulfil {@link Caret} interface). |
700 |
*/ |
701 |
@Override |
702 |
public void removeChangeListener(@NonNull ChangeListener l) { |
703 |
changeListenerList.remove(l); |
704 |
} |
705 |
|
706 |
/** |
707 |
* Determines if the caret is currently visible (it may be blinking depending on settings). |
708 |
* <p> |
709 |
* Caret becomes visible after <code>setVisible(true)</code> gets called on it. |
710 |
* |
711 |
* @return <code>true</code> if visible else <code>false</code> |
712 |
*/ |
713 |
@Override |
714 |
public boolean isVisible() { |
715 |
synchronized (listenerList) { |
716 |
return visible; |
717 |
} |
718 |
} |
719 |
|
720 |
/** |
721 |
* Sets the caret visibility, and repaints the caret. |
722 |
* |
723 |
* @param visible the visibility specifier |
724 |
* @see Caret#setVisible |
725 |
*/ |
726 |
@Override |
727 |
public void setVisible(boolean visible) { |
728 |
if (LOG.isLoggable(Level.FINER)) { |
729 |
LOG.finer("BaseCaret.setVisible(" + visible + ")\n"); |
730 |
if (LOG.isLoggable(Level.FINEST)) { |
731 |
LOG.log(Level.INFO, "", new Exception()); |
732 |
} |
733 |
} |
734 |
synchronized (listenerList) { |
735 |
if (flasher != null) { |
736 |
if (this.visible) { |
737 |
flasher.stop(); |
738 |
} |
739 |
if (LOG.isLoggable(Level.FINER)) { |
740 |
LOG.finer((visible ? "Starting" : "Stopping") + // NOI18N |
741 |
" the caret blinking timer: " + dumpVisibility() + '\n'); // NOI18N |
742 |
} |
743 |
this.visible = visible; |
744 |
if (visible) { |
745 |
flasher.start(); |
746 |
} else { |
747 |
flasher.stop(); |
748 |
} |
749 |
} |
750 |
} |
751 |
JTextComponent c = component; |
752 |
if (c != null) { |
753 |
// TODO only paint carets showing on screen |
754 |
List<CaretInfo> sortedCarets = getSortedCarets(); |
755 |
for (CaretInfo caret : sortedCarets) { |
756 |
CaretItem caretItem = caret.getCaretItem(); |
757 |
if (caretItem.getCaretBounds() != null) { |
758 |
Rectangle repaintRect = caretItem.getCaretBounds(); |
759 |
c.repaint(repaintRect); |
760 |
} |
761 |
} |
762 |
} |
763 |
} |
764 |
|
765 |
@Override |
766 |
public boolean isSelectionVisible() { |
767 |
return selectionVisible; |
768 |
} |
769 |
|
770 |
@Override |
771 |
public void setSelectionVisible(boolean v) { |
772 |
if (selectionVisible == v) { |
773 |
return; |
774 |
} |
775 |
JTextComponent c = component; |
776 |
Document doc; |
777 |
if (c != null && (doc = c.getDocument()) != null) { |
778 |
selectionVisible = v; |
779 |
// [TODO] ensure to repaint |
780 |
} |
781 |
} |
782 |
|
783 |
@Override |
784 |
public void install(JTextComponent c) { |
785 |
assert (SwingUtilities.isEventDispatchThread()); // must be done in AWT |
786 |
if (LOG.isLoggable(Level.FINE)) { |
787 |
LOG.fine("Installing to " + s2s(c)); //NOI18N |
788 |
} |
789 |
|
790 |
component = c; |
791 |
visible = true; |
792 |
modelChanged(null, c.getDocument()); |
793 |
|
794 |
Boolean b = (Boolean) c.getClientProperty(EditorUtilities.CARET_OVERWRITE_MODE_PROPERTY); |
795 |
overwriteMode = (b != null) ? b : false; |
796 |
updateOverwriteModeLayer(true); |
797 |
setBlinkVisible(true); |
798 |
|
799 |
// Attempt to assign initial bounds - usually here the component |
800 |
// is not yet added to the component hierarchy. |
801 |
updateAllCaretsBounds(); |
802 |
|
803 |
if(getLastCaretItem().getCaretBounds() == null) { |
804 |
// For null bounds wait for the component to get resized |
805 |
// and attempt to recompute bounds then |
806 |
component.addComponentListener(listenerImpl); |
807 |
} |
808 |
|
809 |
component.addPropertyChangeListener(listenerImpl); |
810 |
component.addFocusListener(listenerImpl); |
811 |
component.addMouseListener(listenerImpl); |
812 |
component.addMouseMotionListener(listenerImpl); |
813 |
component.addKeyListener(listenerImpl); |
814 |
ViewHierarchy.get(component).addViewHierarchyListener(listenerImpl); |
815 |
|
816 |
if (component.hasFocus()) { |
817 |
if (LOG.isLoggable(Level.FINE)) { |
818 |
LOG.fine("Component has focus, calling BaseCaret.focusGained(); doc=" // NOI18N |
819 |
+ component.getDocument().getProperty(Document.TitleProperty) + '\n'); |
820 |
} |
821 |
listenerImpl.focusGained(null); // emulate focus gained |
822 |
} |
823 |
|
824 |
dispatchUpdate(false); |
825 |
} |
826 |
|
827 |
@Override |
828 |
public void deinstall(JTextComponent c) { |
829 |
if (LOG.isLoggable(Level.FINE)) { |
830 |
LOG.fine("Deinstalling from " + s2s(c)); //NOI18N |
831 |
} |
832 |
|
833 |
synchronized (listenerList) { |
834 |
if (flasher != null) { |
835 |
setBlinkRate(0); |
836 |
} |
837 |
} |
838 |
|
839 |
c.removeComponentListener(listenerImpl); |
840 |
c.removePropertyChangeListener(listenerImpl); |
841 |
c.removeFocusListener(listenerImpl); |
842 |
c.removeMouseListener(listenerImpl); |
843 |
c.removeMouseMotionListener(listenerImpl); |
844 |
ViewHierarchy.get(c).removeViewHierarchyListener(listenerImpl); |
845 |
|
846 |
|
847 |
modelChanged(activeDoc, null); |
848 |
} |
849 |
|
850 |
@Override |
851 |
public void paint(Graphics g) { |
852 |
JTextComponent c = component; |
853 |
if (c == null) return; |
854 |
|
855 |
// Check whether the caret was moved but the component was not |
856 |
// validated yet and therefore the caret bounds are still null |
857 |
// and if so compute the bounds and scroll the view if necessary. |
858 |
// TODO - could this be done by an extra flag rather than bounds checking?? |
859 |
CaretItem lastCaret = getLastCaretItem(); |
860 |
if (getDot() != 0 && lastCaret.getCaretBounds() == null) { |
861 |
update(true); |
862 |
} |
863 |
|
864 |
List<CaretInfo> carets = getSortedCarets(); |
865 |
for (CaretInfo caretInfo : carets) { // TODO only paint the items in the clipped area - use binary search to located first item |
866 |
CaretItem caretItem = caretInfo.getCaretItem(); |
867 |
if (LOG.isLoggable(Level.FINEST)) { |
868 |
LOG.finest("BaseCaret.paint(): caretBounds=" + caretItem.getCaretBounds() + dumpVisibility() + '\n'); |
869 |
} |
870 |
if (caretItem.getCaretBounds() != null && isVisible() && blinkVisible) { |
871 |
paintCaret(g, caretItem); |
872 |
} |
873 |
if (rectangularSelection && rsPaintRect != null && g instanceof Graphics2D) { |
874 |
Graphics2D g2d = (Graphics2D) g; |
875 |
Stroke stroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[] {4, 2}, 0); |
876 |
Stroke origStroke = g2d.getStroke(); |
877 |
Color origColor = g2d.getColor(); |
878 |
try { |
879 |
// Render translucent rectangle |
880 |
Color selColor = c.getSelectionColor(); |
881 |
g2d.setColor(selColor); |
882 |
Composite origComposite = g2d.getComposite(); |
883 |
try { |
884 |
g2d.setComposite(AlphaComposite.SrcOver.derive(0.2f)); |
885 |
g2d.fill(rsPaintRect); |
886 |
} finally { |
887 |
g2d.setComposite(origComposite); |
888 |
} |
889 |
// Paint stroked line around rectangular selection rectangle |
890 |
g.setColor(c.getCaretColor()); |
891 |
g2d.setStroke(stroke); |
892 |
Rectangle onePointSmallerRect = new Rectangle(rsPaintRect); |
893 |
onePointSmallerRect.width--; |
894 |
onePointSmallerRect.height--; |
895 |
g2d.draw(onePointSmallerRect); |
896 |
|
897 |
} finally { |
898 |
g2d.setStroke(origStroke); |
899 |
g2d.setColor(origColor); |
900 |
} |
901 |
} |
902 |
} |
903 |
} |
904 |
|
905 |
public @Override void setMagicCaretPosition(Point p) { |
906 |
getLastCaretItem().setMagicCaretPosition(p); |
907 |
} |
908 |
|
909 |
public @Override final Point getMagicCaretPosition() { |
910 |
return getLastCaretItem().getMagicCaretPosition(); |
911 |
} |
912 |
|
913 |
public @Override void setBlinkRate(int rate) { |
914 |
if (LOG.isLoggable(Level.FINER)) { |
915 |
LOG.finer("setBlinkRate(" + rate + ")" + dumpVisibility() + '\n'); // NOI18N |
916 |
} |
917 |
synchronized (listenerList) { |
918 |
if (flasher == null && rate > 0) { |
919 |
flasher = new Timer(rate, listenerImpl); |
920 |
} |
921 |
if (flasher != null) { |
922 |
if (rate > 0) { |
923 |
if (flasher.getDelay() != rate) { |
924 |
flasher.setDelay(rate); |
925 |
} |
926 |
} else { // zero rate - don't blink |
927 |
flasher.stop(); |
928 |
flasher.removeActionListener(listenerImpl); |
929 |
flasher = null; |
930 |
setBlinkVisible(true); |
931 |
if (LOG.isLoggable(Level.FINER)){ |
932 |
LOG.finer("Zero blink rate - no blinking. flasher=null; blinkVisible=true"); // NOI18N |
933 |
} |
934 |
} |
935 |
} |
936 |
} |
937 |
} |
938 |
|
939 |
@Override |
940 |
public int getBlinkRate() { |
941 |
synchronized (listenerList) { |
942 |
return (flasher != null) ? flasher.getDelay() : 0; |
943 |
} |
944 |
} |
945 |
|
946 |
/** |
947 |
* |
948 |
*/ |
949 |
void setRectangularSelectionToDotAndMark() { |
950 |
int dotOffset = getDot(); |
951 |
int markOffset = getMark(); |
952 |
try { |
953 |
rsDotRect = component.modelToView(dotOffset); |
954 |
rsMarkRect = component.modelToView(markOffset); |
955 |
} catch (BadLocationException ex) { |
956 |
rsDotRect = rsMarkRect = null; |
957 |
} |
958 |
updateRectangularSelectionPaintRect(); |
959 |
} |
960 |
|
961 |
/** |
962 |
* |
963 |
*/ |
964 |
void updateRectangularUpDownSelection() { |
965 |
JTextComponent c = component; |
966 |
int dotOffset = getDot(); |
967 |
try { |
968 |
Rectangle r = c.modelToView(dotOffset); |
969 |
rsDotRect.y = r.y; |
970 |
rsDotRect.height = r.height; |
971 |
} catch (BadLocationException ex) { |
972 |
// Leave rsDotRect unchanged |
973 |
} |
974 |
} |
975 |
|
976 |
/** |
977 |
* Extend rectangular selection either by char in a specified selection |
978 |
* or by word (if ctrl is pressed). |
979 |
* |
980 |
* @param toRight true for right or false for left. |
981 |
* @param ctrl true for ctrl pressed. |
982 |
*/ |
983 |
void extendRectangularSelection(boolean toRight, boolean ctrl) { |
984 |
JTextComponent c = component; |
985 |
Document doc = c.getDocument(); |
986 |
int dotOffset = getDot(); |
987 |
Element lineRoot = doc.getDefaultRootElement(); |
988 |
int lineIndex = lineRoot.getElementIndex(dotOffset); |
989 |
Element lineElement = lineRoot.getElement(lineIndex); |
990 |
float charWidth; |
991 |
LockedViewHierarchy lvh = ViewHierarchy.get(c).lock(); |
992 |
try { |
993 |
charWidth = lvh.getDefaultCharWidth(); |
994 |
} finally { |
995 |
lvh.unlock(); |
996 |
} |
997 |
int newDotOffset = -1; |
998 |
try { |
999 |
int newlineOffset = lineElement.getEndOffset() - 1; |
1000 |
Rectangle newlineRect = c.modelToView(newlineOffset); |
1001 |
if (!ctrl) { |
1002 |
if (toRight) { |
1003 |
if (rsDotRect.x < newlineRect.x) { |
1004 |
newDotOffset = dotOffset + 1; |
1005 |
} else { |
1006 |
rsDotRect.x += charWidth; |
1007 |
} |
1008 |
} else { // toLeft |
1009 |
if (rsDotRect.x > newlineRect.x) { |
1010 |
rsDotRect.x -= charWidth; |
1011 |
if (rsDotRect.x < newlineRect.x) { // Fix on rsDotRect |
1012 |
newDotOffset = newlineOffset; |
1013 |
} |
1014 |
} else { |
1015 |
newDotOffset = Math.max(dotOffset - 1, lineElement.getStartOffset()); |
1016 |
} |
1017 |
} |
1018 |
|
1019 |
} else { // With Ctrl |
1020 |
int numVirtualChars = 8; // Number of virtual characters per one Ctrl+Shift+Arrow press |
1021 |
if (toRight) { |
1022 |
if (rsDotRect.x < newlineRect.x) { |
1023 |
//[TODO] fix newDotOffset = Math.min(Utilities.getNextWord(c, dotOffset), lineElement.getEndOffset() - 1); |
1024 |
} else { // Extend virtually |
1025 |
rsDotRect.x += numVirtualChars * charWidth; |
1026 |
} |
1027 |
} else { // toLeft |
1028 |
if (rsDotRect.x > newlineRect.x) { // Virtually extended |
1029 |
rsDotRect.x -= numVirtualChars * charWidth; |
1030 |
if (rsDotRect.x < newlineRect.x) { |
1031 |
newDotOffset = newlineOffset; |
1032 |
} |
1033 |
} else { |
1034 |
//[TODO] fix newDotOffset = Math.max(Utilities.getPreviousWord(c, dotOffset), lineElement.getStartOffset()); |
1035 |
} |
1036 |
} |
1037 |
} |
1038 |
|
1039 |
if (newDotOffset != -1) { |
1040 |
rsDotRect = c.modelToView(newDotOffset); |
1041 |
moveDot(newDotOffset); // updates rs and fires state change |
1042 |
} else { |
1043 |
updateRectangularSelectionPaintRect(); |
1044 |
fireStateChanged(); |
1045 |
} |
1046 |
} catch (BadLocationException ex) { |
1047 |
// Leave selection as is |
1048 |
} |
1049 |
} |
1050 |
|
1051 |
|
1052 |
// Private implementation |
1053 |
|
1054 |
/** This method should only be accessed by transaction's methods */ |
1055 |
GapList<CaretItem> getCaretItems() { |
1056 |
return caretItems; // No sync as this should only be accessed by transaction's methods |
1057 |
} |
1058 |
|
1059 |
/** This method should only be accessed by transaction's methods */ |
1060 |
GapList<CaretItem> getSortedCaretItems() { |
1061 |
return sortedCaretItems; // No sync as this should only be accessed by transaction's methods |
1062 |
} |
1063 |
|
1064 |
/** This method may be accessed arbitrarily */ |
1065 |
private CaretItem getLastCaretItem() { |
1066 |
synchronized (listenerList) { |
1067 |
return caretItems.get(caretItems.size() - 1); |
1068 |
} |
1069 |
} |
1070 |
|
1071 |
/** |
1072 |
* Run a transaction to modify number of carets or their dots or selections. |
1073 |
* @param removeType type of carets removal. |
1074 |
* @param offset offset of document text removal otherwise the value is ignored. |
1075 |
* Internally this is also used for an end offset of the insertion at offset zero once it happens. |
1076 |
* @param addCarets carets to be added if any. |
1077 |
* @param moveHandler caret move handler or null if there's no caret moving. API client's move handlers |
1078 |
* are only invoked without any extra removals or additions so the original caret infos are used. |
1079 |
* Internal transactions may use caret additions or removals together with caret move handlers |
1080 |
* but they are also passed with original caret infos so the handlers must be aware of the removal |
1081 |
* and only pick the valid non-removed items. |
1082 |
* @return |
1083 |
*/ |
1084 |
private int runTransaction(CaretTransaction.RemoveType removeType, int offset, CaretItem[] addCarets, CaretMoveHandler moveHandler) { |
1085 |
lock(); |
1086 |
try { |
1087 |
if (activeTransaction == null) { |
1088 |
JTextComponent c = component; |
1089 |
Document d = activeDoc; |
1090 |
if (c != null && d != null) { |
1091 |
activeTransaction = new CaretTransaction(this, c, d); |
1092 |
try { |
1093 |
activeTransaction.replaceCarets(removeType, offset, addCarets); |
1094 |
if (moveHandler != null) { |
1095 |
activeTransaction.runCaretMoveHandler(moveHandler); |
1096 |
} |
1097 |
activeTransaction.removeOverlappingRegions(); |
1098 |
int diffCount = 0; |
1099 |
synchronized (listenerList) { |
1100 |
GapList<CaretItem> replaceItems = activeTransaction.getReplaceItems(); |
1101 |
if (replaceItems != null) { |
1102 |
diffCount = replaceItems.size() - caretItems.size(); |
1103 |
caretItems = replaceItems; |
1104 |
sortedCaretItems = activeTransaction.getSortedCaretItems(); |
1105 |
assert (sortedCaretItems != null) : "Null sortedCaretItems! removeType=" + removeType; // NOI18N |
1106 |
} |
1107 |
} |
1108 |
if (activeTransaction.isAnyChange()) { |
1109 |
caretInfos = null; |
1110 |
sortedCaretInfos = null; |
1111 |
} |
1112 |
pendingRepaintRemovedItemsList = activeTransaction. |
1113 |
addRemovedItems(pendingRepaintRemovedItemsList); |
1114 |
pendingUpdateVisualBoundsItemsList = activeTransaction. |
1115 |
addUpdateVisualBoundsItems(pendingUpdateVisualBoundsItemsList); |
1116 |
if (pendingUpdateVisualBoundsItemsList != null || pendingRepaintRemovedItemsList != null) { |
1117 |
// For now clear the lists and use old way TODO update to selective updating and rendering |
1118 |
fireStateChanged(); |
1119 |
dispatchUpdate(true); |
1120 |
pendingRepaintRemovedItemsList = null; |
1121 |
pendingUpdateVisualBoundsItemsList = null; |
1122 |
} |
1123 |
return diffCount; |
1124 |
} finally { |
1125 |
activeTransaction = null; |
1126 |
} |
1127 |
} |
1128 |
return Integer.MIN_VALUE; |
1129 |
|
1130 |
} else { // Nested transaction - document insert/remove within transaction |
1131 |
switch (removeType) { |
1132 |
case DOCUMENT_REMOVE: |
1133 |
activeTransaction.documentRemove(offset); |
1134 |
break; |
1135 |
case DOCUMENT_INSERT_ZERO_OFFSET: |
1136 |
activeTransaction.documentInsertAtZeroOffset(offset); |
1137 |
break; |
1138 |
default: |
1139 |
throw new AssertionError("Unsupported removeType=" + removeType + " in nested transaction"); // NOI18N |
1140 |
} |
1141 |
return 0; |
1142 |
} |
1143 |
} finally { |
1144 |
unlock(); |
1145 |
} |
1146 |
} |
1147 |
|
1148 |
private void moveDotCaret(int offset, CaretItem caret) throws IllegalStateException { |
1149 |
JTextComponent c = component; |
1150 |
AbstractDocument doc; |
1151 |
if (c != null && (doc = activeDoc) != null) { |
1152 |
if (offset >= 0 && offset <= doc.getLength()) { |
1153 |
doc.readLock(); |
1154 |
try { |
1155 |
int oldCaretPos = caret.getDot(); |
1156 |
if (offset == oldCaretPos) { // no change |
1157 |
return; |
1158 |
} |
1159 |
caret.setDotPos(doc.createPosition(offset)); |
1160 |
// Selection highlighting should be handled automatically by highlighting layers |
1161 |
if (rectangularSelection) { |
1162 |
Rectangle r = c.modelToView(offset); |
1163 |
if (rsDotRect != null) { |
1164 |
rsDotRect.y = r.y; |
1165 |
rsDotRect.height = r.height; |
1166 |
} else { |
1167 |
rsDotRect = r; |
1168 |
} |
1169 |
updateRectangularSelectionPaintRect(); |
1170 |
} |
1171 |
} catch (BadLocationException e) { |
1172 |
throw new IllegalStateException(e.toString()); |
1173 |
// position is incorrect |
1174 |
} finally { |
1175 |
doc.readUnlock(); |
1176 |
} |
1177 |
} |
1178 |
fireStateChanged(); |
1179 |
dispatchUpdate(true); |
1180 |
} |
1181 |
} |
1182 |
|
1183 |
private void fireEditorCaretChange(EditorCaretEvent evt) { |
1184 |
for (EditorCaretListener listener : listenerList.getListeners()) { |
1185 |
listener.caretChanged(evt); |
1186 |
} |
1187 |
} |
1188 |
|
1189 |
/** |
1190 |
* Notifies listeners that caret position has changed. |
1191 |
*/ |
1192 |
private void fireStateChanged() { |
1193 |
Runnable runnable = new Runnable() { |
1194 |
public @Override void run() { |
1195 |
JTextComponent c = component; |
1196 |
if (c == null || c.getCaret() != EditorCaret.this) { |
1197 |
return; |
1198 |
} |
1199 |
fireEditorCaretChange(new EditorCaretEvent(EditorCaret.this, 0, Integer.MAX_VALUE)); // [TODO] temp firing without detailed info |
1200 |
ChangeEvent evt = new ChangeEvent(EditorCaret.this); |
1201 |
List<ChangeListener> listeners = changeListenerList.getListeners(); |
1202 |
for (ChangeListener l : listeners) { |
1203 |
l.stateChanged(evt); |
1204 |
} |
1205 |
} |
1206 |
}; |
1207 |
|
1208 |
// Always fire in EDT |
1209 |
if (inAtomicUnlock) { // Cannot fire within atomic lock9 |
1210 |
SwingUtilities.invokeLater(runnable); |
1211 |
} else { |
1212 |
ViewUtils.runInEDT(runnable); |
1213 |
} |
1214 |
updateSystemSelection(); |
1215 |
} |
1216 |
|
1217 |
private void lock() { |
1218 |
Thread curThread = Thread.currentThread(); |
1219 |
try { |
1220 |
synchronized (listenerList) { |
1221 |
while (lockThread != null) { |
1222 |
if (curThread == lockThread) { |
1223 |
lockDepth++; |
1224 |
return; |
1225 |
} |
1226 |
listenerList.wait(); |
1227 |
} |
1228 |
lockThread = curThread; |
1229 |
lockDepth = 1; |
1230 |
|
1231 |
} |
1232 |
} catch (InterruptedException e) { |
1233 |
throw new Error("Interrupted attempt to acquire lock"); // NOI18N |
1234 |
} |
1235 |
} |
1236 |
|
1237 |
private void unlock() { |
1238 |
Thread curThread = Thread.currentThread(); |
1239 |
synchronized (listenerList) { |
1240 |
if (lockThread == curThread) { |
1241 |
lockDepth--; |
1242 |
if (lockDepth == 0) { |
1243 |
lockThread = null; |
1244 |
listenerList.notifyAll(); |
1245 |
} |
1246 |
} else { |
1247 |
throw new IllegalStateException("Invalid thread called EditorCaret.unlock(): thread=" + // NOI18N |
1248 |
curThread + ", lockThread=" + lockThread); // NOI18N |
1249 |
} |
1250 |
} |
1251 |
} |
1252 |
|
1253 |
private void updateType() { |
1254 |
final JTextComponent c = component; |
1255 |
if (c != null) { |
1256 |
Color caretColor = null; |
1257 |
CaretType newType; |
1258 |
boolean cIsTextField = Boolean.TRUE.equals(c.getClientProperty("AsTextField")); |
1259 |
|
1260 |
if (cIsTextField) { |
1261 |
newType = CaretType.THIN_LINE_CARET; |
1262 |
} else if (prefs != null) { |
1263 |
String newTypeStr; |
1264 |
if (overwriteMode) { |
1265 |
newTypeStr = prefs.get(SimpleValueNames.CARET_TYPE_OVERWRITE_MODE, EditorPreferencesDefaults.defaultCaretTypeOverwriteMode); |
1266 |
} else { // insert mode |
1267 |
newTypeStr = prefs.get(SimpleValueNames.CARET_TYPE_INSERT_MODE, EditorPreferencesDefaults.defaultCaretTypeInsertMode); |
1268 |
this.thickCaretWidth = prefs.getInt(SimpleValueNames.THICK_CARET_WIDTH, EditorPreferencesDefaults.defaultThickCaretWidth); |
1269 |
} |
1270 |
newType = CaretType.decode(newTypeStr); |
1271 |
} else { |
1272 |
newType = CaretType.THICK_LINE_CARET; |
1273 |
} |
1274 |
|
1275 |
String mimeType = DocumentUtilities.getMimeType(c); |
1276 |
FontColorSettings fcs = (mimeType != null) ? MimeLookup.getLookup(mimeType).lookup(FontColorSettings.class) : null; |
1277 |
if (fcs != null) { |
1278 |
AttributeSet attribs = fcs.getFontColors(overwriteMode |
1279 |
? FontColorNames.CARET_COLOR_OVERWRITE_MODE |
1280 |
: FontColorNames.CARET_COLOR_INSERT_MODE); //NOI18N |
1281 |
if (attribs != null) { |
1282 |
caretColor = (Color) attribs.getAttribute(StyleConstants.Foreground); |
1283 |
} |
1284 |
} |
1285 |
|
1286 |
this.type = newType; |
1287 |
final Color caretColorFinal = caretColor; |
1288 |
ViewUtils.runInEDT( |
1289 |
new Runnable() { |
1290 |
public @Override void run() { |
1291 |
if (caretColorFinal != null) { |
1292 |
c.setCaretColor(caretColorFinal); |
1293 |
if (LOG.isLoggable(Level.FINER)) { |
1294 |
LOG.finer("Updating caret color:" + caretColorFinal + '\n'); // NOI18N |
1295 |
} |
1296 |
} |
1297 |
|
1298 |
resetBlink(); |
1299 |
dispatchUpdate(false); |
1300 |
} |
1301 |
} |
1302 |
); |
1303 |
} |
1304 |
} |
1305 |
|
1306 |
/** |
1307 |
* Assign new caret bounds into <code>caretBounds</code> variable. |
1308 |
* |
1309 |
* @return true if the new caret bounds were successfully computed |
1310 |
* and assigned or false otherwise. |
1311 |
*/ |
1312 |
private boolean updateAllCaretsBounds() { |
1313 |
JTextComponent c = component; |
1314 |
AbstractDocument doc; |
1315 |
boolean ret = false; |
1316 |
if (c != null && (doc = activeDoc) != null) { |
1317 |
doc.readLock(); |
1318 |
try { |
1319 |
List<CaretInfo> sortedCarets = getSortedCarets(); |
1320 |
for (CaretInfo caret : sortedCarets) { |
1321 |
ret |= updateRealCaretBounds(caret.getCaretItem(), doc, c); |
1322 |
} |
1323 |
} finally { |
1324 |
doc.readUnlock(); |
1325 |
} |
1326 |
} |
1327 |
return ret; |
1328 |
} |
1329 |
|
1330 |
private boolean updateCaretBounds(CaretItem caret) { |
1331 |
JTextComponent c = component; |
1332 |
boolean ret = false; |
1333 |
AbstractDocument doc; |
1334 |
if (c != null && (doc = activeDoc) != null) { |
1335 |
doc.readLock(); |
1336 |
try { |
1337 |
ret = updateRealCaretBounds(caret, doc, c); |
1338 |
} finally { |
1339 |
doc.readUnlock(); |
1340 |
} |
1341 |
} |
1342 |
return ret; |
1343 |
} |
1344 |
|
1345 |
private boolean updateRealCaretBounds(CaretItem caret, Document doc, JTextComponent c) { |
1346 |
Position dotPos = caret.getDotPosition(); |
1347 |
int offset = dotPos == null? 0 : dotPos.getOffset(); |
1348 |
if (offset > doc.getLength()) { |
1349 |
offset = doc.getLength(); |
1350 |
} |
1351 |
Rectangle newCaretBounds; |
1352 |
try { |
1353 |
DocumentView docView = DocumentView.get(c); |
1354 |
if (docView != null) { |
1355 |
// docView.syncViewsRebuild(); // Make sure pending views changes are resolved |
1356 |
} |
1357 |
newCaretBounds = c.getUI().modelToView( |
1358 |
c, offset, Position.Bias.Forward); |
1359 |
// [TODO] Temporary fix - impl should remember real bounds computed by paintCustomCaret() |
1360 |
if (newCaretBounds != null) { |
1361 |
newCaretBounds.width = Math.max(newCaretBounds.width, 2); |
1362 |
} |
1363 |
|
1364 |
} catch (BadLocationException e) { |
1365 |
|
1366 |
newCaretBounds = null; |
1367 |
} |
1368 |
if (newCaretBounds != null) { |
1369 |
if (LOG.isLoggable(Level.FINE)) { |
1370 |
LOG.log(Level.FINE, "updateCaretBounds: old={0}, new={1}, offset={2}", |
1371 |
new Object[]{caret.getCaretBounds(), newCaretBounds, offset}); //NOI18N |
1372 |
} |
1373 |
caret.setCaretBounds(newCaretBounds); |
1374 |
return true; |
1375 |
} else { |
1376 |
return false; |
1377 |
} |
1378 |
} |
1379 |
|
1380 |
private void modelChanged(Document oldDoc, Document newDoc) { |
1381 |
if (oldDoc != null) { |
1382 |
// ideally the oldDoc param shouldn't exist and only listenDoc should be used |
1383 |
assert (oldDoc == activeDoc); |
1384 |
|
1385 |
DocumentUtilities.removeDocumentListener( |
1386 |
oldDoc, listenerImpl, DocumentListenerPriority.CARET_UPDATE); |
1387 |
AtomicLockDocument oldAtomicDoc = LineDocumentUtils.as(oldDoc, AtomicLockDocument.class); |
1388 |
if (oldAtomicDoc != null) { |
1389 |
oldAtomicDoc.removeAtomicLockListener(listenerImpl); |
1390 |
} |
1391 |
|
1392 |
activeDoc = null; |
1393 |
if (prefs != null && weakPrefsListener != null) { |
1394 |
prefs.removePreferenceChangeListener(weakPrefsListener); |
1395 |
} |
1396 |
} |
1397 |
|
1398 |
// EditorCaret only installs successfully into AbstractDocument based documents that carry a mime-type |
1399 |
if (newDoc instanceof AbstractDocument) { |
1400 |
String mimeType = DocumentUtilities.getMimeType(newDoc); |
1401 |
activeDoc = (AbstractDocument) newDoc; |
1402 |
DocumentUtilities.addDocumentListener( |
1403 |
newDoc, listenerImpl, DocumentListenerPriority.CARET_UPDATE); |
1404 |
AtomicLockDocument newAtomicDoc = LineDocumentUtils.as(oldDoc, AtomicLockDocument.class); |
1405 |
if (newAtomicDoc != null) { |
1406 |
newAtomicDoc.addAtomicLockListener(listenerImpl); |
1407 |
} |
1408 |
|
1409 |
// Set caret to zero position upon document change (DefaultCaret impl does this too) |
1410 |
runTransaction(CaretTransaction.RemoveType.REMOVE_ALL_CARETS, 0, |
1411 |
new CaretItem[] { new CaretItem(this, newDoc.getStartPosition(), null) }, null); |
1412 |
|
1413 |
// Leave caretPos and markPos null => offset==0 |
1414 |
prefs = (mimeType != null) ? MimeLookup.getLookup(mimeType).lookup(Preferences.class) : null; |
1415 |
if (prefs != null) { |
1416 |
weakPrefsListener = WeakListeners.create(PreferenceChangeListener.class, listenerImpl, prefs); |
1417 |
prefs.addPreferenceChangeListener(weakPrefsListener); |
1418 |
} |
1419 |
|
1420 |
updateType(); |
1421 |
} |
1422 |
} |
1423 |
|
1424 |
private void paintCaret(Graphics g, CaretItem caret) { |
1425 |
JTextComponent c = component; |
1426 |
if (c != null) { |
1427 |
g.setColor(c.getCaretColor()); |
1428 |
Rectangle caretBounds = caret.getCaretBounds(); |
1429 |
switch (type) { |
1430 |
case THICK_LINE_CARET: |
1431 |
g.fillRect(caretBounds.x, caretBounds.y, this.thickCaretWidth, caretBounds.height - 1); |
1432 |
break; |
1433 |
|
1434 |
case THIN_LINE_CARET: |
1435 |
int upperX = caret.getCaretBounds().x; |
1436 |
g.drawLine((int) upperX, caret.getCaretBounds().y, caret.getCaretBounds().x, |
1437 |
(caret.getCaretBounds().y + caret.getCaretBounds().height - 1)); |
1438 |
break; |
1439 |
|
1440 |
case BLOCK_CARET: |
1441 |
// Use a CaretOverwriteModeHighlighting layer to paint the caret |
1442 |
break; |
1443 |
|
1444 |
default: |
1445 |
throw new IllegalStateException("Invalid caret type=" + type); |
1446 |
} |
1447 |
} |
1448 |
} |
1449 |
|
1450 |
void dispatchUpdate() { |
1451 |
JTextComponent c = component; |
1452 |
if (c != null) { |
1453 |
dispatchUpdate(c.hasFocus()); // Scroll to caret only for component with focus |
1454 |
} |
1455 |
} |
1456 |
/** Update visual position of caret(s) */ |
1457 |
private void dispatchUpdate(final boolean scrollViewToCaret) { |
1458 |
/* Ensure that the caret's document listener will be added AFTER the views hierarchy's |
1459 |
* document listener so the code can run synchronously again |
1460 |
* which should eliminate the problem with caret lag. |
1461 |
* However the document can be modified from non-AWT thread |
1462 |
* which is the case in #57316 and in that case the code |
1463 |
* must run asynchronously in AWT thread. |
1464 |
*/ |
1465 |
ViewUtils.runInEDT( |
1466 |
new Runnable() { |
1467 |
public @Override void run() { |
1468 |
AbstractDocument doc = activeDoc; |
1469 |
if (doc != null) { |
1470 |
doc.readLock(); |
1471 |
try { |
1472 |
update(scrollViewToCaret); |
1473 |
} finally { |
1474 |
doc.readUnlock(); |
1475 |
} |
1476 |
} |
1477 |
} |
1478 |
} |
1479 |
); |
1480 |
} |
1481 |
|
1482 |
/** |
1483 |
* Update the caret's visual position. |
1484 |
* <br> |
1485 |
* The document is read-locked while calling this method. |
1486 |
* |
1487 |
* @param scrollViewToCaret whether the view of the text component should be |
1488 |
* scrolled to the position of the caret. |
1489 |
*/ |
1490 |
private void update(boolean scrollViewToCaret) { |
1491 |
caretUpdatePending = false; |
1492 |
JTextComponent c = component; |
1493 |
if (c != null) { |
1494 |
if (!c.isValid()) { |
1495 |
c.validate(); |
1496 |
} |
1497 |
Document doc = c.getDocument(); |
1498 |
if (doc != null) { |
1499 |
List<CaretInfo> sortedCarets = getSortedCarets(); |
1500 |
for (CaretInfo caret : sortedCarets) { |
1501 |
CaretItem caretItem = caret.getCaretItem(); |
1502 |
Rectangle oldCaretBounds = caretItem.getCaretBounds(); // no need to deep copy |
1503 |
if (oldCaretBounds != null) { |
1504 |
c.repaint(oldCaretBounds); |
1505 |
} |
1506 |
|
1507 |
// note - the order is important ! caret bounds must be updated even if the fold flag is true. |
1508 |
if (updateCaretBounds(caretItem) || updateAfterFoldHierarchyChange) { |
1509 |
Rectangle scrollBounds = new Rectangle(caretItem.getCaretBounds()); |
1510 |
|
1511 |
// Optimization to avoid extra repaint: |
1512 |
// If the caret bounds were not yet assigned then attempt |
1513 |
// to scroll the window so that there is an extra vertical space |
1514 |
// for the possible horizontal scrollbar that may appear |
1515 |
// if the line-view creation process finds line-view that |
1516 |
// is too wide and so the horizontal scrollbar will appear |
1517 |
// consuming an extra vertical space at the bottom. |
1518 |
if (oldCaretBounds == null) { |
1519 |
Component viewport = c.getParent(); |
1520 |
if (viewport instanceof JViewport) { |
1521 |
Component scrollPane = viewport.getParent(); |
1522 |
if (scrollPane instanceof JScrollPane) { |
1523 |
JScrollBar hScrollBar = ((JScrollPane) scrollPane).getHorizontalScrollBar(); |
1524 |
if (hScrollBar != null) { |
1525 |
int hScrollBarHeight = hScrollBar.getPreferredSize().height; |
1526 |
Dimension extentSize = ((JViewport) viewport).getExtentSize(); |
1527 |
// If the extent size is high enough then extend |
1528 |
// the scroll region by extra vertical space |
1529 |
if (extentSize.height >= caretItem.getCaretBounds().height + hScrollBarHeight) { |
1530 |
scrollBounds.height += hScrollBarHeight; |
1531 |
} |
1532 |
} |
1533 |
} |
1534 |
} |
1535 |
} |
1536 |
|
1537 |
Rectangle visibleBounds = c.getVisibleRect(); |
1538 |
|
1539 |
// If folds have changed attempt to scroll the view so that |
1540 |
// relative caret's visual position gets retained |
1541 |
// (the absolute position will change because of collapsed/expanded folds). |
1542 |
boolean doScroll = scrollViewToCaret; |
1543 |
boolean explicit = false; |
1544 |
if (oldCaretBounds != null && (!scrollViewToCaret || updateAfterFoldHierarchyChange)) { |
1545 |
int oldRelY = oldCaretBounds.y - visibleBounds.y; |
1546 |
// Only fix if the caret is within visible bounds and the new x or y coord differs from the old one |
1547 |
if (LOG.isLoggable(Level.FINER)) { |
1548 |
LOG.log(Level.FINER, "oldCaretBounds: {0}, visibleBounds: {1}, caretBounds: {2}", |
1549 |
new Object[]{oldCaretBounds, visibleBounds, caretItem.getCaretBounds()}); |
1550 |
} |
1551 |
if (oldRelY >= 0 && oldRelY < visibleBounds.height |
1552 |
&& (oldCaretBounds.y != caretItem.getCaretBounds().y || oldCaretBounds.x != caretItem.getCaretBounds().x)) { |
1553 |
doScroll = true; // Perform explicit scrolling |
1554 |
explicit = true; |
1555 |
int oldRelX = oldCaretBounds.x - visibleBounds.x; |
1556 |
// Do not retain the horizontal caret bounds by scrolling |
1557 |
// since many modifications do not explicitly say that they are typing modifications |
1558 |
// and this would cause problems like #176268 |
1559 |
// scrollBounds.x = Math.max(caretBounds.x - oldRelX, 0); |
1560 |
scrollBounds.y = Math.max(caretItem.getCaretBounds().y - oldRelY, 0); |
1561 |
// scrollBounds.width = visibleBounds.width; |
1562 |
scrollBounds.height = visibleBounds.height; |
1563 |
} |
1564 |
} |
1565 |
|
1566 |
// Historically the caret is expected to appear |
1567 |
// in the middle of the window if setDot() gets called |
1568 |
// e.g. by double-clicking in Navigator. |
1569 |
// If the caret bounds are more than a caret height below the present |
1570 |
// visible view bounds (or above the view bounds) |
1571 |
// then scroll the window so that the caret is in the middle |
1572 |
// of the visible window to see the context around the caret. |
1573 |
// This should work fine with PgUp/Down because these |
1574 |
// scroll the view explicitly. |
1575 |
if (scrollViewToCaret |
1576 |
&& !explicit |
1577 |
&& // #219580: if the preceding if-block computed new scrollBounds, it cannot be offset yet more |
1578 |
/* # 70915 !updateAfterFoldHierarchyChange && */ (caretItem.getCaretBounds().y > visibleBounds.y + visibleBounds.height + caretItem.getCaretBounds().height |
1579 |
|| caretItem.getCaretBounds().y + caretItem.getCaretBounds().height < visibleBounds.y - caretItem.getCaretBounds().height)) { |
1580 |
// Scroll into the middle |
1581 |
scrollBounds.y -= (visibleBounds.height - caretItem.getCaretBounds().height) / 2; |
1582 |
scrollBounds.height = visibleBounds.height; |
1583 |
} |
1584 |
if (LOG.isLoggable(Level.FINER)) { |
1585 |
LOG.finer("Resetting fold flag, current: " + updateAfterFoldHierarchyChange); |
1586 |
} |
1587 |
updateAfterFoldHierarchyChange = false; |
1588 |
|
1589 |
// Ensure that the viewport will be scrolled either to make the caret visible |
1590 |
// or to retain cart's relative visual position against the begining of the viewport's visible rectangle. |
1591 |
if (doScroll) { |
1592 |
if (LOG.isLoggable(Level.FINER)) { |
1593 |
LOG.finer("Scrolling to: " + scrollBounds); |
1594 |
} |
1595 |
c.scrollRectToVisible(scrollBounds); |
1596 |
if (!c.getVisibleRect().intersects(scrollBounds)) { |
1597 |
// HACK: see #219580: for some reason, the scrollRectToVisible may fail. |
1598 |
c.scrollRectToVisible(scrollBounds); |
1599 |
} |
1600 |
} |
1601 |
resetBlink(); |
1602 |
c.repaint(caretItem.getCaretBounds()); |
1603 |
} |
1604 |
} |
1605 |
} |
1606 |
} |
1607 |
} |
1608 |
|
1609 |
private void updateSystemSelection() { |
1610 |
if(component == null) return; |
1611 |
Clipboard clip = null; |
1612 |
try { |
1613 |
clip = component.getToolkit().getSystemSelection(); |
1614 |
} catch (SecurityException ex) { |
1615 |
// XXX: ignore for now, there is no ExClipboard for SystemSelection Clipboard |
1616 |
} |
1617 |
if(clip != null) { |
1618 |
StringBuilder builder = new StringBuilder(); |
1619 |
boolean first = true; |
1620 |
List<CaretInfo> sortedCarets = getSortedCarets(); |
1621 |
for (CaretInfo caret : sortedCarets) { |
1622 |
CaretItem caretItem = caret.getCaretItem(); |
1623 |
if(caretItem.isSelection()) { |
1624 |
if(!first) { |
1625 |
builder.append("\n"); |
1626 |
} else { |
1627 |
first = false; |
1628 |
} |
1629 |
builder.append(getSelectedText(caretItem)); |
1630 |
} |
1631 |
} |
1632 |
if(builder.length() > 0) { |
1633 |
clip.setContents(new java.awt.datatransfer.StringSelection(builder.toString()), null); |
1634 |
} |
1635 |
} |
1636 |
} |
1637 |
|
1638 |
/** |
1639 |
* Returns the selected text contained for this |
1640 |
* <code>Caret</code>. If the selection is |
1641 |
* <code>null</code> or the document empty, returns <code>null</code>. |
1642 |
* |
1643 |
* @param caret |
1644 |
* @return the text |
1645 |
* @exception IllegalArgumentException if the selection doesn't |
1646 |
* have a valid mapping into the document for some reason |
1647 |
*/ |
1648 |
private String getSelectedText(CaretItem caret) { |
1649 |
String txt = null; |
1650 |
int p0 = Math.min(caret.getDot(), caret.getMark()); |
1651 |
int p1 = Math.max(caret.getDot(), caret.getMark()); |
1652 |
if (p0 != p1) { |
1653 |
try { |
1654 |
Document doc = component.getDocument(); |
1655 |
txt = doc.getText(p0, p1 - p0); |
1656 |
} catch (BadLocationException e) { |
1657 |
throw new IllegalArgumentException(e.getMessage()); |
1658 |
} |
1659 |
} |
1660 |
return txt; |
1661 |
} |
1662 |
|
1663 |
private void updateRectangularSelectionPositionBlocks() { |
1664 |
JTextComponent c = component; |
1665 |
if (rectangularSelection) { |
1666 |
AbstractDocument doc = activeDoc; |
1667 |
if (doc != null) { |
1668 |
doc.readLock(); |
1669 |
try { |
1670 |
if (rsRegions == null) { |
1671 |
rsRegions = new ArrayList<Position>(); |
1672 |
component.putClientProperty(RECTANGULAR_SELECTION_REGIONS_PROPERTY, rsRegions); |
1673 |
} |
1674 |
synchronized (rsRegions) { |
1675 |
if (LOG.isLoggable(Level.FINE)) { |
1676 |
LOG.fine("Rectangular-selection position regions:\n"); |
1677 |
} |
1678 |
rsRegions.clear(); |
1679 |
if (rsPaintRect != null) { |
1680 |
LockedViewHierarchy lvh = ViewHierarchy.get(c).lock(); |
1681 |
try { |
1682 |
float rowHeight = lvh.getDefaultRowHeight(); |
1683 |
double y = rsPaintRect.y; |
1684 |
double maxY = y + rsPaintRect.height; |
1685 |
double minX = rsPaintRect.getMinX(); |
1686 |
double maxX = rsPaintRect.getMaxX(); |
1687 |
do { |
1688 |
int startOffset = lvh.viewToModel(minX, y, null); |
1689 |
int endOffset = lvh.viewToModel(maxX, y, null); |
1690 |
// They could be swapped due to RTL text |
1691 |
if (startOffset > endOffset) { |
1692 |
int tmp = startOffset; |
1693 |
startOffset = endOffset; |
1694 |
endOffset = tmp; |
1695 |
} |
1696 |
Position startPos = activeDoc.createPosition(startOffset); |
1697 |
Position endPos = activeDoc.createPosition(endOffset); |
1698 |
rsRegions.add(startPos); |
1699 |
rsRegions.add(endPos); |
1700 |
if (LOG.isLoggable(Level.FINE)) { |
1701 |
LOG.fine(" <" + startOffset + "," + endOffset + ">\n"); |
1702 |
} |
1703 |
y += rowHeight; |
1704 |
} while (y < maxY); |
1705 |
c.putClientProperty(RECTANGULAR_SELECTION_REGIONS_PROPERTY, rsRegions); |
1706 |
} finally { |
1707 |
lvh.unlock(); |
1708 |
} |
1709 |
} |
1710 |
} |
1711 |
} catch (BadLocationException ex) { |
1712 |
Exceptions.printStackTrace(ex); |
1713 |
} finally { |
1714 |
doc.readUnlock(); |
1715 |
} |
1716 |
} |
1717 |
} |
1718 |
} |
1719 |
|
1720 |
private String dumpVisibility() { |
1721 |
return "visible=" + isVisible() + ", blinkVisible=" + blinkVisible; |
1722 |
} |
1723 |
|
1724 |
/*private*/ void resetBlink() { |
1725 |
boolean visible = isVisible(); |
1726 |
synchronized (listenerList) { |
1727 |
if (flasher != null) { |
1728 |
flasher.stop(); |
1729 |
setBlinkVisible(true); |
1730 |
if (visible) { |
1731 |
if (LOG.isLoggable(Level.FINER)){ |
1732 |
LOG.finer("Reset blinking (caret already visible)" + // NOI18N |
1733 |
" - starting the caret blinking timer: " + dumpVisibility() + '\n'); // NOI18N |
1734 |
} |
1735 |
flasher.start(); |
1736 |
} else { |
1737 |
if (LOG.isLoggable(Level.FINER)){ |
1738 |
LOG.finer("Reset blinking (caret not visible)" + // NOI18N |
1739 |
" - caret blinking timer not started: " + dumpVisibility() + '\n'); // NOI18N |
1740 |
} |
1741 |
} |
1742 |
} |
1743 |
} |
1744 |
} |
1745 |
|
1746 |
/*private*/ void setBlinkVisible(boolean blinkVisible) { |
1747 |
synchronized (listenerList) { |
1748 |
this.blinkVisible = blinkVisible; |
1749 |
} |
1750 |
updateOverwriteModeLayer(false); |
1751 |
} |
1752 |
|
1753 |
private void updateOverwriteModeLayer(boolean forceUpdate) { |
1754 |
JTextComponent c; |
1755 |
if ((forceUpdate || overwriteMode) && (c = component) != null) { |
1756 |
CaretOverwriteModeHighlighting overwriteModeHighlighting = (CaretOverwriteModeHighlighting) |
1757 |
c.getClientProperty(CaretOverwriteModeHighlighting.class); |
1758 |
if (overwriteModeHighlighting != null) { |
1759 |
overwriteModeHighlighting.setVisible(visible && blinkVisible); |
1760 |
} |
1761 |
} |
1762 |
} |
1763 |
|
1764 |
private void adjustRectangularSelectionMouseX(int x, int y) { |
1765 |
if (!rectangularSelection) { |
1766 |
return; |
1767 |
} |
1768 |
JTextComponent c = component; |
1769 |
int offset = c.viewToModel(new Point(x, y)); |
1770 |
Rectangle r = null;; |
1771 |
if (offset >= 0) { |
1772 |
try { |
1773 |
r = c.modelToView(offset); |
1774 |
} catch (BadLocationException ex) { |
1775 |
r = null; |
1776 |
} |
1777 |
} |
1778 |
if (r != null) { |
1779 |
float xDiff = x - r.x; |
1780 |
if (xDiff > 0) { |
1781 |
float charWidth; |
1782 |
LockedViewHierarchy lvh = ViewHierarchy.get(c).lock(); |
1783 |
try { |
1784 |
charWidth = lvh.getDefaultCharWidth(); |
1785 |
} finally { |
1786 |
lvh.unlock(); |
1787 |
} |
1788 |
int n = (int) (xDiff / charWidth); |
1789 |
r.x += n * charWidth; |
1790 |
r.width = (int) charWidth; |
1791 |
} |
1792 |
rsDotRect.x = r.x; |
1793 |
rsDotRect.width = r.width; |
1794 |
updateRectangularSelectionPaintRect(); |
1795 |
fireStateChanged(); |
1796 |
} |
1797 |
} |
1798 |
|
1799 |
private void updateRectangularSelectionPaintRect() { |
1800 |
// Repaint current rect |
1801 |
JTextComponent c = component; |
1802 |
Rectangle repaintRect = rsPaintRect; |
1803 |
if (rsDotRect == null || rsMarkRect == null) { |
1804 |
return; |
1805 |
} |
1806 |
Rectangle newRect = new Rectangle(); |
1807 |
if (rsDotRect.x < rsMarkRect.x) { // Swap selection to left |
1808 |
newRect.x = rsDotRect.x; // -1 to make the visual selection non-empty |
1809 |
newRect.width = rsMarkRect.x - newRect.x; |
1810 |
} else { // Extend or shrink on right |
1811 |
newRect.x = rsMarkRect.x; |
1812 |
newRect.width = rsDotRect.x - newRect.x; |
1813 |
} |
1814 |
if (rsDotRect.y < rsMarkRect.y) { |
1815 |
newRect.y = rsDotRect.y; |
1816 |
newRect.height = (rsMarkRect.y + rsMarkRect.height) - newRect.y; |
1817 |
} else { |
1818 |
newRect.y = rsMarkRect.y; |
1819 |
newRect.height = (rsDotRect.y + rsDotRect.height) - newRect.y; |
1820 |
} |
1821 |
if (newRect.width < 2) { |
1822 |
newRect.width = 2; |
1823 |
} |
1824 |
rsPaintRect = newRect; |
1825 |
|
1826 |
// Repaint merged region with original rect |
1827 |
if (repaintRect == null) { |
1828 |
repaintRect = rsPaintRect; |
1829 |
} else { |
1830 |
repaintRect = repaintRect.union(rsPaintRect); |
1831 |
} |
1832 |
c.repaint(repaintRect); |
1833 |
|
1834 |
updateRectangularSelectionPositionBlocks(); |
1835 |
} |
1836 |
|
1837 |
private void selectEnsureMinSelection(int mark, int dot, int newDot) { |
1838 |
if (LOG.isLoggable(Level.FINE)) { |
1839 |
LOG.fine("selectEnsureMinSelection: mark=" + mark + ", dot=" + dot + ", newDot=" + newDot); // NOI18N |
1840 |
} |
1841 |
if (dot >= mark) { // Existing forward selection |
1842 |
if (newDot >= mark) { |
1843 |
moveDot(Math.max(newDot, minSelectionEndOffset)); |
1844 |
} else { // newDot < mark => swap mark and dot |
1845 |
setDot(minSelectionEndOffset); |
1846 |
moveDot(Math.min(newDot, minSelectionStartOffset)); |
1847 |
} |
1848 |
|
1849 |
} else { // Existing backward selection |
1850 |
if (newDot <= mark) { |
1851 |
moveDot(Math.min(newDot, minSelectionStartOffset)); |
1852 |
} else { // newDot > mark => swap mark and dot |
1853 |
setDot(minSelectionStartOffset); |
1854 |
moveDot(Math.max(newDot, minSelectionEndOffset)); |
1855 |
} |
1856 |
} |
1857 |
} |
1858 |
|
1859 |
private boolean isLeftMouseButtonExt(MouseEvent evt) { |
1860 |
return (SwingUtilities.isLeftMouseButton(evt) |
1861 |
&& !(evt.isPopupTrigger()) |
1862 |
&& (evt.getModifiers() & (InputEvent.META_MASK/* | InputEvent.ALT_MASK*/)) == 0); |
1863 |
} |
1864 |
|
1865 |
private boolean isMiddleMouseButtonExt(MouseEvent evt) { |
1866 |
return (evt.getButton() == MouseEvent.BUTTON2) && |
1867 |
(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; |
1868 |
} |
1869 |
|
1870 |
private int mapDragOperationFromModifiers(MouseEvent e) { |
1871 |
int mods = e.getModifiersEx(); |
1872 |
|
1873 |
if ((mods & InputEvent.BUTTON1_DOWN_MASK) == 0) { |
1874 |
return TransferHandler.NONE; |
1875 |
} |
1876 |
|
1877 |
return TransferHandler.COPY_OR_MOVE; |
1878 |
} |
1879 |
|
1880 |
/** |
1881 |
* Determines if the following are true: |
1882 |
* <ul> |
1883 |
* <li>the press event is located over a selection |
1884 |
* <li>the dragEnabled property is true |
1885 |
* <li>A TranferHandler is installed |
1886 |
* </ul> |
1887 |
* <p> |
1888 |
* This is implemented to check for a TransferHandler. |
1889 |
* Subclasses should perform the remaining conditions. |
1890 |
*/ |
1891 |
private boolean isDragPossible(MouseEvent e) { |
1892 |
Object src = e.getSource(); |
1893 |
if (src instanceof JComponent) { |
1894 |
JComponent comp = (JComponent) src; |
1895 |
boolean possible = (comp == null) ? false : (comp.getTransferHandler() != null); |
1896 |
if (possible && comp instanceof JTextComponent) { |
1897 |
JTextComponent c = (JTextComponent) comp; |
1898 |
if (c.getDragEnabled()) { |
1899 |
Caret caret = c.getCaret(); |
1900 |
int dot = caret.getDot(); |
1901 |
int mark = caret.getMark(); |
1902 |
if (dot != mark) { |
1903 |
Point p = new Point(e.getX(), e.getY()); |
1904 |
int pos = c.viewToModel(p); |
1905 |
|
1906 |
int p0 = Math.min(dot, mark); |
1907 |
int p1 = Math.max(dot, mark); |
1908 |
if ((pos >= p0) && (pos < p1)) { |
1909 |
return true; |
1910 |
} |
1911 |
} |
1912 |
} |
1913 |
} |
1914 |
} |
1915 |
return false; |
1916 |
} |
1917 |
|
1918 |
private void refresh() { |
1919 |
updateType(); |
1920 |
SwingUtilities.invokeLater(new Runnable() { |
1921 |
public @Override void run() { |
1922 |
updateAllCaretsBounds(); // the line height etc. may have change |
1923 |
} |
1924 |
}); |
1925 |
} |
1926 |
|
1927 |
private static String logMouseEvent(MouseEvent evt) { |
1928 |
return "x=" + evt.getX() + ", y=" + evt.getY() + ", clicks=" + evt.getClickCount() //NOI18N |
1929 |
+ ", component=" + s2s(evt.getComponent()) //NOI18N |
1930 |
+ ", source=" + s2s(evt.getSource()) + ", button=" + evt.getButton() + ", mods=" + evt.getModifiers() + ", modsEx=" + evt.getModifiersEx(); //NOI18N |
1931 |
} |
1932 |
|
1933 |
private static String s2s(Object o) { |
1934 |
return o == null ? "null" : o.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(o)); //NOI18N |
1935 |
} |
1936 |
|
1937 |
private final class ListenerImpl extends ComponentAdapter |
1938 |
implements DocumentListener, AtomicLockListener, MouseListener, MouseMotionListener, FocusListener, ViewHierarchyListener, |
1939 |
PropertyChangeListener, ActionListener, PreferenceChangeListener, KeyListener |
1940 |
{ |
1941 |
|
1942 |
ListenerImpl() { |
1943 |
} |
1944 |
|
1945 |
public @Override void preferenceChange(PreferenceChangeEvent evt) { |
1946 |
String setingName = evt == null ? null : evt.getKey(); |
1947 |
if (setingName == null || SimpleValueNames.CARET_BLINK_RATE.equals(setingName)) { |
1948 |
int rate = prefs.getInt(SimpleValueNames.CARET_BLINK_RATE, -1); |
1949 |
if (rate == -1) { |
1950 |
rate = EditorPreferencesDefaults.defaultCaretBlinkRate; |
1951 |
} |
1952 |
setBlinkRate(rate); |
1953 |
refresh(); |
1954 |
} |
1955 |
} |
1956 |
|
1957 |
public @Override void propertyChange(PropertyChangeEvent evt) { |
1958 |
String propName = evt.getPropertyName(); |
1959 |
JTextComponent c = component; |
1960 |
if ("document".equals(propName)) { // NOI18N |
1961 |
if (c != null) { |
1962 |
modelChanged(activeDoc, c.getDocument()); |
1963 |
} |
1964 |
|
1965 |
} else if (EditorUtilities.CARET_OVERWRITE_MODE_PROPERTY.equals(propName)) { |
1966 |
Boolean b = (Boolean) evt.getNewValue(); |
1967 |
overwriteMode = (b != null) ? b : false; |
1968 |
updateOverwriteModeLayer(true); |
1969 |
updateType(); |
1970 |
|
1971 |
} else if ("ancestor".equals(propName) && evt.getSource() == component) { // NOI18N |
1972 |
// The following code ensures that when the width of the line views |
1973 |
// gets computed on background after the file gets opened |
1974 |
// (so the horizontal scrollbar gets added after several seconds |
1975 |
// for larger files) that the suddenly added horizontal scrollbar |
1976 |
// will not hide the caret laying on the last line of the viewport. |
1977 |
// A component listener gets installed into horizontal scrollbar |
1978 |
// and if it's fired the caret's bounds will be checked whether |
1979 |
// they intersect with the horizontal scrollbar |
1980 |
// and if so the view will be scrolled. |
1981 |
Container parent = component.getParent(); |
1982 |
if (parent instanceof JViewport) { |
1983 |
parent = parent.getParent(); // parent of viewport |
1984 |
if (parent instanceof JScrollPane) { |
1985 |
JScrollPane scrollPane = (JScrollPane) parent; |
1986 |
JScrollBar hScrollBar = scrollPane.getHorizontalScrollBar(); |
1987 |
if (hScrollBar != null) { |
1988 |
// Add weak listener so that editor pane could be removed |
1989 |
// from scrollpane without being held by scrollbar |
1990 |
hScrollBar.addComponentListener( |
1991 |
(ComponentListener) WeakListeners.create( |
1992 |
ComponentListener.class, listenerImpl, hScrollBar)); |
1993 |
} |
1994 |
} |
1995 |
} |
1996 |
} else if ("enabled".equals(propName)) { |
1997 |
Boolean enabled = (Boolean) evt.getNewValue(); |
1998 |
if (component.isFocusOwner()) { |
1999 |
if (enabled == Boolean.TRUE) { |
2000 |
if (component.isEditable()) { |
2001 |
setVisible(true); |
2002 |
} |
2003 |
setSelectionVisible(true); |
2004 |
} else { |
2005 |
setVisible(false); |
2006 |
setSelectionVisible(false); |
2007 |
} |
2008 |
} |
2009 |
} else if (RECTANGULAR_SELECTION_PROPERTY.equals(propName)) { |
2010 |
boolean origRectangularSelection = rectangularSelection; |
2011 |
rectangularSelection = Boolean.TRUE.equals(component.getClientProperty(RECTANGULAR_SELECTION_PROPERTY)); |
2012 |
if (rectangularSelection != origRectangularSelection) { |
2013 |
if (rectangularSelection) { |
2014 |
setRectangularSelectionToDotAndMark(); |
2015 |
RectangularSelectionTransferHandler.install(component); |
2016 |
|
2017 |
} else { // No rectangular selection |
2018 |
RectangularSelectionTransferHandler.uninstall(component); |
2019 |
} |
2020 |
fireStateChanged(); |
2021 |
} |
2022 |
} |
2023 |
} |
2024 |
|
2025 |
// ActionListener methods |
2026 |
/** |
2027 |
* Fired when blink timer fires |
2028 |
*/ |
2029 |
public @Override void actionPerformed(ActionEvent evt) { |
2030 |
JTextComponent c = component; |
2031 |
if (c != null) { |
2032 |
setBlinkVisible(!blinkVisible); |
2033 |
List<CaretInfo> sortedCarets = getSortedCarets(); // TODO only repaint carets showing on screen |
2034 |
for (CaretInfo caret : sortedCarets) { |
2035 |
CaretItem caretItem = caret.getCaretItem(); |
2036 |
if (caretItem.getCaretBounds() != null) { |
2037 |
Rectangle repaintRect = caretItem.getCaretBounds(); |
2038 |
c.repaint(repaintRect); |
2039 |
} |
2040 |
} |
2041 |
} |
2042 |
} |
2043 |
|
2044 |
// DocumentListener methods |
2045 |
public @Override void insertUpdate(DocumentEvent evt) { |
2046 |
JTextComponent c = component; |
2047 |
if (c != null) { |
2048 |
Document doc = evt.getDocument(); |
2049 |
int offset = evt.getOffset(); |
2050 |
final int endOffset = offset + evt.getLength(); |
2051 |
if (offset == 0) { |
2052 |
// Manually shift carets at offset zero |
2053 |
runTransaction(CaretTransaction.RemoveType.DOCUMENT_INSERT_ZERO_OFFSET, endOffset, null, null); |
2054 |
} |
2055 |
// [TODO] proper undo solution |
2056 |
modified = true; |
2057 |
modifiedUpdate(true); |
2058 |
|
2059 |
} |
2060 |
} |
2061 |
|
2062 |
public @Override void removeUpdate(DocumentEvent evt) { |
2063 |
JTextComponent c = component; |
2064 |
if (c != null) { |
2065 |
// [TODO] proper undo solution |
2066 |
modified = true; |
2067 |
int offset = evt.getOffset(); |
2068 |
runTransaction(CaretTransaction.RemoveType.DOCUMENT_REMOVE, offset, null, null); |
2069 |
modifiedUpdate(true); |
2070 |
} |
2071 |
} |
2072 |
|
2073 |
public @Override void changedUpdate(DocumentEvent evt) { |
2074 |
} |
2075 |
|
2076 |
public @Override |
2077 |
void atomicLock(AtomicLockEvent evt) { |
2078 |
inAtomicLock = true; |
2079 |
} |
2080 |
|
2081 |
public @Override |
2082 |
void atomicUnlock(AtomicLockEvent evt) { |
2083 |
inAtomicLock = false; |
2084 |
inAtomicUnlock = true; |
2085 |
try { |
2086 |
modifiedUpdate(typingModificationOccurred); |
2087 |
} finally { |
2088 |
inAtomicUnlock = false; |
2089 |
typingModificationOccurred = false; |
2090 |
} |
2091 |
} |
2092 |
|
2093 |
private void modifiedUpdate(boolean typingModification) { |
2094 |
if (!inAtomicLock) { |
2095 |
JTextComponent c = component; |
2096 |
if (modified && c != null) { |
2097 |
fireStateChanged(); |
2098 |
modified = false; |
2099 |
} |
2100 |
} else { |
2101 |
typingModificationOccurred |= typingModification; |
2102 |
} |
2103 |
} |
2104 |
|
2105 |
// MouseListener methods |
2106 |
@Override |
2107 |
public void mousePressed(MouseEvent evt) { |
2108 |
if (LOG.isLoggable(Level.FINE)) { |
2109 |
LOG.fine("mousePressed: " + logMouseEvent(evt) + ", state=" + mouseState + '\n'); // NOI18N |
2110 |
} |
2111 |
|
2112 |
JTextComponent c = component; |
2113 |
AbstractDocument doc = activeDoc; |
2114 |
if (c != null && doc != null && isLeftMouseButtonExt(evt)) { |
2115 |
doc.readLock(); |
2116 |
try { |
2117 |
// Expand fold if offset is in collapsed fold |
2118 |
int offset = mouse2Offset(evt); |
2119 |
switch (evt.getClickCount()) { |
2120 |
case 1: // Single press |
2121 |
if (c.isEnabled() && !c.hasFocus()) { |
2122 |
c.requestFocus(); |
2123 |
} |
2124 |
c.setDragEnabled(true); |
2125 |
if (evt.isAltDown() && evt.isShiftDown()) { |
2126 |
mouseState = MouseState.CHAR_SELECTION; |
2127 |
try { |
2128 |
Position pos = doc.createPosition(offset); |
2129 |
runTransaction(CaretTransaction.RemoveType.NO_REMOVE, 0, |
2130 |
new CaretItem[] { new CaretItem(EditorCaret.this, pos, pos) }, null); |
2131 |
} catch (BadLocationException ex) { |
2132 |
// Do nothing |
2133 |
} |
2134 |
} else if (evt.isShiftDown()) { // Select till offset |
2135 |
moveDot(offset); |
2136 |
adjustRectangularSelectionMouseX(evt.getX(), evt.getY()); // also fires state change |
2137 |
mouseState = MouseState.CHAR_SELECTION; |
2138 |
} else // Regular press |
2139 |
// check whether selection drag is possible |
2140 |
if (isDragPossible(evt) && mapDragOperationFromModifiers(evt) != TransferHandler.NONE) { |
2141 |
mouseState = MouseState.DRAG_SELECTION_POSSIBLE; |
2142 |
} else { // Drag not possible |
2143 |
mouseState = MouseState.CHAR_SELECTION; |
2144 |
setDot(offset); |
2145 |
} |
2146 |
break; |
2147 |
|
2148 |
case 2: // double-click => word selection |
2149 |
mouseState = MouseState.WORD_SELECTION; |
2150 |
// Disable drag which would otherwise occur when mouse would be over text |
2151 |
c.setDragEnabled(false); |
2152 |
// Check possible fold expansion |
2153 |
try { |
2154 |
// hack, to get knowledge of possible expansion. Editor depends on Folding, so it's not really possible |
2155 |
// to have Folding depend on BaseCaret (= a cycle). If BaseCaret moves to editor.lib2, this contract |
2156 |
// can be formalized as an interface. |
2157 |
@SuppressWarnings("unchecked") |
2158 |
Callable<Boolean> cc = (Callable<Boolean>) c.getClientProperty("org.netbeans.api.fold.expander"); |
2159 |
if (cc == null || !cc.equals(this)) { |
2160 |
if (selectWordAction == null) { |
2161 |
selectWordAction = EditorActionUtilities.getAction( |
2162 |
c.getUI().getEditorKit(c), DefaultEditorKit.selectWordAction); |
2163 |
} |
2164 |
if (selectWordAction != null) { |
2165 |
selectWordAction.actionPerformed(null); |
2166 |
} |
2167 |
// Select word action selects forward i.e. dot > mark |
2168 |
minSelectionStartOffset = getMark(); |
2169 |
minSelectionEndOffset = getDot(); |
2170 |
} |
2171 |
} catch (Exception ex) { |
2172 |
Exceptions.printStackTrace(ex); |
2173 |
} |
2174 |
break; |
2175 |
|
2176 |
case 3: // triple-click => line selection |
2177 |
mouseState = MouseState.LINE_SELECTION; |
2178 |
// Disable drag which would otherwise occur when mouse would be over text |
2179 |
c.setDragEnabled(false); |
2180 |
if (selectLineAction == null) { |
2181 |
selectLineAction = EditorActionUtilities.getAction( |
2182 |
c.getUI().getEditorKit(c), DefaultEditorKit.selectLineAction); |
2183 |
} |
2184 |
if (selectLineAction != null) { |
2185 |
selectLineAction.actionPerformed(null); |
2186 |
// Select word action selects forward i.e. dot > mark |
2187 |
minSelectionStartOffset = getMark(); |
2188 |
minSelectionEndOffset = getDot(); |
2189 |
} |
2190 |
break; |
2191 |
|
2192 |
default: // multi-click |
2193 |
} |
2194 |
} finally { |
2195 |
doc.readUnlock(); |
2196 |
} |
2197 |
} |
2198 |
} |
2199 |
|
2200 |
@Override |
2201 |
public void mouseReleased(MouseEvent evt) { |
2202 |
if (LOG.isLoggable(Level.FINE)) { |
2203 |
LOG.fine("mouseReleased: " + logMouseEvent(evt) + ", state=" + mouseState + '\n'); // NOI18N |
2204 |
} |
2205 |
|
2206 |
int offset = mouse2Offset(evt); |
2207 |
switch (mouseState) { |
2208 |
case DRAG_SELECTION_POSSIBLE: |
2209 |
setDot(offset); |
2210 |
adjustRectangularSelectionMouseX(evt.getX(), evt.getY()); // also fires state change |
2211 |
break; |
2212 |
|
2213 |
case CHAR_SELECTION: |
2214 |
if (evt.isAltDown() && evt.isShiftDown()) { |
2215 |
moveDotCaret(offset, getLastCaretItem()); |
2216 |
} else { |
2217 |
moveDot(offset); // Will do setDot() if no selection |
2218 |
adjustRectangularSelectionMouseX(evt.getX(), evt.getY()); // also fires state change |
2219 |
} |
2220 |
break; |
2221 |
} |
2222 |
// Set DEFAULT state; after next mouse press the state may change |
2223 |
// to another state according to particular click count |
2224 |
mouseState = MouseState.DEFAULT; |
2225 |
component.setDragEnabled(true); |
2226 |
} |
2227 |
|
2228 |
/** |
2229 |
* Translates mouse event to text offset |
2230 |
*/ |
2231 |
int mouse2Offset(MouseEvent evt) { |
2232 |
JTextComponent c = component; |
2233 |
int offset = 0; |
2234 |
if (c != null) { |
2235 |
int y = evt.getY(); |
2236 |
if (y < 0) { |
2237 |
offset = 0; |
2238 |
} else if (y > c.getSize().getHeight()) { |
2239 |
offset = c.getDocument().getLength(); |
2240 |
} else { |
2241 |
offset = c.viewToModel(new Point(evt.getX(), evt.getY())); |
2242 |
} |
2243 |
} |
2244 |
return offset; |
2245 |
} |
2246 |
|
2247 |
@Override |
2248 |
public void mouseClicked(MouseEvent evt) { |
2249 |
if (LOG.isLoggable(Level.FINE)) { |
2250 |
LOG.fine("mouseClicked: " + logMouseEvent(evt) + ", state=" + mouseState + '\n'); // NOI18N |
2251 |
} |
2252 |
|
2253 |
JTextComponent c = component; |
2254 |
if (c != null) { |
2255 |
if (isMiddleMouseButtonExt(evt)) { |
2256 |
if (evt.getClickCount() == 1) { |
2257 |
if (c == null) { |
2258 |
return; |
2259 |
} |
2260 |
Clipboard buffer = component.getToolkit().getSystemSelection(); |
2261 |
|
2262 |
if (buffer == null) { |
2263 |
return; |
2264 |
} |
2265 |
|
2266 |
Transferable trans = buffer.getContents(null); |
2267 |
if (trans == null) { |
2268 |
return; |
2269 |
} |
2270 |
|
2271 |
final Document doc = c.getDocument(); |
2272 |
if (doc == null) { |
2273 |
return; |
2274 |
} |
2275 |
|
2276 |
final int offset = c.getUI().viewToModel(c, new Point(evt.getX(), evt.getY())); |
2277 |
|
2278 |
try { |
2279 |
final String pastingString = (String) trans.getTransferData(DataFlavor.stringFlavor); |
2280 |
if (pastingString == null) { |
2281 |
return; |
2282 |
} |
2283 |
Runnable pasteRunnable = new Runnable() { |
2284 |
public @Override |
2285 |
void run() { |
2286 |
try { |
2287 |
doc.insertString(offset, pastingString, null); |
2288 |
setDot(offset + pastingString.length()); |
2289 |
} catch (BadLocationException exc) { |
2290 |
} |
2291 |
} |
2292 |
}; |
2293 |
AtomicLockDocument ald = LineDocumentUtils.as(doc, AtomicLockDocument.class); |
2294 |
if (ald != null) { |
2295 |
ald.runAtomic(pasteRunnable); |
2296 |
} else { |
2297 |
pasteRunnable.run(); |
2298 |
} |
2299 |
} catch (UnsupportedFlavorException ufe) { |
2300 |
} catch (IOException ioe) { |
2301 |
} |
2302 |
} |
2303 |
} |
2304 |
} |
2305 |
} |
2306 |
|
2307 |
@Override |
2308 |
public void mouseEntered(MouseEvent evt) { |
2309 |
} |
2310 |
|
2311 |
@Override |
2312 |
public void mouseExited(MouseEvent evt) { |
2313 |
} |
2314 |
|
2315 |
// MouseMotionListener methods |
2316 |
@Override |
2317 |
public void mouseMoved(MouseEvent evt) { |
2318 |
if (mouseState == MouseState.DEFAULT) { |
2319 |
boolean textCursor = true; |
2320 |
int position = component.viewToModel(evt.getPoint()); |
2321 |
if (RectangularSelectionUtils.isRectangularSelection(component)) { |
2322 |
List<Position> positions = RectangularSelectionUtils.regionsCopy(component); |
2323 |
for (int i = 0; textCursor && i < positions.size(); i += 2) { |
2324 |
int a = positions.get(i).getOffset(); |
2325 |
int b = positions.get(i + 1).getOffset(); |
2326 |
if (a == b) { |
2327 |
continue; |
2328 |
} |
2329 |
|
2330 |
textCursor &= !(position >= a && position <= b || position >= b && position <= a); |
2331 |
} |
2332 |
} else // stream selection |
2333 |
if (getDot() == getMark()) { |
2334 |
// empty selection |
2335 |
textCursor = true; |
2336 |
} else { |
2337 |
int dot = getDot(); |
2338 |
int mark = getMark(); |
2339 |
if (position >= dot && position <= mark || position >= mark && position <= dot) { |
2340 |
textCursor = false; |
2341 |
} else { |
2342 |
textCursor = true; |
2343 |
} |
2344 |
} |
2345 |
|
2346 |
if (textCursor != showingTextCursor) { |
2347 |
int cursorType = textCursor ? Cursor.TEXT_CURSOR : Cursor.DEFAULT_CURSOR; |
2348 |
component.setCursor(Cursor.getPredefinedCursor(cursorType)); |
2349 |
showingTextCursor = textCursor; |
2350 |
} |
2351 |
} |
2352 |
} |
2353 |
|
2354 |
@Override |
2355 |
public void mouseDragged(MouseEvent evt) { |
2356 |
if (LOG.isLoggable(Level.FINE)) { |
2357 |
LOG.fine("mouseDragged: " + logMouseEvent(evt) + ", state=" + mouseState + '\n'); //NOI18N |
2358 |
} |
2359 |
|
2360 |
if (isLeftMouseButtonExt(evt)) { |
2361 |
JTextComponent c = component; |
2362 |
int offset = mouse2Offset(evt); |
2363 |
int dot = getDot(); |
2364 |
int mark = getMark(); |
2365 |
LineDocument lineDoc = LineDocumentUtils.asRequired(c.getDocument(), LineDocument.class); |
2366 |
|
2367 |
try { |
2368 |
switch (mouseState) { |
2369 |
case DEFAULT: |
2370 |
case DRAG_SELECTION: |
2371 |
break; |
2372 |
|
2373 |
case DRAG_SELECTION_POSSIBLE: |
2374 |
mouseState = MouseState.DRAG_SELECTION; |
2375 |
break; |
2376 |
|
2377 |
case CHAR_SELECTION: |
2378 |
if (evt.isAltDown() && evt.isShiftDown()) { |
2379 |
moveDotCaret(offset, getLastCaretItem()); |
2380 |
} else { |
2381 |
moveDot(offset); |
2382 |
adjustRectangularSelectionMouseX(evt.getX(), evt.getY()); |
2383 |
} |
2384 |
break; // Use the offset under mouse pointer |
2385 |
|
2386 |
case WORD_SELECTION: |
2387 |
// Increase selection if at least in the middle of a word. |
2388 |
// It depends whether selection direction is from lower offsets upward or back. |
2389 |
if (offset >= mark) { // Selection extends forward. |
2390 |
offset = LineDocumentUtils.getWordEnd(lineDoc, offset); |
2391 |
} else { // Selection extends backward. |
2392 |
offset = LineDocumentUtils.getWordStart(lineDoc, offset); |
2393 |
} |
2394 |
selectEnsureMinSelection(mark, dot, offset); |
2395 |
break; |
2396 |
|
2397 |
case LINE_SELECTION: |
2398 |
if (offset >= mark) { // Selection extends forward |
2399 |
offset = Math.min(LineDocumentUtils.getLineEnd(lineDoc, offset) + 1, c.getDocument().getLength()); |
2400 |
} else { // Selection extends backward |
2401 |
offset = LineDocumentUtils.getLineStart(lineDoc, offset); |
2402 |
} |
2403 |
selectEnsureMinSelection(mark, dot, offset); |
2404 |
break; |
2405 |
|
2406 |
default: |
2407 |
throw new AssertionError("Invalid state " + mouseState); // NOI18N |
2408 |
} |
2409 |
} catch (BadLocationException ex) { |
2410 |
Exceptions.printStackTrace(ex); |
2411 |
} |
2412 |
} |
2413 |
} |
2414 |
|
2415 |
// FocusListener methods |
2416 |
public @Override void focusGained(FocusEvent evt) { |
2417 |
if (LOG.isLoggable(Level.FINE)) { |
2418 |
LOG.fine( |
2419 |
"BaseCaret.focusGained(); doc=" + // NOI18N |
2420 |
component.getDocument().getProperty(Document.TitleProperty) + '\n' |
2421 |
); |
2422 |
} |
2423 |
|
2424 |
JTextComponent c = component; |
2425 |
if (c != null) { |
2426 |
updateType(); |
2427 |
if (component.isEnabled()) { |
2428 |
if (component.isEditable()) { |
2429 |
setVisible(true); |
2430 |
} |
2431 |
setSelectionVisible(true); |
2432 |
} |
2433 |
if (LOG.isLoggable(Level.FINER)) { |
2434 |
LOG.finer("Caret visibility: " + isVisible() + '\n'); // NOI18N |
2435 |
} |
2436 |
} else { |
2437 |
if (LOG.isLoggable(Level.FINER)) { |
2438 |
LOG.finer("Text component is null, caret will not be visible" + '\n'); // NOI18N |
2439 |
} |
2440 |
} |
2441 |
} |
2442 |
|
2443 |
public @Override void focusLost(FocusEvent evt) { |
2444 |
if (LOG.isLoggable(Level.FINE)) { |
2445 |
LOG.fine("BaseCaret.focusLost(); doc=" + // NOI18N |
2446 |
component.getDocument().getProperty(Document.TitleProperty) + |
2447 |
"\nFOCUS GAINER: " + evt.getOppositeComponent() + '\n' // NOI18N |
2448 |
); |
2449 |
if (LOG.isLoggable(Level.FINER)) { |
2450 |
LOG.finer("FOCUS EVENT: " + evt + '\n'); // NOI18N |
2451 |
} |
2452 |
} |
2453 |
setVisible(false); |
2454 |
setSelectionVisible(evt.isTemporary()); |
2455 |
} |
2456 |
|
2457 |
// ComponentListener methods |
2458 |
/** |
2459 |
* May be called for either component or horizontal scrollbar. |
2460 |
*/ |
2461 |
public @Override void componentShown(ComponentEvent e) { |
2462 |
// Called when horizontal scrollbar gets visible |
2463 |
// (but the same listener added to component as well so must check first) |
2464 |
// Check whether present caret position will not get hidden |
2465 |
// under horizontal scrollbar and if so scroll the view |
2466 |
Component hScrollBar = e.getComponent(); |
2467 |
if (hScrollBar != component) { // really called for horizontal scrollbar |
2468 |
Component scrollPane = hScrollBar.getParent(); |
2469 |
boolean needsUpdate = false; |
2470 |
List<CaretInfo> sortedCarets = getSortedCarets(); |
2471 |
for (CaretInfo caret : sortedCarets) { // TODO This is wrong, but a quick prototype |
2472 |
CaretItem caretItem = caret.getCaretItem(); |
2473 |
if (caretItem.getCaretBounds() != null && scrollPane instanceof JScrollPane) { |
2474 |
Rectangle viewRect = ((JScrollPane)scrollPane).getViewport().getViewRect(); |
2475 |
Rectangle hScrollBarRect = new Rectangle( |
2476 |
viewRect.x, |
2477 |
viewRect.y + viewRect.height, |
2478 |
hScrollBar.getWidth(), |
2479 |
hScrollBar.getHeight() |
2480 |
); |
2481 |
if (hScrollBarRect.intersects(caretItem.getCaretBounds())) { |
2482 |
// Update caret's position |
2483 |
needsUpdate = true; |
2484 |
} |
2485 |
} |
2486 |
} |
2487 |
if(needsUpdate) { |
2488 |
dispatchUpdate(true); // should be visible so scroll the view |
2489 |
} |
2490 |
} |
2491 |
} |
2492 |
|
2493 |
/** |
2494 |
* May be called for either component or horizontal scrollbar. |
2495 |
*/ |
2496 |
public @Override void componentResized(ComponentEvent e) { |
2497 |
Component c = e.getComponent(); |
2498 |
if (c == component) { // called for component |
2499 |
// In case the caretBounds are still null |
2500 |
// (component not connected to hierarchy yet or it has zero size |
2501 |
// so the modelToView() returned null) re-attempt to compute the bounds. |
2502 |
CaretItem caret = getLastCaretItem(); |
2503 |
if (caret.getCaretBounds() == null) { |
2504 |
dispatchUpdate(true); |
2505 |
if (caret.getCaretBounds() != null) { // detach the listener - no longer necessary |
2506 |
c.removeComponentListener(this); |
2507 |
} |
2508 |
} |
2509 |
} |
2510 |
} |
2511 |
|
2512 |
@Override |
2513 |
public void viewHierarchyChanged(ViewHierarchyEvent evt) { |
2514 |
if (!caretUpdatePending) { |
2515 |
caretUpdatePending = true; |
2516 |
SwingUtilities.invokeLater(new Runnable() { |
2517 |
@Override |
2518 |
public void run() { |
2519 |
update(false); |
2520 |
} |
2521 |
}); |
2522 |
} |
2523 |
} |
2524 |
|
2525 |
@Override |
2526 |
public void keyPressed(KeyEvent e) { |
2527 |
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { |
2528 |
// Retain just the last caret |
2529 |
retainLastCaretOnly(); |
2530 |
} |
2531 |
} |
2532 |
|
2533 |
@Override |
2534 |
public void keyTyped(KeyEvent e) { |
2535 |
} |
2536 |
|
2537 |
@Override |
2538 |
public void keyReleased(KeyEvent e) { |
2539 |
} |
2540 |
|
2541 |
} // End of ListenerImpl class |
2542 |
|
2543 |
|
2544 |
private enum CaretType { |
2545 |
|
2546 |
/** |
2547 |
* Two-pixel line caret (the default). |
2548 |
*/ |
2549 |
THICK_LINE_CARET, |
2550 |
|
2551 |
/** |
2552 |
* Thin one-pixel line caret. |
2553 |
*/ |
2554 |
THIN_LINE_CARET, |
2555 |
|
2556 |
/** |
2557 |
* Rectangle corresponding to a single character. |
2558 |
*/ |
2559 |
BLOCK_CARET; |
2560 |
|
2561 |
static CaretType decode(String typeStr) { |
2562 |
switch (typeStr) { |
2563 |
case EditorPreferencesDefaults.THIN_LINE_CARET: |
2564 |
return THIN_LINE_CARET; |
2565 |
case EditorPreferencesDefaults.BLOCK_CARET: |
2566 |
return BLOCK_CARET; |
2567 |
default: |
2568 |
return THICK_LINE_CARET; |
2569 |
} |
2570 |
}; |
2571 |
|
2572 |
} |
2573 |
|
2574 |
private static enum MouseState { |
2575 |
|
2576 |
DEFAULT, // Mouse released; not extending any selection |
2577 |
CHAR_SELECTION, // Extending character selection after single mouse press |
2578 |
WORD_SELECTION, // Extending word selection after double-click when mouse button still pressed |
2579 |
LINE_SELECTION, // Extending line selection after triple-click when mouse button still pressed |
2580 |
DRAG_SELECTION_POSSIBLE, // There was a selected text when mouse press arrived so drag is possible |
2581 |
DRAG_SELECTION // Drag is being done (text selection existed at the mouse press) |
2582 |
|
2583 |
} |
2584 |
|
2585 |
} |