/*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License
* Version 1.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://www.sun.com/
*
* The Original Code is NetBeans. The Initial Developer of the Original
* Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun
* Microsystems, Inc. All Rights Reserved.
*/
package org.netbeans.editor;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.TextUI;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.JTextComponent;
import javax.swing.text.View;
import java.awt.*;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
/**
* Editor UI for the component. All the additional UI features
* like advanced scrolling, info about fonts, abbreviations,
* keyword matching are based on this class.
*
* @author Miloslav Metelka
* @version 1.00
*/
public class EditorUI implements ChangeListener, PropertyChangeListener, SettingsChangeListener {
public static final String OVERWRITE_MODE_PROPERTY = "overwriteMode"; // NOI18N
public static final String COMPONENT_PROPERTY = "component"; // NOI18N
/** Default scrolling type is used for the standard
* setDot() call. If the area is on the screen, it
* jumps to it, otherwise it centers the requested area
* vertically in the middle of the window and it uses
* smallest covering on the right side.
*/
public static final int SCROLL_DEFAULT = 0;
/** Scrolling type used for regular caret moves.
* The scrollJump is used when the caret requests area outside the screen.
*/
public static final int SCROLL_MOVE = 1;
/** Scrolling type where the smallest covering
* for the requested rectangle is used. It's useful
* for going to the end of the line for example.
*/
public static final int SCROLL_SMALLEST = 2;
/** Scrolling type for find operations, that can
* request additional configurable area in each
* direction, so the context around is visible too.
*/
public static final int SCROLL_FIND = 3;
private static final Insets NULL_INSETS = new Insets(0, 0, 0, 0);
private static final Dimension NULL_DIMENSION = new Dimension(0, 0);
private static final int STYLE_CNT = 4;
private static final boolean debugUpdateLineHeight
= Boolean.getBoolean("netbeans.debug.editor.updateLineHeight");
/** Map holding the coloring maps for the different languages.
* It helps to minimize the amount of the coloring maps
* and also save the time necessary for their creation.
*/
private static final HashMap sharedColoringMaps = new HashMap(57);
private static final SettingsChangeListener clearingListener
= new SettingsChangeListener() {
public void settingsChange(SettingsChangeEvent evt) {
// Fired when the Settings are locked
sharedColoringMaps.clear();
}
};
static {
Settings.addSettingsChangeListener( clearingListener );
}
public void clearSharedColoringMaps()
{
sharedColoringMaps.clear();
coloringMap=null;
}
/** Component this extended UI is related to. */
private JTextComponent component;
private JComponent extComponent;
/** Property change support for firing property changes */
PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
/** Document for the case ext ui is constructed without the component */
private BaseDocument printDoc;
/** Draw layer chain */
private DrawLayerList drawLayerList = new DrawLayerList();
/** Map holding the [name, coloring] pairs */
private Map coloringMap;
/** Character (or better line) height. Particular view can use a different
* character height however most views will probably use this one.
*/
private int lineHeight = 1; // prevent possible division by zero
private float lineHeightCorrection = 1.0f;
/** Ascent of the line which is maximum ascent of all the fonts used. */
private int lineAscent;
/** Width of the space in the default coloring's font */
int defaultSpaceWidth = 1;
/** Flag to initialize fonts */
private boolean fontsInited;
/** First paint after preferenceChanged after fonts were inited. */
private boolean fontsInitedPreferenceChanged;
/** Should the search words be colored? */
boolean highlightSearch;
/** Enable displaying line numbers. Both this flag and <tt>lineNumberVisibleSetting</tt>
* must be true to have the line numbers visible in the window. This flag is false
* by default. It's turned on automatically if the getExtComponent is called.
*/
boolean lineNumberEnabled;
/** This flag corresponds to the LINE_NUMBER_VISIBLE setting. */
boolean lineNumberVisibleSetting;
/** Whether to show line numbers or not. This flag is obtained using bitwise AND
* operation on lineNumberEnabled flag and lineNumberVisibleSetting flag.
*/
boolean lineNumberVisible;
/** Line number total width with indentation. It includes left and right
* line-number margins and lineNumberDigitWidth * lineNumberMaxDigitCount.
*/
int lineNumberWidth;
/** Width of one digit used for line numbering. It's based
* on the information from the line coloring.
*/
int lineNumberDigitWidth;
/** Current maximum count of digits in line number */
int lineNumberMaxDigitCount;
/** Margin on the left and right side of the line number */
Insets lineNumberMargin;
/** This is the size of the editor as component while the real size
* of the lines edited can be lower. The reason why to use this
* virtual size is that each resizing of the component means
* revalidating and therefore repainting of the whole component.
*/
Rectangle virtualSize = new Rectangle();
// /** This is the increment by which the size of the component
// * is increased.
// */
// Rectangle virtualSizeIncrement = new Rectangle(); !!!
/** Margin between the line-number bar and the text. */
int textLeftMarginWidth;
/** This is the full margin around the text. The left margin
* is an addition of component's margin and lineNumberWidth
* and textLeftMarginWidth.
*/
Insets textMargin = NULL_INSETS;
/** How much columns/lines to add when the scroll is performed
* so that the component is not scrolled so often.
* Negative number means portion of the extent width/height
*/
Insets scrollJumpInsets;
/** How much columns/lines to add when the scroll is performed
* so that the component is not scrolled so often.
* Negative number means portion of the extent width/height
*/
Insets scrollFindInsets;
/** Flag saying whether either the width or height in virtualSize
* were updated.
*/
boolean virtualSizeUpdated;
/** Listener to changes in settings */
private PropertyChangeListener settingsListener;
/** EditorUI properties */
Hashtable props = new Hashtable(11);
boolean textLimitLineVisible;
Color textLimitLineColor;
int textLimitWidth;
private Rectangle lastExtentBounds = new Rectangle();
private Dimension componentSizeIncrement = new Dimension();
private Abbrev abbrev;
private WordMatch wordMatch;
private Object componentLock;
/** Status bar */
StatusBar statusBar;
private FocusAdapter focusL;
Map renderingHints;
/** Glyph gutter used for drawing of annotation glyph icons. */
private GlyphGutter glyphGutter = null;
/** The line numbers can be shown in glyph gutter and therefore it is necessary
* to disable drawing of lines here. During the printing on the the other hand, line
* numbers must be visible. */
private boolean disableLineNumbers = true;
/** Left right corner of the JScrollPane */
private JPanel glyphCorner;
/** Construct extended UI for the use with a text component */
public EditorUI() {
Settings.addSettingsChangeListener(this);
focusL = new FocusAdapter() {
public void focusGained(FocusEvent evt) {
Registry.activate(getComponent());
/* Fix of #25475 - copyAction's enabled flag
* must be updated on focus change
*/
stateChanged(null);
}
};
}
/** Construct extended UI for printing the given document */
public EditorUI(BaseDocument printDoc) {
this.printDoc = printDoc;
settingsChange(null);
setLineNumberEnabled(true);
updateLineNumberWidth(0);
drawLayerList.add(printDoc.getDrawLayerList());
}
/** Gets the coloring map that can be shared by the components
* with the same kit. Only the component coloring map is provided.
*/
protected static Map getSharedColoringMap(Class kitClass) {
synchronized (Settings.class) { // must sync like this against dedloks
Map cm = (Map)sharedColoringMaps.get(kitClass);
if (cm == null) {
cm = SettingsUtil.getColoringMap(kitClass, false, true);
// Test if there's a default coloring
// if (cm.get(SettingsNames.DEFAULT_COLORING) == null) {
cm.put(SettingsNames.DEFAULT_COLORING, SettingsDefaults.defaultColoring);
// }
sharedColoringMaps.put(kitClass, cm);
}
return cm;
}
}
/** Called when the <tt>BaseTextUI</tt> is being installed
* into the component.
*/
public void installUI(JTextComponent c) {
synchronized (getComponentLock()) {
this.component = c;
putProperty(COMPONENT_PROPERTY, c);
// listen on component
component.addPropertyChangeListener(this);
component.addFocusListener(focusL);
// listen on caret
Caret caret = component.getCaret();
if (caret != null) {
caret.addChangeListener(this);
}
BaseDocument doc = getDocument();
if (doc != null) {
modelChanged(null, doc);
}
}
// Make sure all the things depending on non-null component will be updated
settingsChange(null);
// fix for issue #16352
getDefaultColoring().apply(component);
}
/** Called when the <tt>BaseTextUI</tt> is being uninstalled
* from the component.
*/
public void uninstallUI(JTextComponent c) {
synchronized (getComponentLock()) {
// fix for issue 12996
if (component != null) {
// stop listening on caret
Caret caret = component.getCaret();
if (caret != null) {
caret.removeChangeListener(this);
}
// stop listening on component
component.removePropertyChangeListener(this);
component.removeFocusListener(focusL);
}
BaseDocument doc = getDocument();
if (doc != null) {
modelChanged(doc, null);
}
component = null;
putProperty(COMPONENT_PROPERTY, null);
// Clear the font-metrics cache
FontMetricsCache.clear();
}
}
/** Get the lock assuring the component will not be changed
* by <tt>installUI()</tt> or <tt>uninstallUI()</tt>.
* It's useful for the classes that want to listen for the
* component change in <tt>EditorUI</tt>.
*/
public Object getComponentLock() {
if (componentLock == null) {
componentLock = new ComponentLock();
}
return componentLock;
}
static class ComponentLock {};
public void addPropertyChangeListener(PropertyChangeListener l) {
propertyChangeSupport.addPropertyChangeListener(l);
}
public void addPropertyChangeListener(String propertyName, PropertyChangeListener l) {
propertyChangeSupport.addPropertyChangeListener(propertyName, l);
}
public void removePropertyChangeListener(PropertyChangeListener l) {
propertyChangeSupport.removePropertyChangeListener(l);
}
public void removePropertyChangeListener(String propertyName, PropertyChangeListener l) {
propertyChangeSupport.removePropertyChangeListener(propertyName, l);
}
protected final void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
propertyChangeSupport.firePropertyChange(propertyName, oldValue, newValue);
}
public void settingsChange(SettingsChangeEvent evt) {
if (component != null) {
if (Utilities.getKit(component) == null) {
return; // prevent problems if not garbage collected and settings changed
}
}
Class kitClass = getKitClass();
String settingName = (evt != null) ? evt.getSettingName() : null;
if (settingName == null || SettingsNames.LINE_NUMBER_VISIBLE.equals(settingName)
|| SettingsNames.PRINT_LINE_NUMBER_VISIBLE.equals(settingName)
) {
lineNumberVisibleSetting = SettingsUtil.getBoolean(kitClass, (component != null)
? SettingsNames.LINE_NUMBER_VISIBLE : SettingsNames.PRINT_LINE_NUMBER_VISIBLE,
(component != null) ? SettingsDefaults.defaultLineNumberVisible
: SettingsDefaults.defaultPrintLineNumberVisible
);
lineNumberVisible = lineNumberEnabled && lineNumberVisibleSetting;
// if this is printing, the drawing of original line numbers must be enabled
if (component == null)
disableLineNumbers = false;
if (disableLineNumbers)
lineNumberVisible = false;
}
BaseDocument doc = getDocument();
if (doc != null) {
if (settingName == null || SettingsNames.LINE_NUMBER_MARGIN.equals(settingName)) {
Object value = Settings.getValue(kitClass, SettingsNames.LINE_NUMBER_MARGIN);
lineNumberMargin = (value instanceof Insets) ? (Insets)value : NULL_INSETS;
}
if (settingName == null || SettingsNames.TEXT_LEFT_MARGIN_WIDTH.equals(settingName)) {
textLeftMarginWidth = SettingsUtil.getInteger(kitClass,
SettingsNames.TEXT_LEFT_MARGIN_WIDTH,
SettingsDefaults.defaultTextLeftMarginWidth);
}
if (settingName == null || SettingsNames.LINE_HEIGHT_CORRECTION.equals(settingName)) {
Object value = Settings.getValue(kitClass, SettingsNames.LINE_HEIGHT_CORRECTION);
if (!(value instanceof Float) || ((Float)value).floatValue() < 0) {
value = SettingsDefaults.defaultLineHeightCorrection;
}
lineHeightCorrection = ((Float)value).floatValue();
}
if (settingName == null || SettingsNames.TEXT_LIMIT_LINE_VISIBLE.equals(settingName)) {
textLimitLineVisible = SettingsUtil.getBoolean(kitClass,
SettingsNames.TEXT_LIMIT_LINE_VISIBLE, SettingsDefaults.defaultTextLimitLineVisible);
}
if (settingName == null || SettingsNames.TEXT_LIMIT_LINE_COLOR.equals(settingName)) {
Object value = Settings.getValue(kitClass, SettingsNames.TEXT_LIMIT_LINE_COLOR);
textLimitLineColor = (value instanceof Color) ? (Color)value
: SettingsDefaults.defaultTextLimitLineColor;
}
if (settingName == null || SettingsNames.TEXT_LIMIT_WIDTH.equals(settingName)) {
textLimitWidth = SettingsUtil.getPositiveInteger(kitClass,
SettingsNames.TEXT_LIMIT_WIDTH, SettingsDefaults.defaultTextLimitWidth);
}
// component only properties
if (component != null) {
if (settingName == null || SettingsNames.SCROLL_JUMP_INSETS.equals(settingName)) {
Object value = Settings.getValue(kitClass, SettingsNames.SCROLL_JUMP_INSETS);
scrollJumpInsets = (value instanceof Insets) ? (Insets)value : NULL_INSETS;
}
if (settingName == null || SettingsNames.SCROLL_FIND_INSETS.equals(settingName)) {
Object value = Settings.getValue(kitClass, SettingsNames.SCROLL_FIND_INSETS);
scrollFindInsets = (value instanceof Insets) ? (Insets)value : NULL_INSETS;
}
if (settingName == null || SettingsNames.COMPONENT_SIZE_INCREMENT.equals(settingName)) {
Object value = Settings.getValue(kitClass, SettingsNames.COMPONENT_SIZE_INCREMENT);
componentSizeIncrement = (value instanceof Dimension) ? (Dimension)value : NULL_DIMENSION;
}
if (settingName == null || SettingsNames.RENDERING_HINTS.equals(settingName)) {
Object value = Settings.getValue(kitClass, SettingsNames.RENDERING_HINTS);
renderingHints = (value instanceof Map) ? (Map)value : null;
}
if (settingName == null || SettingsNames.CARET_COLOR_INSERT_MODE.equals(settingName)
|| SettingsNames.CARET_COLOR_OVERWRITE_MODE.equals(settingName)
) {
Boolean b = (Boolean)getProperty(OVERWRITE_MODE_PROPERTY);
Color caretColor;
if (b == null || !b.booleanValue()) {
Object value = Settings.getValue(kitClass, SettingsNames.CARET_COLOR_INSERT_MODE);
caretColor = (value instanceof Color) ? (Color)value
: SettingsDefaults.defaultCaretColorInsertMode;
} else {
Object value = Settings.getValue(kitClass, SettingsNames.CARET_COLOR_OVERWRITE_MODE);
caretColor = (value instanceof Color) ? (Color)value
: SettingsDefaults.defaultCaretColorOvwerwriteMode;
}
if (caretColor != null) {
component.setCaretColor(caretColor);
}
}
// fix for issues 13842, 14003
if (SwingUtilities.isEventDispatchThread()) {
component.setKeymap(Utilities.getKit(component).getKeymap());
BaseTextUI ui = (BaseTextUI)component.getUI();
ui.updateHeight();
component.repaint();
} else {
SwingUtilities.invokeLater(
new Runnable() {
public void run() {
JTextComponent c = component;
if (c != null) {
BaseKit kit = Utilities.getKit(c);
if (kit != null) {
c.setKeymap(kit.getKeymap());
BaseTextUI ui = (BaseTextUI)c.getUI();
if (ui != null) {
ui.updateHeight();
c.repaint();
}
}
}
}
}
);
}
}
}
coloringMap = null; // reset coloring map so it's lazily rebuilt
/* make sure there's no pending preferenceChanged() request
* because if it would be then the fontsInited = false
* would have no effect.
*/
fontsInitedPreferenceChanged = false;
fontsInited = false;
}
public void stateChanged(ChangeEvent evt) {
SwingUtilities.invokeLater(
new Runnable() {
/** @return true if the document supports guarded sections
* and when either the caret is in guarded block
* or when selection spans any guarded block(s).
*/
private boolean isCaretGuarded(){
JTextComponent c = component;
BaseDocument bdoc = getDocument();
boolean inGuardedBlock = false;
if (bdoc instanceof GuardedDocument){
GuardedDocument gdoc = (GuardedDocument)bdoc;
boolean selectionSpansGuardedSection = false;
for (int i=c.getSelectionStart(); i<c.getSelectionEnd(); i++){
if (gdoc.isPosGuarded(i)){
selectionSpansGuardedSection = true;
break;
}
}
inGuardedBlock = (gdoc.isPosGuarded(c.getCaretPosition()) ||
selectionSpansGuardedSection);
}
return inGuardedBlock;
}
public void run() {
JTextComponent c = component;
if (c != null) {
BaseKit kit = Utilities.getKit(c);
if (kit != null) {
boolean isEditable = c.isEditable();
boolean selectionVisible = c.getCaret().isSelectionVisible();
boolean caretGuarded = isCaretGuarded();
Action a = kit.getActionByName(BaseKit.copyAction);
if (a != null) {
a.setEnabled(selectionVisible);
}
a = kit.getActionByName(BaseKit.cutAction);
if (a != null) {
a.setEnabled(selectionVisible && !caretGuarded && isEditable);
}
a = kit.getActionByName(BaseKit.removeSelectionAction);
if (a != null) {
a.setEnabled(selectionVisible && !caretGuarded && isEditable);
}
a = kit.getActionByName(BaseKit.pasteAction);
if (a != null) {
a.setEnabled(!caretGuarded && isEditable);
}
}
}
}
}
);
}
protected void modelChanged(BaseDocument oldDoc, BaseDocument newDoc) {
if (oldDoc != null) {
// remove all document layers
drawLayerList.remove(oldDoc.getDrawLayerList());
}
if (newDoc != null) {
settingsChange(null);
// add all document layers
drawLayerList.add(newDoc.getDrawLayerList());
}
if (oldDoc != null)
oldDoc.getBookmarks().removeAll();
}
public void propertyChange(PropertyChangeEvent evt) {
String propName = evt.getPropertyName();
if ("document".equals(propName)) {
BaseDocument oldDoc = (evt.getOldValue() instanceof BaseDocument)
? (BaseDocument)evt.getOldValue() : null;
BaseDocument newDoc = (evt.getNewValue() instanceof BaseDocument)
? (BaseDocument)evt.getNewValue() : null;
modelChanged(oldDoc, newDoc);
} else if ("margin".equals(propName)) { // NOI18N
updateTextMargin();
} else if ("caret".equals(propName)) { // NOI18N
if (evt.getOldValue() instanceof Caret) {
((Caret)evt.getOldValue()).removeChangeListener(this);
}
if (evt.getNewValue() instanceof Caret) {
((Caret)evt.getNewValue()).addChangeListener(this);
}
} else if ("enabled".equals(propName)) { // NOI18N
if (!component.isEnabled()) {
component.getCaret().setVisible(false);
}
}
}
protected Map createColoringMap() {
Map cm;
if (component != null) {
// Use the shared coloring-map to save space and time
cm = getSharedColoringMap(getKitClass());
} else { // print coloring-map must be created
cm = SettingsUtil.getColoringMap(getKitClass(), (component == null), true);
// Test if there's a default coloring
if (cm.get(SettingsNames.DEFAULT_COLORING) == null) {
cm.put(SettingsNames.DEFAULT_COLORING, SettingsDefaults.defaultColoring);
}
}
return cm;
}
public int getLineHeight() {
return lineHeight;
}
public int getLineAscent() {
return lineAscent;
}
public Map getColoringMap() {
if (coloringMap == null) {
coloringMap = createColoringMap();
}
return coloringMap;
}
public Coloring getDefaultColoring() {
return (Coloring)getColoringMap().get(SettingsNames.DEFAULT_COLORING);
}
public Coloring getColoring(String coloringName) {
return (Coloring)getColoringMap().get(coloringName);
}
private void updateLineHeight(Graphics g) {
if (debugUpdateLineHeight) {
System.err.println("EditorUI.updateLineHeight(): Computing lineHeight ...");
}
Map cm = getColoringMap();
Iterator i = cm.entrySet().iterator();
int maxHeight = 1;
int maxAscent = 0;
while (i.hasNext()) {
Map.Entry me = (Map.Entry)i.next();
String coloringName = (String)me.getKey();
Coloring c = (Coloring)me.getValue();
if (c != null) {
Font font = c.getFont();
if (font != null && (c.getFontMode() & Coloring.FONT_MODE_APPLY_SIZE) != 0) {
FontMetrics fm = g.getFontMetrics(font);
if (fm != null) {
if (debugUpdateLineHeight) {
if (maxHeight < fm.getHeight()) {
System.err.println("Updating maxHeight from "
+ maxHeight + " to " + fm.getHeight()
+ ", coloringName=" + coloringName
+ ", font=" + font
);
}
if (maxHeight < fm.getHeight()) {
System.err.println("Updating maxAscent from "
+ maxAscent + " to " + fm.getAscent()
+ ", coloringName=" + coloringName
+ ", font=" + font
);
}
}
maxHeight = Math.max(maxHeight, fm.getHeight());
maxAscent = Math.max(maxAscent, fm.getAscent());
}
}
}
}
// Apply lineHeightCorrection
lineHeight = (int)(maxHeight * lineHeightCorrection);
lineAscent = (int)(maxAscent * lineHeightCorrection);
}
/** Return whether the fonts are already initialized or not.
*/
boolean isFontsInited() {
return fontsInited;
}
protected void update(Graphics g) {
Class kitClass = Utilities.getKitClass(component);
// Set the margin
if (kitClass != null) {
Object value = Settings.getValue(kitClass, SettingsNames.MARGIN);
Insets margin = (value instanceof Insets) ? (Insets)value : null;
component.setMargin(margin);
}
// Apply the default coloring to the component
// getDefaultColoring().apply(component);
// Possibly apply the rendering hints
if (renderingHints != null) {
((Graphics2D)g).setRenderingHints(renderingHints);
}
Coloring dc = getDefaultColoring();
// Handle line number fonts and widths
Coloring lnc = (Coloring)getColoringMap().get(SettingsNames.LINE_NUMBER_COLORING);
if (lnc != null) {
Font lnFont = lnc.getFont();
if (lnFont == null) {
lnFont = dc.getFont();
}
FontMetrics lnFM = g.getFontMetrics(lnFont);
int maxWidth = 1;
char[] digit = new char[1]; // will be used for '0' - '9'
for (int i = 0; i <= 9; i++) {
digit[0] = (char)('0' + i);
maxWidth = Math.max(maxWidth, lnFM.charsWidth(digit, 0, 1));
}
lineNumberDigitWidth = maxWidth;
}
// Update line height
updateLineHeight(g);
// Update space width of the default coloring's font
FontMetricsCache.Info fmcInfo = FontMetricsCache.getInfo(getDefaultColoring().getFont());
defaultSpaceWidth = fmcInfo.getSpaceWidth(g);
// Update total height
if (component != null) {
((BaseTextUI)component.getUI()).updateHeight();
updateLineNumberWidth(0);
checkLineLimit();
}
/* JDK1.3 patch for the behavior that occurs when the line is wider
* than the screen and the user first clicks End key to go to the end
* and then goes back by (Ctrl+)Left. As the non-simple scrolling mode
* is used in JViewport in 1.3 the line number block appears shifted
* to the right and gets repainted after 300ms which looks ugly.
* The patch is to set the simple scrolling mode into JViewport.
*
* getParentViewport().setScrollMode(0); // 2 stands for SIMPLE_SCROLL_MODE
*
*/
try {
JViewport vp = getParentViewport();
if (vp != null) {
java.lang.reflect.Method setScrollModeMethod = JViewport.class.getDeclaredMethod(
"setScrollMode", new Class[] { Integer.TYPE }); // NOI18N
setScrollModeMethod.invoke(vp, new Object[] { new Integer(0) });
}
} catch (Throwable t) {
}
// update glyph gutter colors and fonts
if (isGlyphGutterVisible()) {
glyphGutter.update();
updateScrollPaneCornerColor();
}
// FIx of #14295
updateVirtualHeight(0);
/* Fix of #8123 - the caret is physically set to the end of the file
* but the window is not scrolled there.
* The problem is that the caret has the right position
* but the editor pane has not yet the right size. Although
* at such time the TextUI.preferenceChanged() was already
* called, the request for revalidation is waiting
* in the queue to be done.
* The fix adds a flag that determines whether the fonts
* were reinited already but there is a pending preferenceChanged()
* request that was not finished yet. Once it's finished
* the fontsInited flag is set.
* The other part of the fix is in BaseCaret.
*/
fontsInitedPreferenceChanged = true;
if (component != null) {
// revalidate the component
((BaseTextUI)component.getUI()).preferenceChanged(true, true);
}
}
public final JTextComponent getComponent() {
return component;
}
/** Get the document to work on. Either component's document or printed document
* is returned. It can return null in case the component's document is not instance
* of BaseDocument.
*/
public final BaseDocument getDocument() {
return (component != null) ? Utilities.getDocument(component) : printDoc;
}
private Class getKitClass() {
return (component != null) ? Utilities.getKitClass(component)
: ((printDoc != null) ? printDoc.getKitClass() : null);
}
public Object getProperty(Object key) {
return props.get(key);
}
public void putProperty(Object key, Object value) {
Object oldValue;
if (value != null) {
oldValue = props.put(key, value);
} else {
oldValue = props.remove(key);
}
firePropertyChange(key.toString(), oldValue, value);
}
/** Get extended editor component.
* The extended component should normally be used
* for editing files instead of just the JEditorPane
* because it offers status bar and possibly
* other useful components.
* The getExtComponent() should not be used when
* the JEditorPane is included in dialog.
* @see #hasExtComponent()
*/
public JComponent getExtComponent() {
if (extComponent == null) {
if (component != null) {
extComponent = createExtComponent();
}
}
return extComponent;
}
protected JComponent createExtComponent() {
setLineNumberEnabled(true); // enable line numbering
// extComponent will be a panel
JComponent ec = new JPanel(new BorderLayout());
ec.putClientProperty(JTextComponent.class, component);
// Add the scroll-pane with the component to the center
JScrollPane scroller = new JScrollPane(component);
scroller.getViewport().setMinimumSize(new Dimension(4,4));
// glyph gutter must be created here
glyphGutter = new GlyphGutter(this);
scroller.setRowHeaderView(glyphGutter);
glyphCorner = new JPanel();
updateScrollPaneCornerColor();
scroller.setCorner(JScrollPane.LOWER_LEFT_CORNER, glyphCorner);
ec.add(scroller);
// Install the status-bar panel to the bottom
ec.add(getStatusBar().getPanel(), BorderLayout.SOUTH);
return ec;
}
/** Whether this ui uses extComponent or not.
* @see #getExtComponent()
*/
public boolean hasExtComponent() {
return (extComponent != null);
}
public Abbrev getAbbrev() {
if (abbrev == null) {
abbrev = new Abbrev(this, true, true);
}
return abbrev;
}
public WordMatch getWordMatch() {
if (wordMatch == null) {
wordMatch = new WordMatch(this);
}
return wordMatch;
}
public StatusBar getStatusBar() {
if (statusBar == null) {
statusBar = new StatusBar(this);
}
return statusBar;
}
final DrawLayerList getDrawLayerList() {
return drawLayerList;
}
/** Find the layer with some layer name in the layer hierarchy */
public DrawLayer findLayer(String layerName) {
return drawLayerList.findLayer(layerName);
}
/** Add new layer and use its priority to position it in the chain.
* If there's the layer with same visibility then the inserted layer
* will be placed after it.
*
* @param layer layer to insert into the chain
*/
public boolean addLayer(DrawLayer layer, int visibility) {
return drawLayerList.add(layer, visibility);
}
public DrawLayer removeLayer(String layerName) {
return drawLayerList.remove(layerName);
}
public void repaint(int startY) {
repaint(startY, component.getHeight());
}
public void repaint(int startY, int height) {
if (height <= 0) {
return;
}
int width = Math.max(component.getWidth(), 0);
startY = Math.max(startY, 0);
component.repaint(0, startY, width, height);
}
public void repaintOffset(int pos) throws BadLocationException {
repaintBlock(pos, pos);
}
/** Repaint the block between the given positions. */
public void repaintBlock(int startPos, int endPos)
throws BadLocationException {
BaseTextUI ui = (BaseTextUI)component.getUI();
if (startPos > endPos) { // swap
int tmpPos = startPos;
startPos = endPos;
endPos = tmpPos;
}
try {
int yFrom = ui.getYFromPos(startPos);
int yTo = ui.getYFromPos(endPos);
repaint(yFrom, (yTo - yFrom) + lineHeight);
} catch (BadLocationException e) {
if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N
e.printStackTrace();
}
}
}
/** Is the parent of some editor component a viewport */
private JViewport getParentViewport() {
Component pc = component.getParent();
return (pc instanceof JViewport) ? (JViewport)pc : null;
}
/** Finds the frame - parent of editor component */
public static Frame getParentFrame(Component c) {
do {
c = c.getParent();
if (c instanceof Frame) {
return (Frame)c;
}
} while (c != null);
return null;
}
/** Possibly update virtual width. If the width
* is really updated, the method returns true.
*/
public boolean updateVirtualWidth(int width) {
boolean updated = false;
if (width > virtualSize.width) {
int widthInc = componentSizeIncrement.width;
widthInc = (widthInc < 0) ? (lastExtentBounds.width * (-widthInc) / 100)
: widthInc * defaultSpaceWidth;
virtualSize.width = width + widthInc;
virtualSizeUpdated = true;
updated = true;
}
return updated;
}
/** Possibly update virtual height. If the height
* is really updated, the method returns true. There is
* a slight difference against virtual width in that
* if the height is shrinked too much the virtual height
* is shrinked too.
* 0 can be used to update to the real height.
*/
public boolean updateVirtualHeight(int height) {
boolean updated = false;
updateLineNumberWidth(0);
// changed to fix #18648
if (height <= 0) { //compute real height - fix of #14295
height = (int)((TextUI)component.getUI()).getRootView(component).getPreferredSpan(View.Y_AXIS);
}
if (height != virtualSize.height) {
virtualSize.height = height;
virtualSizeUpdated = true;
updated = true;
}
return updated;
}
public boolean isLineNumberEnabled() {
return lineNumberEnabled;
}
public void setLineNumberEnabled(boolean lineNumberEnabled) {
this.lineNumberEnabled = lineNumberEnabled;
lineNumberVisible = lineNumberEnabled && lineNumberVisibleSetting;
if (disableLineNumbers)
lineNumberVisible = false;
}
/** Update the width that will be occupied by the line number.
* @param maxDigitCount maximum digit count that can the line number have.
* if it's lower or equal to zero it will be computed automatically.
*/
public void updateLineNumberWidth(int maxDigitCount) {
int oldWidth = lineNumberWidth;
if (lineNumberVisible) {
try {
if (maxDigitCount <= 0) {
BaseDocument doc = getDocument();
int lineCnt = Utilities.getLineOffset(doc, doc.getLength()) + 1;
maxDigitCount = Integer.toString(lineCnt).length();
}
if (maxDigitCount > lineNumberMaxDigitCount) {
lineNumberMaxDigitCount = maxDigitCount;
}
} catch (BadLocationException e) {
lineNumberMaxDigitCount = 1;
}
lineNumberWidth = lineNumberMaxDigitCount * lineNumberDigitWidth;
if (lineNumberMargin != null) {
lineNumberWidth += lineNumberMargin.left + lineNumberMargin.right;
}
} else {
lineNumberWidth = 0;
}
updateTextMargin();
if (oldWidth != lineNumberWidth) { // changed
if (component != null) {
component.repaint();
}
}
}
void checkLineLimit() {
BaseDocument doc = getDocument();
if (doc != null) {
Integer lineLimit = (Integer)doc.getProperty(BaseDocument.LINE_LIMIT_PROP);
if (lineLimit != null) {
if (component != null) {
// Not using FM cache - could be called too early
FontMetrics fm = component.getFontMetrics(getDefaultColoring().getFont());
if (fm != null) {
int charWidth = fm.stringWidth("A");
updateVirtualWidth(charWidth * lineLimit.intValue() + lineNumberWidth);
}
}
}
}
}
public void updateTextMargin() {
if (!SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(
new Runnable() {
public void run() {
updateTextMargin();
}
}
);
}
Insets orig = textMargin;
Insets cm = (component != null) ? component.getMargin() : null;
int leftWidth = lineNumberWidth + textLeftMarginWidth;
if (cm != null) {
textMargin = new Insets(cm.top, cm.left + leftWidth,
cm.bottom, cm.right);
} else {
textMargin = new Insets(0, leftWidth, 0, 0);
}
if (orig.top != textMargin.top || orig.bottom != textMargin.bottom) {
((BaseTextUI)component.getUI()).invalidateStartY();
}
}
public Rectangle getExtentBounds() {
return getExtentBounds(null);
}
/** Get position of the component extent. The (x, y) are set to (0, 0) if there's
* no viewport or (-x, -y) if there's one.
*/
public Rectangle getExtentBounds(Rectangle r) {
if (r == null) {
r = new Rectangle();
}
if (component != null) {
JViewport port = getParentViewport();
if (port != null) {
Point p = port.getViewPosition();
r.width = port.getWidth();
r.height = port.getHeight();
r.x = p.x;
r.y = p.y;
} else { // no viewport
r.setBounds(component.getVisibleRect());
}
}
return r;
}
/** Get the begining of the area covered by text */
public Insets getTextMargin() {
return textMargin;
}
public void scrollRectToVisible(final Rectangle r, final int scrollPolicy) {
Utilities.runInEventDispatchThread(
new Runnable() {
public void run() {
scrollRectToVisibleFragile(r, scrollPolicy);
}
}
);
}
/** Must be called with EventDispatchThread */
boolean scrollRectToVisibleFragile(Rectangle r, int scrollPolicy) {
Insets margin = getTextMargin();
Rectangle bounds = getExtentBounds();
r = new Rectangle(r); // make copy of orig rect
r.x -= margin.left;
r.y -= margin.top;
bounds.width -= margin.left + margin.right;
bounds.height -= margin.top + margin.bottom;
return scrollRectToVisibleImpl(r, scrollPolicy, bounds);
}
/** Scroll the view so that requested rectangle is best visible.
* There are different scroll policies available.
* @return whether the extent has to be scrolled in any direction.
*/
private boolean scrollRectToVisibleImpl(Rectangle r, int scrollPolicy,
Rectangle bounds) {
if (bounds.width <= 0 || bounds.height <= 0) {
return false;
}
// handle find scrolling specifically
if (scrollPolicy == SCROLL_FIND) {
// converted inset
int cnvFI = (scrollFindInsets.left < 0)
? (- bounds.width * scrollFindInsets.left / 100)
: scrollFindInsets.left * defaultSpaceWidth;
int nx = Math.max(r.x - cnvFI, 0);
cnvFI = (scrollFindInsets.right < 0)
? (- bounds.width * scrollFindInsets.right / 100)
: scrollFindInsets.right * defaultSpaceWidth;
r.width += (r.x - nx) + cnvFI;
r.x = nx;
cnvFI = (scrollFindInsets.top < 0)
? (- bounds.height * scrollFindInsets.top / 100)
: scrollFindInsets.top * lineHeight;
int ny = Math.max(r.y - cnvFI, 0);
cnvFI = (scrollFindInsets.bottom < 0)
? (- bounds.height * scrollFindInsets.bottom / 100)
: scrollFindInsets.bottom * lineHeight;
r.height += (r.y - ny) + cnvFI;
r.y = ny;
return scrollRectToVisibleImpl(r, SCROLL_SMALLEST, bounds); // recall
}
// r must be within virtualSize's width
if (r.x + r.width > virtualSize.width) {
r.x = virtualSize.width - r.width;
if (r.x < 0) {
r.x = 0;
r.width = virtualSize.width;
}
return scrollRectToVisibleImpl(r, scrollPolicy, bounds); // recall
}
// r must be within virtualSize's height
if (r.y + r.height > virtualSize.height) {
r.y = virtualSize.height - r.height;
if (r.y < 0) {
r.y = 0;
r.height = virtualSize.height;
}
return scrollRectToVisibleImpl(r, scrollPolicy, bounds);
}
// if r extends bounds dimension it must be corrected now
if (r.width > bounds.width || r.height > bounds.height) {
Rectangle caretRect = new Rectangle((Rectangle)component.getCaret());
if (caretRect.x >= r.x
&& caretRect.x + caretRect.width <= r.x + r.width
&& caretRect.y >= r.y
&& caretRect.y + caretRect.height <= r.y + r.height
) { // caret inside requested rect
// move scroll rect for best caret visibility
int overX = r.width - bounds.width;
int overY = r.height - bounds.height;
if (overX > 0) {
r.x -= overX * (caretRect.x - r.x) / r.width;
}
if (overY > 0) {
r.y -= overY * (caretRect.y - r.y) / r.height;
}
}
r.height = bounds.height;
r.width = bounds.width; // could be different algorithm
return scrollRectToVisibleImpl(r, scrollPolicy, bounds);
}
int newX = bounds.x;
int newY = bounds.y;
boolean move = false;
// now the scroll rect is within bounds of the component
// and can have size of the extent at maximum
if (r.x < bounds.x) {
move = true;
switch (scrollPolicy) {
case SCROLL_MOVE:
newX = (scrollJumpInsets.left < 0)
? (bounds.width * (-scrollJumpInsets.left) / 100)
: scrollJumpInsets.left * defaultSpaceWidth;
newX = Math.min(newX, bounds.x + bounds.width - (r.x + r.width));
newX = Math.max(r.x - newX, 0); // new bounds.x
break;
case SCROLL_DEFAULT:
case SCROLL_SMALLEST:
default:
newX = r.x;
break;
}
updateVirtualWidth(newX + bounds.width);
} else if (r.x + r.width > bounds.x + bounds.width) {
move = true;
switch (scrollPolicy) {
case SCROLL_SMALLEST:
newX = r.x + r.width - bounds.width;
break;
default:
newX = (scrollJumpInsets.right < 0)
? (bounds.width * (-scrollJumpInsets.right) / 100 )
: scrollJumpInsets.right * defaultSpaceWidth;
newX = Math.min(newX, bounds.width - r.width);
newX = (r.x + r.width) + newX - bounds.width;
break;
}
updateVirtualWidth(newX + bounds.width);
}
if (r.y < bounds.y) {
move = true;
switch (scrollPolicy) {
case SCROLL_MOVE:
newY = r.y;
newY -= (scrollJumpInsets.top < 0)
? (bounds.height * (-scrollJumpInsets.top) / 100 )
: scrollJumpInsets.top * lineHeight;
break;
case SCROLL_SMALLEST:
newY = r.y;
break;
case SCROLL_DEFAULT:
default:
newY = r.y - (bounds.height - r.height) / 2; // center
break;
}
newY = Math.max(newY, 0);
} else if (r.y + r.height > bounds.y + bounds.height) {
move = true;
switch (scrollPolicy) {
case SCROLL_MOVE:
newY = (r.y + r.height) - bounds.height;
newY += (scrollJumpInsets.bottom < 0)
? (bounds.height * (-scrollJumpInsets.bottom) / 100 )
: scrollJumpInsets.bottom * lineHeight;
break;
case SCROLL_SMALLEST:
newY = (r.y + r.height) - bounds.height;
break;
case SCROLL_DEFAULT:
default:
newY = r.y - (bounds.height - r.height) / 2; // center
break;
}
newY = Math.max(newY, 0);
}
if (move) {
setExtentPosition(newX, newY);
}
return move;
}
void setExtentPosition(int x, int y) {
JViewport port = getParentViewport();
if (port != null) {
Point p = new Point(Math.max(x, 0), Math.max(y, 0));
port.setViewPosition(p);
}
}
public void adjustWindow(int caretPercentFromWindowTop) {
final Rectangle bounds = getExtentBounds();
if (component != null && (component.getCaret() instanceof Rectangle)) {
Rectangle caretRect = (Rectangle)component.getCaret();
bounds.y = caretRect.y - (caretPercentFromWindowTop * bounds.height) / 100
+ (caretPercentFromWindowTop * lineHeight) / 100;
Utilities.runInEventDispatchThread(
new Runnable() {
public void run() {
scrollRectToVisible(bounds, SCROLL_SMALLEST);
}
}
);
}
}
/** Set the dot according to the currently visible screen window.
* #param percentFromWindowTop percentage giving the distance of the caret
* from the top of the currently visible window.
*/
public void adjustCaret(int percentFromWindowTop) {
JTextComponent c = component;
if (c != null) {
Rectangle bounds = getExtentBounds();
bounds.y += (percentFromWindowTop * bounds.height) / 100
- (percentFromWindowTop * lineHeight) / 100;
try {
int offset = ((BaseTextUI)c.getUI()).getPosFromY(bounds.y);
if (offset >= 0) {
caretSetDot(offset, null, SCROLL_SMALLEST);
}
} catch (BadLocationException e) {
}
}
}
/** Set the position of the caret and scroll the extent if necessary.
* @param offset position where the caret should be placed
* @param scrollRect rectangle that should become visible. It can be null
* when no scrolling should be done.
* @param scrollPolicy policy to be used when scrolling.
* @deprecated
*/
public void caretSetDot(int offset, Rectangle scrollRect, int scrollPolicy) {
if (component != null) {
Caret caret = component.getCaret();
if (caret instanceof BaseCaret) {
((BaseCaret)caret).setDot(offset, scrollRect, scrollPolicy);
} else {
caret.setDot(offset);
}
}
}
/** Set the position of the caret and scroll the extent if necessary.
* @param offset position where the caret should be placed
* @param scrollRect rectangle that should become visible. It can be null
* when no scrolling should be done.
* @param scrollPolicy policy to be used when scrolling.
* @deprecated
*/
public void caretMoveDot(int offset, Rectangle scrollRect, int scrollPolicy) {
if (component != null) {
Caret caret = component.getCaret();
if (caret instanceof BaseCaret) {
((BaseCaret)caret).moveDot(offset, scrollRect, scrollPolicy);
} else {
caret.moveDot(offset);
}
}
}
/** This method is called by textui to do the paint.
* It is forwarded either to paint through the image
* and then copy the image area to the screen or to
* paint directly to this graphics. The real work occurs
* in draw-engine.
*/
protected void paint(Graphics g) {
if (component != null) { // component must be installed
if (fontsInitedPreferenceChanged) {
fontsInitedPreferenceChanged = false;
fontsInited = true;
getExtentBounds(lastExtentBounds);
}
if (!fontsInited && g != null) {
update(g);
}
((BaseTextUI)component.getUI()).paintRegion(g);
}
}
/** Returns the line number margin */
public Insets getLineNumberMargin() {
return lineNumberMargin;
}
/** Returns width of the one digit */
public int getLineNumberDigitWidth() {
return lineNumberDigitWidth;
}
/** Is glyph gutter created and visible for the document or not */
public boolean isGlyphGutterVisible() {
return glyphGutter != null;
}
public GlyphGutter getGlyphGutter() {
return glyphGutter;
}
protected void updateScrollPaneCornerColor() {
Coloring lineColoring = (Coloring)getColoringMap().get(SettingsNames.LINE_NUMBER_COLORING);
Coloring defaultColoring = (Coloring)getDefaultColoring();
Color backgroundColor;
if (lineColoring.getBackColor() != null)
backgroundColor = lineColoring.getBackColor();
else
backgroundColor = defaultColoring.getBackColor();
glyphCorner.setBackground(backgroundColor);
}
}