2
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4
* Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
6
* The contents of this file are subject to the terms of either the GNU
7
* General Public License Version 2 only ("GPL") or the Common
8
* Development and Distribution License("CDDL") (collectively, the
9
* "License"). You may not use this file except in compliance with the
10
* License. You can obtain a copy of the License at
11
* http://www.netbeans.org/cddl-gplv2.html
12
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13
* specific language governing permissions and limitations under the
14
* License. When distributing the software, include this License Header
15
* Notice in each file and include the License file at
16
* nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
17
* particular file as subject to the "Classpath" exception as provided
18
* by Sun in the GPL Version 2 section of the License file that
19
* accompanied this code. If applicable, add the following below the
20
* License Header, with the fields enclosed by brackets [] replaced by
21
* your own identifying information:
22
* "Portions Copyrighted [year] [name of copyright owner]"
26
* The Original Software is NetBeans. The Initial Developer of the Original
27
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
28
* Microsystems, Inc. All Rights Reserved.
30
* If you wish your version of this file to be governed by only the CDDL
31
* or only the GPL Version 2, indicate your decision by adding
32
* "[Contributor] elects to include this software in this distribution
33
* under the [CDDL or GPL Version 2] license." If you do not indicate a
34
* single choice of license, a recipient has the option to distribute
35
* your version of this file under either the CDDL, the GPL Version 2 or
36
* to extend the choice of license to its licensees as provided above.
37
* However, if you add GPL Version 2 code and therefore, elected the GPL
38
* Version 2 license, then the option applies only if the new code is
39
* made subject to such option by the copyright holder.
42
package org.netbeans.core.output2.ui;
44
import java.awt.Rectangle;
45
import javax.swing.plaf.TextUI;
48
import javax.swing.event.ChangeEvent;
49
import javax.swing.event.ChangeListener;
50
import javax.swing.event.DocumentEvent;
51
import javax.swing.event.DocumentListener;
52
import javax.swing.text.*;
54
import java.awt.event.*;
55
import org.netbeans.core.output2.OutputDocument;
56
import org.openide.util.Exceptions;
59
* A scroll pane containing an editor pane, with special handling of the caret
60
* and scrollbar - until a keyboard or mouse event, after a call to setDocument(),
61
* the caret and scrollbar are locked to the last line of the document. This avoids
62
* "jumping" scrollbars as the position of the caret (and thus the scrollbar) get updated
63
* to reposition them at the bottom of the document on every document change.
65
* @author Tim Boudreau
67
public abstract class AbstractOutputPane extends JScrollPane implements DocumentListener, MouseListener, MouseMotionListener, KeyListener, ChangeListener, MouseWheelListener, Runnable {
68
private boolean locked = true;
70
private int fontHeight = -1;
71
private int fontWidth = -1;
72
protected JEditorPane textView;
73
int lastCaretLine = 0;
74
boolean hadSelection = false;
75
boolean recentlyReset = false;
77
public AbstractOutputPane() {
78
textView = createTextView();
83
public void doUpdateCaret() {
84
Caret car = textView.getCaret();
85
if (car instanceof DefaultCaret) {
86
((DefaultCaret)car).setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
90
public void dontUpdateCaret() {
91
Caret car = textView.getCaret();
92
if (car instanceof DefaultCaret) {
93
((DefaultCaret)car).setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
98
public void requestFocus() {
99
textView.requestFocus();
103
public boolean requestFocusInWindow() {
104
return textView.requestFocusInWindow();
107
protected abstract JEditorPane createTextView();
109
protected void documentChanged() {
111
if (pendingCaretLine != -1) {
112
if (!sendCaretToLine (pendingCaretLine, pendingCaretSelect)) {
113
ensureCaretPosition();
116
ensureCaretPosition();
118
if (recentlyReset && isShowing()) {
119
recentlyReset = false;
125
//Saves having OutputEditorKit have to do its own listening
126
getViewport().revalidate();
127
getViewport().repaint();
131
public abstract boolean isWrapped();
132
public abstract void setWrapped (boolean val);
134
public boolean hasSelection() {
135
return textView.getSelectionStart() != textView.getSelectionEnd();
138
boolean isScrollLocked() {
143
* Ensure that the document is scrolled all the way to the bottom (unless
144
* some user event like scrolling or placing the caret has unlocked it).
146
* Note that this method is always called on the event queue, since
147
* OutputDocument only fires changes on the event queue.
149
public final void ensureCaretPosition() {
151
//Make sure the scrollbar is updated *after* the document change
152
//has been processed and the scrollbar model's maximum updated
154
SwingUtilities.invokeLater(this);
160
/** True when invokeLater has already been called on this instance */
161
private boolean enqueued = false;
163
* Scrolls the pane to the bottom, invokeLatered to ensure all state has
164
* been updated, so the bottom really *is* the bottom.
168
getVerticalScrollBar().setValue(getVerticalScrollBar().getModel().getMaximum());
169
getHorizontalScrollBar().setValue(getHorizontalScrollBar().getModel().getMinimum());
172
public int getSelectionStart() {
173
return textView.getSelectionStart();
176
public int getSelectionEnd() {
177
return textView.getSelectionEnd();
180
public void setSelection (int start, int end) {
181
int rstart = Math.min (start, end);
182
int rend = Math.max (start, end);
183
if (rstart == rend) {
184
getCaret().setDot(rstart);
186
textView.setSelectionStart(rstart);
187
textView.setSelectionEnd(rend);
191
public void selectAll() {
193
getCaret().setVisible(true);
194
textView.setSelectionStart(0);
195
textView.setSelectionEnd(getLength());
198
public boolean isAllSelected() {
199
return textView.getSelectionStart() == 0 && textView.getSelectionEnd() == getLength();
202
protected void init() {
203
setViewportView(textView);
204
textView.setEditable(false);
206
textView.addMouseListener(this);
207
textView.addMouseWheelListener(this);
208
textView.addMouseMotionListener(this);
209
textView.addKeyListener(this);
211
OCaret oc = new OCaret();
212
oc.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
213
textView.setCaret (oc);
215
getCaret().setVisible(true);
216
getCaret().setBlinkRate(0);
217
getCaret().setSelectionVisible(true);
219
getVerticalScrollBar().getModel().addChangeListener(this);
220
getVerticalScrollBar().addMouseMotionListener(this);
222
getViewport().addMouseListener(this);
223
getVerticalScrollBar().addMouseListener(this);
224
setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_AS_NEEDED);
225
setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_ALWAYS);
226
addMouseListener(this);
228
getCaret().addChangeListener(this);
229
Integer i = (Integer) UIManager.get("customFontSize"); //NOI18N
234
Font f = (Font) UIManager.get("controlFont");
235
size = f != null ? f.getSize() : 11;
237
textView.setFont (new Font ("Monospaced", Font.PLAIN, size)); //NOI18N
238
setBorder (BorderFactory.createEmptyBorder());
239
setViewportBorder (BorderFactory.createEmptyBorder());
241
Color c = UIManager.getColor("nb.output.selectionBackground");
243
textView.setSelectionColor(c);
247
public final Document getDocument() {
248
return textView.getDocument();
252
* This method is here for use *only* by unit tests.
254
public final JTextComponent getTextView() {
258
public final void copy() {
259
if (getCaret().getDot() != getCaret().getMark()) {
262
Toolkit.getDefaultToolkit().beep();
266
protected void setDocument (Document doc) {
267
if (hasSelection()) {
268
hasSelectionChanged(false);
270
hadSelection = false;
273
Document old = textView.getDocument();
274
old.removeDocumentListener(this);
276
textView.setDocument(doc);
277
doc.addDocumentListener(this);
279
recentlyReset = true;
280
pendingCaretLine = -1;
282
textView.setDocument (new PlainDocument());
283
textView.setEditorKit(new DefaultEditorKit());
287
protected void setEditorKit(EditorKit kit) {
288
Document doc = textView.getDocument();
290
textView.setEditorKit(kit);
291
textView.setDocument(doc);
293
getCaret().setVisible(true);
294
getCaret().setBlinkRate(0);
298
* Setting the editor kit will clear the action map/key map connection
299
* to the TopComponent, so we reset it here.
301
protected final void updateKeyBindings() {
302
Keymap keymap = textView.getKeymap();
303
keymap.removeKeyStrokeBinding(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0));
306
protected EditorKit getEditorKit() {
307
return textView.getEditorKit();
310
public final int getLineCount() {
311
return textView.getDocument().getDefaultRootElement().getElementCount();
314
private int lastLength = -1;
315
public final int getLength() {
316
if (lastLength == -1) {
317
lastLength = textView.getDocument().getLength();
323
* If we are sending the caret to a hyperlinked line, but it is < 3 lines
324
* from the bottom, we will hold the line number in this field until there
325
* are enough lines that it will be semi-centered.
327
private int pendingCaretLine = -1;
328
private boolean pendingCaretSelect = false;
329
private boolean inSendCaretToLine = false;
331
public final boolean sendCaretToLine(int idx, boolean select) {
332
int count = getLineCount();
333
if (count - idx < 3) {
334
pendingCaretLine = idx;
335
pendingCaretSelect = select;
338
inSendCaretToLine = true;
339
pendingCaretLine = -1;
341
getCaret().setVisible(true);
342
getCaret().setSelectionVisible(true);
343
Element el = textView.getDocument().getDefaultRootElement().getElement(Math.min(idx, getLineCount() - 1));
344
int position = el.getStartOffset();
346
getCaret().setDot (el.getEndOffset()-1);
347
getCaret().moveDot (position);
348
getCaret().setSelectionVisible(true);
351
getCaret().setDot(position);
353
if (idx + 3 < getLineCount()) {
355
Rectangle r = textView.modelToView(textView.getDocument().getDefaultRootElement().getElement(idx + 3).getStartOffset());
356
if (r != null) { //Will be null if maximized - no parent, no coordinate space
357
textView.scrollRectToVisible(r);
359
} catch (BadLocationException ble) {
360
Exceptions.printStackTrace(ble);
363
inSendCaretToLine = false;
369
public final void lockScroll() {
375
public final void unlockScroll() {
381
protected abstract void caretEnteredLine (int line);
383
protected abstract void lineClicked (int line, Point p);
385
protected abstract void postPopupMenu (Point p, Component src);
387
public final int getCaretLine() {
389
int charPos = getCaret().getDot();
391
result = textView.getDocument().getDefaultRootElement().getElementIndex(charPos);
396
public final int getCaretPos() {
397
return getCaret().getDot();
401
public final void paint (Graphics g) {
402
if (fontHeight == -1) {
403
fontHeight = g.getFontMetrics(textView.getFont()).getHeight();
404
fontWidth = g.getFontMetrics(textView.getFont()).charWidth('m'); //NOI18N
409
//***********************Listener implementations*****************************
411
public void stateChanged(ChangeEvent e) {
412
if (e.getSource() instanceof JViewport) {
414
ensureCaretPosition();
416
} else if (e.getSource() == getVerticalScrollBar().getModel()) {
417
if (!locked) { //XXX check if doc is still being written?
418
BoundedRangeModel mdl = getVerticalScrollBar().getModel();
419
if (mdl.getValue() + mdl.getExtent() == mdl.getMaximum()) {
425
maybeSendCaretEnteredLine();
427
boolean hasSelection = textView.getSelectionStart() != textView.getSelectionEnd();
428
if (hasSelection != hadSelection) {
429
hadSelection = hasSelection;
430
hasSelectionChanged (hasSelection);
435
private boolean caretLineChanged() {
436
int line = getCaretLine();
437
boolean result = line != lastCaretLine;
438
lastCaretLine = line;
442
private void maybeSendCaretEnteredLine() {
443
if (EventQueue.getCurrentEvent() instanceof MouseEvent) {
444
//User may have clicked a hyperlink, in which case, we'll test
445
//it and see if it's really in the text of the hyperlink - so
446
//don't do anything here
449
//Don't message the controller if we're programmatically setting
450
//the selection, or if the caret moved because output was written -
451
//it can cause the controller to send events to OutputListeners which
452
//should only happen for user events
453
if ((!locked && caretLineChanged()) && !inSendCaretToLine) {
454
int line = getCaretLine();
455
boolean sel = textView.getSelectionStart() != textView.getSelectionEnd();
456
if (line != -1 && !sel) {
457
caretEnteredLine(getCaretLine());
459
if (sel != hadSelection) {
461
hasSelectionChanged (sel);
467
private void hasSelectionChanged(boolean sel) {
468
((AbstractOutputTab) getParent()).hasSelectionChanged(sel);
471
public final void changedUpdate(DocumentEvent e) {
472
//Ensure it is consumed
475
if (e.getOffset() >= getCaretPos() && (locked || !(e instanceof OutputDocument.DO))) {
476
//#119985 only move caret when not in editable section
477
OutputDocument doc = (OutputDocument)e.getDocument();
478
if (! (e instanceof OutputDocument.DO) && getCaretPos() >= doc.getOutputLength()) {
482
getCaret().setDot(e.getOffset() + e.getLength());
486
public final void insertUpdate(DocumentEvent e) {
487
//Ensure it is consumed
490
if (e.getOffset() >= getCaretPos() && (locked || !(e instanceof OutputDocument.DO))) {
491
//#119985 only move caret when not in editable section
492
OutputDocument doc = (OutputDocument)e.getDocument();
493
if (! (e instanceof OutputDocument.DO) && getCaretPos() >= doc.getOutputLength()) {
497
getCaret().setDot(e.getOffset() + e.getLength());
501
public final void removeUpdate(DocumentEvent e) {
502
//Ensure it is consumed
507
public void mouseClicked(MouseEvent e) {
510
public void mouseEntered(MouseEvent e) {
513
public void mouseExited(MouseEvent e) {
517
private int mouseLine = -1;
518
public void setMouseLine (int line, Point p) {
519
if (mouseLine != line) {
524
public final void setMouseLine (int line) {
525
setMouseLine (line, null);
529
public void mouseMoved(MouseEvent e) {
530
Point p = e.getPoint();
531
int pos = textView.viewToModel(p);
532
if (pos < getLength()) {
533
int line = getDocument().getDefaultRootElement().getElementIndex(pos);
534
int lineStart = getDocument().getDefaultRootElement().getElement(line).getStartOffset();
535
int lineLength = getDocument().getDefaultRootElement().getElement(line).getEndOffset() -
539
Rectangle r = textView.modelToView(lineStart + lineLength -1);
540
int maxX = r.x + r.width;
541
boolean inLine = p.x <= maxX;
543
Rectangle ra = textView.modelToView(lineStart);
552
setMouseLine (line, p);
556
} catch (BadLocationException ble) {
562
public void mouseDragged(MouseEvent e) {
563
if (e.getSource() == getVerticalScrollBar()) {
565
if (y > getVerticalScrollBar().getHeight()) {
571
public void mousePressed(MouseEvent e) {
572
if (locked && !e.isPopupTrigger()) {
573
Element el = getDocument().getDefaultRootElement().getElement(getLineCount()-1);
574
getCaret().setDot(el.getStartOffset());
576
//We should now set the caret position so the caret doesn't
577
//seem to ignore the first click
578
if (e.getSource() == textView) {
579
getCaret().setDot (textView.viewToModel(e.getPoint()));
582
if (e.isPopupTrigger()) {
583
//Convert immediately to our component space - if the
584
//text view scrolls before the component is opened, popup can
585
//appear above the top of the screen
586
Point p = SwingUtilities.convertPoint((Component) e.getSource(),
589
postPopupMenu (p, this);
593
public final void mouseReleased(MouseEvent e) {
594
if (e.getSource() == textView && SwingUtilities.isLeftMouseButton(e)) {
595
int pos = textView.viewToModel(e.getPoint());
597
int line = textView.getDocument().getDefaultRootElement().getElementIndex(pos);
599
lineClicked(line, e.getPoint());
600
e.consume(); //do NOT allow this window's caret to steal the focus from editor window
604
if (e.isPopupTrigger()) {
605
Point p = SwingUtilities.convertPoint((Component) e.getSource(),
606
//Convert immediately to our component space - if the
607
//text view scrolls before the component is opened, popup can
608
//appear above the top of the screen
611
postPopupMenu (p, this);
615
public void keyPressed(KeyEvent keyEvent) {
616
if (keyEvent.getKeyCode() == KeyEvent.VK_END) {
623
public void keyReleased(KeyEvent keyEvent) {
626
public void keyTyped(KeyEvent keyEvent) {
629
public final void mouseWheelMoved(MouseWheelEvent e) {
630
BoundedRangeModel sbmodel = getVerticalScrollBar().getModel();
631
int max = sbmodel.getMaximum();
632
int range = sbmodel.getExtent();
634
int currPosition = sbmodel.getValue();
635
if (e.getSource() == textView) {
636
int newPosition = Math.max (0, Math.min (sbmodel.getMaximum(),
637
currPosition + (e.getUnitsToScroll() * textView.getFontMetrics(textView.getFont()).getHeight())));
638
// height is a magic constant because of #57532
639
sbmodel.setValue (newPosition);
640
if (newPosition + range >= max) {
649
return textView.getCaret();
652
private class OCaret extends DefaultCaret {
654
public void setSelectionVisible(boolean val) {
655
super.setSelectionVisible(true);
656
super.setBlinkRate(0);
659
public boolean isSelectionVisible() {
663
public void setBlinkRate(int rate) {
664
super.setBlinkRate(0);
668
public boolean isVisible() { return true; }
671
public void paint(Graphics g) {
672
JTextComponent component = textView;
673
if(isVisible() && y >= 0) {
675
TextUI mapper = component.getUI();
676
Rectangle r = mapper.modelToView(component, getDot(), Position.Bias.Forward);
678
if ((r == null) || ((r.width == 0) && (r.height == 0))) {
681
if (width > 0 && height > 0 &&
682
!this._contains(r.x, r.y, r.width, r.height)) {
683
// We seem to have gotten out of sync and no longer
684
// contain the right location, adjust accordingly.
685
Rectangle clip = g.getClipBounds();
687
if (clip != null && !clip.contains(this)) {
688
// Clip doesn't contain the old location, force it
689
// to be repainted lest we leave a caret around.
692
// System.err.println("WRONG! Caret dot m2v = " + r + " but my bounds are " + x + "," + y + "," + width + "," + height);
694
// This will potentially cause a repaint of something
695
// we're already repainting, but without changing the
696
// semantics of damage we can't really get around this.
699
g.setColor(component.getCaretColor());
700
g.drawLine(r.x, r.y, r.x, r.y + r.height - 1);
701
g.drawLine(r.x+1, r.y, r.x+1, r.y + r.height - 1);
703
} catch (BadLocationException e) {
704
// can't render I guess
705
// System.err.println("Can't render cursor");
710
private boolean _contains(int X, int Y, int W, int H) {
713
if ((w | h | W | H) < 0) {
714
// At least one of the dimensions is negative...
717
// Note: if any dimension is zero, tests below must return false...
720
if (X < x || Y < y) {
727
// X+W overflowed or W was zero, return false if...
728
// either original w or W was zero or
729
// x+w did not overflow or
730
// the overflowed x+w is smaller than the overflowed X+W
731
if (w >= x || W > w) {
735
// X+W did not overflow and W was not zero, return false if...
736
// original w was zero or
737
// x+w did not overflow and x+w is smaller than X+W
738
if (w >= x && W > w) {
739
//This is the bug in DefaultCaret - returns false here
744
else if ((x + w) < X) {
751
if (h >= y || H > h) return false;
753
if (h >= y && H > h) return false;
756
else if ((y + h) < Y) {
763
public void mouseReleased(MouseEvent e) {
764
if( !e.isConsumed() ) {
765
super.mouseReleased(e);