/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.editor.impl;
import com.intellij.ide.CutProvider;
import com.intellij.ide.DataManager;
import com.intellij.ide.IdeEventQueue;
import com.intellij.ide.PasteProvider;
import com.intellij.ide.ui.UISettings;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.impl.ApplicationImpl;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.command.UndoConfirmationPolicy;
import com.intellij.openapi.editor.Caret;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.actionSystem.EditorActionManager;
import com.intellij.openapi.editor.actions.EditorActionUtil;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.event.CaretEvent;
import com.intellij.openapi.editor.event.CaretListener;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.editor.ex.util.EditorUIUtil;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Queryable;
import com.intellij.openapi.ui.TypingTarget;
import com.intellij.openapi.util.*;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.Grayer;
import com.intellij.ui.components.Magnificator;
import com.intellij.util.ui.JBSwingUtilities;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.accessibility.ScreenReader;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.accessibility.*;
import javax.swing.*;
import javax.swing.event.ChangeListener;
import javax.swing.event.UndoableEditListener;
import javax.swing.plaf.TextUI;
import javax.swing.text.*;
import javax.swing.text.Segment;
import java.awt.*;
import java.awt.event.*;
import java.awt.im.InputMethodRequests;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class EditorComponentImpl extends JTextComponent implements Scrollable, DataProvider, Queryable, TypingTarget, Accessible {
private final EditorImpl myEditor;
private final ApplicationImpl myApplication;
public EditorComponentImpl(@NotNull EditorImpl editor) {
myEditor = editor;
enableEvents(AWTEvent.KEY_EVENT_MASK | AWTEvent.INPUT_METHOD_EVENT_MASK);
enableInputMethods(true);
// Note: Ideally, we should always set "FocusCycleRoot" to "false", but,
// in the interest of backward compatibility, we only do so when a
// screen reader is active.
setFocusCycleRoot(!ScreenReader.isActive());
setOpaque(true);
putClientProperty(Magnificator.CLIENT_PROPERTY_KEY, new Magnificator() {
@Override
public Point magnify(double scale, Point at) {
if (myEditor.isDisposed()) return at;
VisualPosition magnificationPosition = myEditor.xyToVisualPosition(at);
double currentSize = myEditor.getColorsScheme().getEditorFontSize();
int defaultFontSize = EditorColorsManager.getInstance().getGlobalScheme().getEditorFontSize();
myEditor.setFontSize(Math.max((int)(currentSize * scale), defaultFontSize));
return myEditor.visualPositionToXY(magnificationPosition);
}
});
myApplication = (ApplicationImpl)ApplicationManager.getApplication();
// This editor extends JTextComponent rather than JComponent *only* for accessibility
// purposes, and the JTextComponent is not fully supported: it does not reflect the
// true contents of the document, it doesn't paint, it doesn't have a proper UI delegate,
// etc. It simply extends JTextComponent and accepts document and caret listeners (and
// dispatches editing events to them) because on some platforms, the accessibility
// support is hardcoded to only work for JTextComponent rather than AccessibleText in
// general.
setupJTextComponentContext();
// Remove JTextComponent's mouse/focus listeners added in its ctor.
for (MouseListener l : getMouseListeners()) removeMouseListener(l);
for (FocusListener l : getFocusListeners()) removeFocusListener(l);
}
@Override
public void paint(@NotNull Graphics g) {
if (!isEnabled()) {
g = new Grayer((Graphics2D)g, getBackground());
}
super.paint(g);
}
@NotNull
public EditorImpl getEditor() {
return myEditor;
}
@Override
public Object getData(String dataId) {
if (myEditor.isDisposed() || myEditor.isRendererMode()) return null;
if (CommonDataKeys.EDITOR.is(dataId)) {
return myEditor;
}
if (CommonDataKeys.CARET.is(dataId)) {
return myEditor.getCaretModel().getCurrentCaret();
}
if (PlatformDataKeys.DELETE_ELEMENT_PROVIDER.is(dataId)) {
return myEditor.getDeleteProvider();
}
if (PlatformDataKeys.CUT_PROVIDER.is(dataId)) {
return myEditor.getCutProvider();
}
if (PlatformDataKeys.COPY_PROVIDER.is(dataId)) {
return myEditor.getCopyProvider();
}
if (PlatformDataKeys.PASTE_PROVIDER.is(dataId)) {
return myEditor.getPasteProvider();
}
if (CommonDataKeys.EDITOR_VIRTUAL_SPACE.is(dataId)) {
LogicalPosition location = myEditor.myLastMousePressedLocation;
if (location == null) {
location = myEditor.getCaretModel().getLogicalPosition();
}
return EditorUtil.inVirtualSpace(myEditor, location);
}
return null;
}
@Override
public Color getBackground() {
return myEditor.getBackgroundColor();
}
@Override
public Dimension getPreferredSize() {
return myEditor.getPreferredSize();
}
protected void fireResized() {
processComponentEvent(new ComponentEvent(this, ComponentEvent.COMPONENT_RESIZED));
}
@Override
protected void processInputMethodEvent(InputMethodEvent e) {
// Don't dispatch to super first; now that EditorComponentImpl is a JTextComponent,
// this would have the side effect of invoking Swing document machinery which relies
// on creating Document positions etc (and won't update the document in an IntelliJ safe
// way, such as running through all the carets etc.
// super.processInputMethodEvent(e);
if (!e.isConsumed()) {
switch (e.getID()) {
case InputMethodEvent.INPUT_METHOD_TEXT_CHANGED:
myEditor.replaceInputMethodText(e);
// No breaks over here.
//noinspection fallthrough
case InputMethodEvent.CARET_POSITION_CHANGED:
myEditor.inputMethodCaretPositionChanged(e);
e.consume();
break;
}
}
super.processInputMethodEvent(e);
}
@Override
public ActionCallback type(final String text) {
final ActionCallback result = new ActionCallback();
UIUtil.invokeLaterIfNeeded(() -> myEditor.type(text).notify(result));
return result;
}
@Nullable
@Override
public InputMethodRequests getInputMethodRequests() {
return IdeEventQueue.getInstance().isInputMethodEnabled() ? myEditor.getInputMethodRequests() : null;
}
@Override
protected Graphics getComponentGraphics(Graphics graphics) {
return JBSwingUtilities.runGlobalCGTransform(this, super.getComponentGraphics(graphics));
}
@Override
public void paintComponent(Graphics g) {
myApplication.editorPaintStart();
try {
Graphics2D gg = (Graphics2D)g;
UIUtil.setupComposite(gg);
if (myEditor.useEditorAntialiasing()) {
EditorUIUtil.setupAntialiasing(gg);
}
else {
UISettings.setupAntialiasing(gg);
}
myEditor.paint(gg);
}
finally {
myApplication.editorPaintFinish();
}
}
public void repaintEditorComponent() {
repaint();
}
public void repaintEditorComponent(int x, int y, int width, int height) {
int topOverhang = Math.max(0, myEditor.myView.getTopOverhang());
int bottomOverhang = Math.max(0, myEditor.myView.getBottomOverhang());
repaint(x, y - topOverhang, width, height + topOverhang + bottomOverhang);
}
//--implementation of Scrollable interface--------------------------------------
@Override
public Dimension getPreferredScrollableViewportSize() {
return myEditor.getPreferredSize();
}
@Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
if (orientation == SwingConstants.VERTICAL) {
return myEditor.getLineHeight();
}
// if orientation == SwingConstants.HORIZONTAL
return EditorUtil.getSpaceWidth(Font.PLAIN, myEditor);
}
@Override
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
if (orientation == SwingConstants.VERTICAL) {
int lineHeight = myEditor.getLineHeight();
if (direction > 0) {
int lineNumber = (visibleRect.y + visibleRect.height) / lineHeight;
return lineHeight * lineNumber - visibleRect.y;
}
else {
int lineNumber = (visibleRect.y - visibleRect.height) / lineHeight;
return visibleRect.y - lineHeight * lineNumber;
}
}
// if orientation == SwingConstants.HORIZONTAL
return visibleRect.width;
}
@Override
public boolean getScrollableTracksViewportWidth() {
return getParent() instanceof JViewport && getParent().getWidth() > getPreferredSize().width;
}
@Override
public boolean getScrollableTracksViewportHeight() {
return getParent() instanceof JViewport && getParent().getHeight() > getPreferredSize().height;
}
@Override
public void putInfo(@NotNull Map<String, String> info) {
myEditor.putInfo(info);
}
@NonNls
@Override
public String toString() {
return "EditorComponent file=" + myEditor.getVirtualFile();
}
// -----------------------------------------------------------------------------------------------
// Accessibility/screen reader support for the editor
// -----------------------------------------------------------------------------------------------
// Swing supports accessibility via the AccessibleText interface, as well as AccessibleEditableText
// and some other accessibility interfaces. In theory, all we would need to do to support
// accessibility for the IDE editor is to implement these in the accessible context for
// the editor component.
//
// However, it turns out that on some platforms, such as on MacOSX, the accessibility integration
// for Java is hardcoded to only work with JTextComponents! Not only does the code ignore
// property change events announcing caret motion and document changes; it performs specific
// instanceof checks, and the way it adds listeners is to directly add caret and document
// listeners on the JTextComponent. It also performs other JTextComponent operations such
// as looking up the TextUI delegate and performing viewToModel calls, it asks for the Swing
// document's root element and uses it to compute line numbers (and line offset ranges),
// etc.
//
// Therefore, in order to support accessibility for the source editor, we have to make the source
// editor actually extend JTextComponent. However, we don't want to use any of the editor functionality,
// so we use very simple stub implementations for:
// - the caret
// - the document
// - the UI delegate
// and we override various JTextComponent to disable their normal operation. This leaves us with
// (1) an accessible component which extends JTextComponent, and which provides a document and
// a caret instance that the accessibility infrastructure will register listeners with
// (2) the ability to translate real IDE editor events (caret motion, editing events) into
// corresponding Swing events and dispatch these to the caret/document listeners
@Override
public AccessibleContext getAccessibleContext() {
if (accessibleContext == null) {
accessibleContext = new AccessibleEditorComponentImpl();
}
return accessibleContext;
}
private void setupJTextComponentContext() {
setDocument(new EditorAccessibilityDocument());
setCaret(new EditorAccessibilityCaret());
}
/**
* We're inheriting method now in order to support accessibility, but you
* should <b>NOT</b> call this method if you have an {@linkplain EditorComponentImpl}
* and you're looking for the real document.
*/
@Deprecated
@Override
public javax.swing.text.Document getDocument() {
return super.getDocument();
}
@Override
public int getCaretPosition() {
return myEditor.getCaretModel().getOffset();
}
@Override
public void updateUI() {
// Don't use the default TextUI, BaseTextUI, which does a lot of unnecessary
// work. We do however need to provide a TextUI implementation since some
// screen reader support code will invoke it
setUI(new EditorAccessibilityTextUI());
UISettings.setupEditorAntialiasing(this);
invalidate();
}
@Override
public String getToolTipText(MouseEvent event) {
// Undo effect of JTextComponent superclass: this is the default JComponent implementation
return this.getToolTipText();
}
/** Redispatch an IDE {@link CaretEvent} to a Swing {@link javax.swing.event.CaretListener} */
private void fireJTextComponentCaretChange(final CaretEvent event) {
javax.swing.event.CaretEvent swingEvent = new javax.swing.event.CaretEvent(this) {
@Override
public int getDot() {
Caret caret = event.getCaret();
if (caret != null) {
return caret.getOffset();
}
return 0;
}
@Override
public int getMark() {
Caret caret = event.getCaret();
if (caret != null) {
return caret.getLeadSelectionOffset();
}
return 0;
}
};
for (javax.swing.event.CaretListener listener : getCaretListeners()) {
listener.caretUpdate(swingEvent);
}
}
/** Redispatch an IDE {@link DocumentEvent} to a Swing {@link javax.swing.event.DocumentListener} */
private void fireJTextComponentDocumentChange(final DocumentEvent event) {
//noinspection deprecation
List<javax.swing.event.DocumentListener> listeners = ((EditorAccessibilityDocument)getDocument()).getListeners();
if (listeners == null) {
return;
}
javax.swing.event.DocumentEvent swingEvent = new javax.swing.event.DocumentEvent() {
@Override
public int getOffset() {
return event.getOffset();
}
@Override
public int getLength() {
return event.getNewLength();
}
@Override
public javax.swing.text.Document getDocument() {
//noinspection deprecation
return EditorComponentImpl.this.getDocument();
}
@Override
public EventType getType() {
return event.getOldLength() == 0 ? EventType.INSERT : event.getNewLength() == 0 ? EventType.REMOVE : EventType.CHANGE;
}
@Nullable
@Override
public ElementChange getChange(Element element) {
return null;
}
};
for (javax.swing.event.DocumentListener listener : listeners) {
javax.swing.event.DocumentEvent.EventType type = swingEvent.getType();
if (type == javax.swing.event.DocumentEvent.EventType.INSERT) {
listener.insertUpdate(swingEvent);
} else if (type == javax.swing.event.DocumentEvent.EventType.REMOVE) {
listener.removeUpdate(swingEvent);
} else if (type == javax.swing.event.DocumentEvent.EventType.CHANGE) {
listener.changedUpdate(swingEvent);
}
}
}
private static void notSupported() {
throw new RuntimeException("Not supported for this text implementation");
}
/** {@linkplain javax.swing.text.PlainDocument} does a lot of work we don't need.
* This exists simply to be able to send editing events to the screen reader. */
@SuppressWarnings("UnnecessaryFullyQualifiedName")
private class EditorAccessibilityDocument implements javax.swing.text.Document, javax.swing.text.Element {
private List<javax.swing.event.DocumentListener> myListeners;
@Nullable
public List<javax.swing.event.DocumentListener> getListeners() {
return myListeners;
}
@Override
public int getLength() {
return myEditor.getDocument().getTextLength();
}
@Override
public void addDocumentListener(javax.swing.event.DocumentListener documentListener) {
if (myListeners == null) {
myListeners = new ArrayList<>(2);
}
myListeners.add(documentListener);
}
@Override
public void removeDocumentListener(javax.swing.event.DocumentListener documentListener) {
if (myListeners != null) {
myListeners.remove(documentListener);
}
}
@Override
public void addUndoableEditListener(UndoableEditListener undoableEditListener) {
}
@Override
public void removeUndoableEditListener(UndoableEditListener undoableEditListener) {
}
@Override
@Nullable
public Object getProperty(Object o) {
return null;
}
@Override
public void putProperty(Object o, Object o1) {
}
@Override
public void remove(final int offset, final int length) throws BadLocationException {
editDocumentSafely(offset, length, null);
}
@Override
public void insertString(final int offset, final String text, AttributeSet attributeSet) throws BadLocationException {
editDocumentSafely(offset, 0, text);
}
@Override
public String getText(final int offset, final int length) throws BadLocationException {
return ApplicationManager.getApplication().runReadAction(new Computable<String>() {
@Override
public String compute() {
return myEditor.getDocument().getText(new TextRange(offset, offset + length));
}
});
}
@Override
public void getText(int offset, int length, Segment segment) throws BadLocationException {
char[] s = getText(offset, length).toCharArray();
segment.array = s;
segment.offset = 0;
segment.count = s.length;
}
@Nullable
@Override
public Position getStartPosition() {
notSupported();
return null;
}
@Nullable
@Override
public Position getEndPosition() {
notSupported();
return null;
}
@Nullable
@Override
public Position createPosition(int i) throws BadLocationException {
notSupported();
return null;
}
@Override
public Element[] getRootElements() {
return new Element[] { this };
}
@Override
public Element getDefaultRootElement() {
return this;
}
@Override
public void render(Runnable runnable) {
ApplicationManager.getApplication().runReadAction(runnable);
}
// ---- Implements Element for the root element ----
//
// This is here because the accessibility code ends up calling some JTextComponent
// methods; in particular, CAccessibleText calls root.getElementIndex(index)
// to map an offset to a line number, and getRangeForLine calls root.getElement(lineIndex)
// to get a range object for a given line, and then getStartOffset() and getEndOffset()
// on the result.
@Override
public javax.swing.text.Document getDocument() {
return this;
}
@Nullable
@Override
public Element getParentElement() {
return null;
}
@Nullable
@Override
public String getName() {
return null;
}
@Nullable
@Override
public AttributeSet getAttributes() {
return null;
}
@Override
public int getStartOffset() {
return 0;
}
@Override
public int getEndOffset() {
return getLength();
}
@Override
public int getElementIndex(int i) {
// For the root element this asks for the index of the offset, which
// means the line number
Document document = myEditor.getDocument();
return document.getLineNumber(i);
}
@Override
public int getElementCount() {
Document document = myEditor.getDocument();
return document.getLineCount();
}
@Override
public Element getElement(final int i) {
return new Element() {
@Override
public javax.swing.text.Document getDocument() {
return EditorAccessibilityDocument.this;
}
@Override
public Element getParentElement() {
return EditorAccessibilityDocument.this;
}
@Nullable
@Override
public String getName() {
return null;
}
@Nullable
@Override
public AttributeSet getAttributes() {
return null;
}
@Override
public int getStartOffset() {
Document document = myEditor.getDocument();
return document.getLineStartOffset(i);
}
@Override
public int getEndOffset() {
Document document = myEditor.getDocument();
return document.getLineEndOffset(i);
}
@Override
public int getElementIndex(int i) {
return 0;
}
@Override
public int getElementCount() {
return 0;
}
@Nullable
@Override
public Element getElement(int i) {
return null;
}
@Override
public boolean isLeaf() {
return true;
}
};
}
@Override
public boolean isLeaf() {
return false;
}
}
@Override
public void setText(String text) {
editDocumentSafely(0, myEditor.getDocument().getTextLength(), text);
}
/** Inserts, removes or replaces the given text at the given offset */
private void editDocumentSafely(final int offset, final int length, @Nullable final String text) {
final Project project = myEditor.getProject();
final Document document = myEditor.getDocument();
if (!FileDocumentManager.getInstance().requestWriting(document, project)) {
return;
}
CommandProcessor.getInstance().executeCommand(project,
() -> ApplicationManager.getApplication().runWriteAction(new DocumentRunnable(document, project) {
@Override
public void run() {
document.startGuardedBlockChecking();
try {
if (text == null) {
// remove
document.deleteString(offset, offset + length);
} else if (length == 0) {
// insert
document.insertString(offset, text);
} else {
document.replaceString(offset, offset + length, text);
}
}
catch (ReadOnlyFragmentModificationException e) {
EditorActionManager.getInstance().getReadonlyFragmentModificationHandler(document).handle(e);
}
finally {
document.stopGuardedBlockChecking();
}
}
}), "", document, UndoConfirmationPolicy.DEFAULT, document);
}
/** {@linkplain DefaultCaret} does a lot of work we don't want (listening
* for focus events etc). This exists simply to be able to send caret events to the screen reader. */
private class EditorAccessibilityCaret implements javax.swing.text.Caret {
@Override
public void install(JTextComponent jTextComponent) {
}
@Override
public void deinstall(JTextComponent jTextComponent) {
}
@Override
public void paint(Graphics graphics) {
}
@Override
public void addChangeListener(ChangeListener changeListener) {
}
@Override
public void removeChangeListener(ChangeListener changeListener) {
}
@Override
public boolean isVisible() {
return true;
}
@Override
public void setVisible(boolean visible) {
}
@Override
public boolean isSelectionVisible() {
return true;
}
@Override
public void setSelectionVisible(boolean visible) {
}
@Override
public void setMagicCaretPosition(Point point) {
}
@Nullable
@Override
public Point getMagicCaretPosition() {
return null;
}
@Override
public void setBlinkRate(int rate) {
}
@Override
public int getBlinkRate() {
return 250;
}
@Override
public int getDot() {
return myEditor.getCaretModel().getOffset();
}
@Override
public int getMark() {
return myEditor.getSelectionModel().getSelectionStart();
}
@Override
public void setDot(int offset) {
if (!myEditor.isDisposed()) {
myEditor.getCaretModel().moveToOffset(offset);
}
}
@Override
public void moveDot(int offset) {
if (!myEditor.isDisposed()) {
myEditor.getCaretModel().moveToOffset(offset);
}
}
}
/**
* Specialized TextUI intended *only* for accessibility usage. Not all the methods are called; only viewToModel, not modelToView.
*/
private class EditorAccessibilityTextUI extends TextUI {
@Nullable
@Override
public Rectangle modelToView(JTextComponent tc, int offset) throws BadLocationException {
LogicalPosition pos = myEditor.offsetToLogicalPosition(offset);
Point point = myEditor.logicalPositionToXY(pos);
FontMetrics fontMetrics = myEditor.getFontMetrics(Font.PLAIN);
char c = myEditor.getDocument().getCharsSequence().subSequence(offset, offset + 1).charAt(0);
return new Rectangle(point.x, point.y, fontMetrics.charWidth(c), fontMetrics.getHeight());
}
@Override
public int viewToModel(JTextComponent tc, Point pt) {
LogicalPosition logicalPosition = myEditor.xyToLogicalPosition(pt);
return myEditor.logicalPositionToOffset(logicalPosition);
}
@Nullable
@Override
public Rectangle modelToView(JTextComponent tc, int pos, Position.Bias ignored) throws BadLocationException {
return modelToView(tc, pos);
}
@Override
public int viewToModel(JTextComponent tc, Point pt, Position.Bias[] ignored) {
return viewToModel(tc, pt);
}
@Override
public int getNextVisualPositionFrom(JTextComponent t, int pos, Position.Bias b,
int direction,
Position.Bias[] biasRet) throws BadLocationException {
notSupported();
return 0;
}
@Override
public void damageRange(JTextComponent t, int p0, int p1) {
myEditor.repaint(p0, p1);
}
@Override
public void damageRange(JTextComponent t, int p0, int p1, Position.Bias ignored1, Position.Bias ignored2) {
damageRange(t, p0, p1);
}
@Nullable
@Override
public EditorKit getEditorKit(JTextComponent t) {
notSupported();
return null;
}
@Nullable
@Override
public View getRootView(JTextComponent t) {
notSupported();
return null;
}
}
private static class TextAccessibleRole extends AccessibleRole {
// Can't use AccessibleRole.TEXT: The screen reader verbally refers to it as a text field
// and doesn't do multi-line iteration. (This is hardcoded into the sun/lwawt/macosx implementation.)
// As you can see from JavaAccessibilityUtilities.m, we should use the exact key "textarea" to get
// proper text area handling.
// Note: This is true for MacOS only. For other platform, we need to return the "regular"
// TEXT role to ensure screen readers behave as expected.
@SuppressWarnings("SpellCheckingInspection")
private static final AccessibleRole TEXT_AREA = new TextAccessibleRole("textarea");
private TextAccessibleRole(String key) {
super(key);
}
}
private class AccessibleEditorComponentImpl extends AccessibleJComponent
implements AccessibleText, AccessibleEditableText, AccessibleExtendedText,
CaretListener, DocumentListener {
public AccessibleEditorComponentImpl() {
if (myEditor.isDisposed()) return;
myEditor.getCaretModel().addCaretListener(this);
myEditor.getDocument().addDocumentListener(this);
Disposer.register(myEditor.getDisposable(), new Disposable() {
@Override
public void dispose() {
myEditor.getCaretModel().removeCaretListener(AccessibleEditorComponentImpl.this);
myEditor.getDocument().removeDocumentListener(AccessibleEditorComponentImpl.this);
}
});
}
// ---- Implements CaretListener ----
private int myCaretPos;
@Override
public void caretPositionChanged(CaretEvent e) {
Caret caret = e.getCaret();
if (caret == null) {
return;
}
int dot = caret.getOffset();
int mark = caret.getLeadSelectionOffset();
if (myCaretPos != dot) {
ApplicationManager.getApplication().assertIsDispatchThread();
firePropertyChange(ACCESSIBLE_CARET_PROPERTY,
new Integer(myCaretPos), new Integer(dot));
if (SystemInfo.isMac) {
// For MacOSX we also need to fire a caret event to anyone listening
// to our Document, since *that* rather than the accessible property
// change is the only way to trigger a speech update
//fireJTextComponentCaretChange(dot, mark);
fireJTextComponentCaretChange(e);
}
myCaretPos = dot;
}
if (mark != dot) {
ApplicationManager.getApplication().assertIsDispatchThread();
firePropertyChange(ACCESSIBLE_SELECTION_PROPERTY, null,
getSelectedText());
}
}
@Override
public void caretAdded(CaretEvent e) {
}
@Override
public void caretRemoved(CaretEvent e) {
}
// ---- Implements DocumentListener ----
@Override
public void beforeDocumentChange(DocumentEvent event) {
}
@Override
public void documentChanged(final DocumentEvent event) {
final Integer pos = event.getOffset();
if (ApplicationManager.getApplication().isDispatchThread()) {
firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, null, pos);
if (SystemInfo.isMac) {
// For MacOSX we also need to fire a JTextComponent event to anyone listening
// to our Document, since *that* rather than the accessible property
// change is the only way to trigger a speech update
fireJTextComponentDocumentChange(event);
}
} else {
ApplicationManager.getApplication().invokeLater(() -> {
firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, null, pos);
fireJTextComponentDocumentChange(event);
});
}
}
// ---- Implements AccessibleContext ----
@Nullable
@Override
public String getAccessibleName() {
if (accessibleName != null) {
return accessibleName;
}
VirtualFile file = myEditor.getVirtualFile();
if (file != null) {
return "Editor for " + file.getName();
}
return "Editor";
}
@Override
public AccessibleRole getAccessibleRole() {
// See comment on TextAccessibleRole class.
if (SystemInfo.isMac) {
return TextAccessibleRole.TEXT_AREA;
} else {
return AccessibleRole.TEXT;
}
}
@Override
public AccessibleText getAccessibleText() {
return this;
}
@Override
public AccessibleEditableText getAccessibleEditableText() {
return this;
}
@Override
public AccessibleStateSet getAccessibleStateSet() {
AccessibleStateSet states = super.getAccessibleStateSet();
if (myEditor.getDocument().isWritable()) {
states.add(AccessibleState.EDITABLE);
}
states.add(AccessibleState.MULTI_LINE);
return states;
}
// ---- Implements AccessibleText ----
@Override
public int getIndexAtPoint(Point point) {
LogicalPosition logicalPosition = myEditor.xyToLogicalPosition(point);
return myEditor.logicalPositionToOffset(logicalPosition);
}
@Override
public Rectangle getCharacterBounds(int offset) {
// Since we report the very end of the document as being 1 character past the document
// length, we need to validate the offset passed back by the screen reader.
if (offset < 0 || offset > myEditor.getDocument().getTextLength() - 1) {
return null;
}
LogicalPosition pos = myEditor.offsetToLogicalPosition(offset);
Point point = myEditor.logicalPositionToXY(pos);
FontMetrics fontMetrics = myEditor.getFontMetrics(Font.PLAIN);
char c = myEditor.getDocument().getCharsSequence().subSequence(offset, offset + 1).charAt(0);
return new Rectangle(point.x, point.y, fontMetrics.charWidth(c), fontMetrics.getHeight());
}
@Override
public int getCharCount() {
return myEditor.getDocument().getTextLength();
}
@Override
public int getCaretPosition() {
return myEditor.getCaretModel().getOffset();
}
@Nullable
@Override
public String getAtIndex(
@MagicConstant(intValues = {
AccessibleText.CHARACTER,
AccessibleText.WORD,
AccessibleText.SENTENCE})
int part,
int index) {
return getTextAtOffset(part, index, HERE);
}
@Nullable
@Override
public String getAfterIndex(
@MagicConstant(intValues = {AccessibleText.CHARACTER, AccessibleText.WORD, AccessibleText.SENTENCE})
int part,
int index) {
return getTextAtOffset(part, index, AFTER);
}
@Nullable
@Override
public String getBeforeIndex(
@MagicConstant(intValues = {AccessibleText.CHARACTER, AccessibleText.WORD, AccessibleText.SENTENCE})
int part,
int index) {
return getTextAtOffset(part, index, BEFORE);
}
@Override
public AttributeSet getCharacterAttribute(int index) {
return new SimpleAttributeSet();
}
@Override
public int getSelectionStart() {
return myEditor.getSelectionModel().getSelectionStart();
}
@Override
public int getSelectionEnd() {
return myEditor.getSelectionModel().getSelectionEnd();
}
@Nullable
@Override
public String getSelectedText() {
return myEditor.getSelectionModel().getSelectedText();
}
// ---- Implements AccessibleEditableText ----
@Override
public void setTextContents(String s) {
setText(s);
}
@Override
public void insertTextAtIndex(int index, String s) {
editDocumentSafely(index, 0, s);
}
@Override
public String getTextRange(int startIndex, int endIndex) {
return myEditor.getDocument().getCharsSequence().subSequence(startIndex, endIndex).toString();
}
@Override
public void delete(int startIndex, int endIndex) {
editDocumentSafely(startIndex, endIndex - startIndex, null);
}
@Override
public void cut(int startIndex, int endIndex) {
myEditor.getSelectionModel().setSelection(startIndex, endIndex);
DataContext dataContext = DataManager.getInstance().getDataContext(EditorComponentImpl.this);
CutProvider cutProvider = myEditor.getCutProvider();
if (cutProvider.isCutEnabled(dataContext)) {
cutProvider.performCut(dataContext);
}
}
@Override
public void paste(int startIndex) {
myEditor.getCaretModel().moveToOffset(startIndex);
DataContext dataContext = DataManager.getInstance().getDataContext(EditorComponentImpl.this);
PasteProvider pasteProvider = myEditor.getPasteProvider();
if (pasteProvider.isPasteEnabled(dataContext)) {
pasteProvider.performPaste(dataContext);
}
}
@Override
public void replaceText(int startIndex, int endIndex, String s) {
editDocumentSafely(startIndex, endIndex, s);
}
@Override
public void selectText(int startIndex, int endIndex) {
myEditor.getSelectionModel().setSelection(startIndex, endIndex);
}
@Override
public void setAttributes(int startIndex, int endIndex, AttributeSet as) {
}
// ---- Implements AccessibleExtendedText ----
/** Looking for text before the given offset */
private static final int BEFORE = -1;
/** Looking for text at the given offset */
private static final int HERE = 0;
/** Looking for text after the given offset */
private static final int AFTER = 1;
@Nullable
@Override
public AccessibleTextSequence getTextSequenceAt(
@MagicConstant(intValues = {
AccessibleText.CHARACTER,
AccessibleText.WORD,
AccessibleText.SENTENCE,
AccessibleExtendedText.LINE,
AccessibleExtendedText.ATTRIBUTE_RUN})
int part,
int index) {
return getSequenceAtIndex(part, index, HERE);
}
@Nullable
@Override
public AccessibleTextSequence getTextSequenceAfter(
@MagicConstant(intValues = {
AccessibleText.CHARACTER,
AccessibleText.WORD,
AccessibleText.SENTENCE,
AccessibleExtendedText.LINE,
AccessibleExtendedText.ATTRIBUTE_RUN})
int part,
int index) {
return getSequenceAtIndex(part, index, AFTER);
}
@Nullable
@Override
public AccessibleTextSequence getTextSequenceBefore(
@MagicConstant(intValues = {
AccessibleText.CHARACTER,
AccessibleText.WORD,
AccessibleText.SENTENCE,
AccessibleExtendedText.LINE,
AccessibleExtendedText.ATTRIBUTE_RUN})
int part,
int index) {
return getSequenceAtIndex(part, index, BEFORE);
}
@Override
@Nullable
public Rectangle getTextBounds(int startIndex, int endIndex) {
LogicalPosition startPos = myEditor.offsetToLogicalPosition(startIndex);
Point startPoint = myEditor.logicalPositionToXY(startPos);
Rectangle rectangle = new Rectangle(startPoint);
LogicalPosition endPos = myEditor.offsetToLogicalPosition(endIndex);
Point endPoint = myEditor.logicalPositionToXY(endPos);
FontMetrics fontMetrics = myEditor.getFontMetrics(Font.PLAIN);
char c = myEditor.getDocument().getCharsSequence().subSequence(endIndex - 1, endIndex).charAt(0);
endPoint.x += fontMetrics.charWidth(c);
endPoint.y += fontMetrics.getHeight();
rectangle.add(endPoint);
return rectangle;
}
@Nullable
private String getTextAtOffset(
@MagicConstant(intValues = {AccessibleText.CHARACTER, AccessibleText.WORD, AccessibleText.SENTENCE})
int type,
int offset,
@MagicConstant(intValues = {BEFORE, HERE, AFTER})
int direction) {
DocumentEx document = myEditor.getDocument();
if (offset < 0 || offset >= document.getTextLength()) {
return null;
}
switch (type) {
case AccessibleText.CHARACTER: {
if (offset + direction < document.getTextLength() && offset + direction >= 0) {
int startOffset = offset + direction;
return document.getCharsSequence().subSequence(startOffset, startOffset + 1).toString();
}
break;
}
case AccessibleText.WORD: {
int wordStart = getWordAtOffsetStart(offset, direction);
int wordEnd = getWordAtOffsetEnd(offset, direction);
if (wordStart == -1 || wordEnd == -1) {
return null;
}
return myEditor.getDocument().getCharsSequence().subSequence(wordStart, wordEnd).toString();
}
case AccessibleText.SENTENCE: {
int lineStart = getLineAtOffsetStart(offset, direction);
int lineEnd = getLineAtOffsetEnd(offset, direction);
if (lineStart == -1 || lineEnd == -1) {
return null;
}
return document.getCharsSequence().subSequence(lineStart, lineEnd).toString();
}
case AccessibleExtendedText.LINE:
case AccessibleExtendedText.ATTRIBUTE_RUN:
// Not expected to be called in this method!
assert false : type;
default:
break;
}
return null;
}
/**
* Similar to {@link #getTextAtOffset} but returns an {@link AccessibleTextSequence} and can accept a few more types.
*/
@Nullable
private AccessibleTextSequence getSequenceAtIndex(
@MagicConstant(intValues = {
AccessibleText.CHARACTER,
AccessibleText.WORD,
AccessibleText.SENTENCE,
AccessibleExtendedText.LINE,
AccessibleExtendedText.ATTRIBUTE_RUN})
int type,
int offset,
@MagicConstant(intValues = {BEFORE, HERE, AFTER})
int direction) {
assert direction == BEFORE || direction == HERE || direction == AFTER;
DocumentEx document = myEditor.getDocument();
if (offset < 0 || offset >= document.getTextLength()) {
return null;
}
switch (type) {
case AccessibleText.CHARACTER:
AccessibleTextSequence charSequence = null;
if (offset + direction < document.getTextLength() &&
offset + direction >= 0) {
int startOffset = offset + direction;
charSequence = new AccessibleTextSequence(startOffset, startOffset + 1,
document.getCharsSequence().subSequence(startOffset, startOffset + 1).toString());
}
return charSequence;
case AccessibleExtendedText.ATTRIBUTE_RUN:
case AccessibleText.WORD: {
int wordStart = getWordAtOffsetStart(offset, direction);
int wordEnd = getWordAtOffsetEnd(offset, direction);
if (wordStart == -1 || wordEnd == -1) {
return null;
}
return new AccessibleTextSequence(wordStart, wordEnd,
document.getCharsSequence().subSequence(wordStart, wordEnd).toString());
}
case AccessibleExtendedText.LINE:
case AccessibleText.SENTENCE: {
int lineStart = getLineAtOffsetStart(offset, direction);
int lineEnd = getLineAtOffsetEnd(offset, direction);
if (lineStart == -1 || lineEnd == -1) {
return null;
}
return new AccessibleTextSequence(lineStart, lineEnd,
document.getCharsSequence().subSequence(lineStart, lineEnd).toString());
}
}
return null;
}
private int getLineAtOffsetStart(int offset) {
Document document = myEditor.getDocument();
if (offset == 0) {
return 0;
}
int lineNumber = myEditor.offsetToLogicalPosition(offset).line;
return document.getLineStartOffset(lineNumber);
}
private int moveLineOffset(int offset, @MagicConstant(intValues = {BEFORE, HERE, AFTER}) int direction) {
if (direction == AFTER) {
int lineNumber = myEditor.offsetToLogicalPosition(offset).line;
lineNumber++;
Document document = myEditor.getDocument();
if (lineNumber == document.getLineCount()) {
return -1;
}
return document.getLineStartOffset(lineNumber);
} else if (direction == BEFORE) {
int lineNumber = myEditor.offsetToLogicalPosition(offset).line;
lineNumber--;
if (lineNumber < 0) {
return -1;
}
Document document = myEditor.getDocument();
return document.getLineStartOffset(lineNumber);
} else {
assert direction == HERE;
return offset;
}
}
private int getLineAtOffsetStart(int offset, @MagicConstant(intValues = {BEFORE, HERE, AFTER}) int direction) {
offset = moveLineOffset(offset, direction);
if (offset == -1) {
return -1;
}
return getLineAtOffsetStart(offset);
}
private int getLineAtOffsetEnd(int offset) {
Document document = myEditor.getDocument();
if (offset == 0) {
return 0;
}
int lineNumber = myEditor.offsetToLogicalPosition(offset).line;
return document.getLineEndOffset(lineNumber);
}
private int getLineAtOffsetEnd(int offset, @MagicConstant(intValues = {BEFORE, HERE, AFTER}) int direction) {
offset = moveLineOffset(offset, direction);
if (offset == -1) {
return -1;
}
return getLineAtOffsetEnd(offset);
}
private int moveWordOffset(int offset, @MagicConstant(intValues = {BEFORE, HERE, AFTER}) int direction) {
if (direction == AFTER) {
Document document = myEditor.getDocument();
CharSequence text = document.getCharsSequence();
int maxOffset = document.getTextLength();
int newOffset = offset - 1;
boolean camel = myEditor.getSettings().isCamelWords();
for (; newOffset < maxOffset; newOffset++) {
if (EditorActionUtil.isWordEnd(text, newOffset, camel)) {
break;
}
}
newOffset++;
for (; newOffset < maxOffset; newOffset++) {
if (EditorActionUtil.isWordStart(text, newOffset, camel)) {
return newOffset;
}
}
return -1;
} else if (direction == BEFORE) {
Document document = myEditor.getDocument();
CharSequence text = document.getCharsSequence();
int newOffset = offset - 1;
boolean camel = myEditor.getSettings().isCamelWords();
for (; newOffset >= 0; newOffset--) {
if (EditorActionUtil.isWordStart(text, newOffset, camel)) {
break;
}
}
newOffset--;
for (; newOffset >= 0; newOffset--) {
if (EditorActionUtil.isWordEnd(text, newOffset, camel)) {
return newOffset;
}
}
return -1;
} else {
assert direction == HERE;
return offset;
}
}
private int getWordAtOffsetStart(int offset, @MagicConstant(intValues = {BEFORE, HERE, AFTER}) int direction) {
offset = moveWordOffset(offset, direction);
if (offset == -1) {
return -1;
}
return getWordAtOffsetStart(offset);
}
private int getWordAtOffsetEnd(int offset, @MagicConstant(intValues = {BEFORE, HERE, AFTER}) int direction) {
offset = moveWordOffset(offset, direction);
if (offset == -1) {
return -1;
}
return getWordAtOffsetEnd(offset);
}
// Based on CaretImpl#getWordAtCaretStart
private int getWordAtOffsetStart(int offset) {
Document document = myEditor.getDocument();
if (offset == 0) {
return 0;
}
int lineNumber = myEditor.offsetToLogicalPosition(offset).line;
CharSequence text = document.getCharsSequence();
int newOffset = offset - 1;
int minOffset = lineNumber > 0 ? document.getLineEndOffset(lineNumber - 1) : 0;
boolean camel = myEditor.getSettings().isCamelWords();
for (; newOffset > minOffset; newOffset--) {
if (EditorActionUtil.isWordStart(text, newOffset, camel)) {
break;
}
}
return newOffset;
}
// Based on CaretImpl#getWordAtCaretEnd
private int getWordAtOffsetEnd(int offset) {
Document document = myEditor.getDocument();
CharSequence text = document.getCharsSequence();
if (offset >= document.getTextLength() - 1 || document.getLineCount() == 0) {
return offset;
}
int newOffset = offset + 1;
int lineNumber = myEditor.offsetToLogicalPosition(offset).line;
int maxOffset = document.getLineEndOffset(lineNumber);
if (newOffset > maxOffset) {
if (lineNumber + 1 >= document.getLineCount()) {
return offset;
}
maxOffset = document.getLineEndOffset(lineNumber + 1);
}
boolean camel = myEditor.getSettings().isCamelWords();
for (; newOffset < maxOffset; newOffset++) {
if (EditorActionUtil.isWordEnd(text, newOffset, camel)) {
break;
}
}
return newOffset;
}
}
}