/*
* Copyright 2000-2017 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.application.options.EditorFontsConstants;
import com.intellij.codeInsight.hint.DocumentFragmentTooltipRenderer;
import com.intellij.codeInsight.hint.EditorFragmentComponent;
import com.intellij.codeInsight.hint.TooltipController;
import com.intellij.codeInsight.hint.TooltipGroup;
import com.intellij.concurrency.JobScheduler;
import com.intellij.diagnostic.Dumpable;
import com.intellij.ide.*;
import com.intellij.ide.dnd.DnDManager;
import com.intellij.ide.ui.UISettings;
import com.intellij.ide.ui.customization.CustomActionsSchema;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
import com.intellij.openapi.actionSystem.ex.ActionUtil;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.TransactionGuard;
import com.intellij.openapi.application.TransactionGuardImpl;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.command.UndoConfirmationPolicy;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.actionSystem.*;
import com.intellij.openapi.editor.colors.*;
import com.intellij.openapi.editor.colors.impl.DelegateColorScheme;
import com.intellij.openapi.editor.colors.impl.FontPreferencesImpl;
import com.intellij.openapi.editor.event.*;
import com.intellij.openapi.editor.ex.*;
import com.intellij.openapi.editor.ex.util.EditorUIUtil;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.editor.ex.util.EmptyEditorHighlighter;
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
import com.intellij.openapi.editor.highlighter.HighlighterClient;
import com.intellij.openapi.editor.impl.event.MarkupModelListener;
import com.intellij.openapi.editor.impl.softwrap.SoftWrapAppliancePlaces;
import com.intellij.openapi.editor.impl.view.EditorView;
import com.intellij.openapi.editor.markup.GutterDraggableObject;
import com.intellij.openapi.editor.markup.GutterIconRenderer;
import com.intellij.openapi.editor.markup.RangeHighlighter;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory;
import com.intellij.openapi.fileEditor.impl.EditorsSplitters;
import com.intellij.openapi.keymap.Keymap;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.AbstractPainter;
import com.intellij.openapi.ui.Queryable;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.openapi.wm.IdeGlassPane;
import com.intellij.openapi.wm.IdeGlassPaneUtil;
import com.intellij.openapi.wm.ToolWindowAnchor;
import com.intellij.openapi.wm.ex.ToolWindowManagerEx;
import com.intellij.openapi.wm.impl.IdeGlassPaneImpl;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.ui.*;
import com.intellij.ui.components.JBLayeredPane;
import com.intellij.ui.components.JBScrollBar;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.mac.MacGestureSupportForEditor;
import com.intellij.util.*;
import com.intellij.util.concurrency.EdtExecutorService;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.ContainerUtilRt;
import com.intellij.util.ui.*;
import com.intellij.util.ui.update.Activatable;
import com.intellij.util.ui.update.UiNotifyConnector;
import org.intellij.lang.annotations.JdkConstants;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import javax.swing.*;
import javax.swing.Timer;
import javax.swing.border.Border;
import javax.swing.plaf.ScrollBarUI;
import javax.swing.plaf.ScrollPaneUI;
import javax.swing.plaf.basic.BasicScrollBarUI;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.event.*;
import java.awt.font.TextHitInfo;
import java.awt.geom.Point2D;
import java.awt.im.InputMethodRequests;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.lang.reflect.Field;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.text.CharacterIterator;
import java.util.*;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public final class EditorImpl extends UserDataHolderBase implements EditorEx, HighlighterClient, Queryable, Dumpable {
private static final int MIN_FONT_SIZE = 8;
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl");
private static final Key DND_COMMAND_KEY = Key.create("DndCommand");
@NonNls
public static final Object IGNORE_MOUSE_TRACKING = "ignore_mouse_tracking";
private static final Key<JComponent> PERMANENT_HEADER = Key.create("PERMANENT_HEADER");
public static final Key<Boolean> DO_DOCUMENT_UPDATE_TEST = Key.create("DoDocumentUpdateTest");
static final Key<Boolean> FORCED_SOFT_WRAPS = Key.create("forced.soft.wraps");
public static final Key<Boolean> DISABLE_CARET_POSITION_KEEPING = Key.create("editor.disable.caret.position.keeping");
static final Key<Boolean> DISABLE_CARET_SHIFT_ON_WHITESPACE_INSERTION = Key.create("editor.disable.caret.shift.on.whitespace.insertion");
private static final boolean HONOR_CAMEL_HUMPS_ON_TRIPLE_CLICK = Boolean.parseBoolean(System.getProperty("idea.honor.camel.humps.on.triple.click"));
private static final Key<BufferedImage> BUFFER = Key.create("buffer");
@NotNull
private final DocumentEx myDocument;
private final JPanel myPanel;
@NotNull
private final JScrollPane myScrollPane;
@NotNull
private final EditorComponentImpl myEditorComponent;
@NotNull
private final EditorGutterComponentImpl myGutterComponent;
private final TraceableDisposable myTraceableDisposable = new TraceableDisposable(true);
private static final Cursor EMPTY_CURSOR;
static {
Cursor emptyCursor = null;
if (!GraphicsEnvironment.isHeadless()) {
try {
emptyCursor = Toolkit.getDefaultToolkit().createCustomCursor(UIUtil.createImage(1, 1, BufferedImage.TYPE_INT_ARGB), new Point(), "Empty cursor");
}
catch (Exception e) {
LOG.warn("Couldn't create an empty cursor", e);
}
}
EMPTY_CURSOR = emptyCursor;
}
private final CommandProcessor myCommandProcessor;
@NotNull
private final MyScrollBar myVerticalScrollBar;
private final List<EditorMouseListener> myMouseListeners = ContainerUtil.createLockFreeCopyOnWriteList();
@NotNull
private final List<EditorMouseMotionListener> myMouseMotionListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private boolean myIsInsertMode = true;
@NotNull
private final CaretCursor myCaretCursor;
private final ScrollingTimer myScrollingTimer = new ScrollingTimer();
@SuppressWarnings("RedundantStringConstructorCall")
private final Object MOUSE_DRAGGED_GROUP = new String("MouseDraggedGroup");
@NotNull
private final SettingsImpl mySettings;
private boolean isReleased;
@Nullable
private MouseEvent myMousePressedEvent;
@Nullable
private MouseEvent myMouseMovedEvent;
private final MouseListener myMouseListener = new MyMouseAdapter();
private final MouseMotionListener myMouseMotionListener = new MyMouseMotionListener();
/**
* Holds information about area where mouse was pressed.
*/
@Nullable
private EditorMouseEventArea myMousePressArea;
private int mySavedSelectionStart = -1;
private int mySavedSelectionEnd = -1;
private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this);
private MyEditable myEditable;
@NotNull
private EditorColorsScheme myScheme;
private boolean myIsViewer;
@NotNull
private final SelectionModelImpl mySelectionModel;
@NotNull
private final EditorMarkupModelImpl myMarkupModel;
@NotNull
private final EditorFilteringMarkupModelEx myDocumentMarkupModel;
@NotNull
private final MarkupModelListener myMarkupModelListener;
@NotNull
private final FoldingModelImpl myFoldingModel;
@NotNull
private final ScrollingModelImpl myScrollingModel;
@NotNull
private final CaretModelImpl myCaretModel;
@NotNull
private final SoftWrapModelImpl mySoftWrapModel;
@NotNull
private final InlayModelImpl myInlayModel;
@NotNull
private static final RepaintCursorCommand ourCaretBlinkingCommand = new RepaintCursorCommand();
private final DocumentBulkUpdateListener myBulkUpdateListener;
@MouseSelectionState
private int myMouseSelectionState;
@Nullable
private FoldRegion myMouseSelectedRegion;
@MagicConstant(intValues = {MOUSE_SELECTION_STATE_NONE, MOUSE_SELECTION_STATE_LINE_SELECTED, MOUSE_SELECTION_STATE_WORD_SELECTED})
private @interface MouseSelectionState {
}
private static final int MOUSE_SELECTION_STATE_NONE = 0;
private static final int MOUSE_SELECTION_STATE_WORD_SELECTED = 1;
private static final int MOUSE_SELECTION_STATE_LINE_SELECTED = 2;
private EditorHighlighter myHighlighter;
private Disposable myHighlighterDisposable = Disposer.newDisposable();
private final TextDrawingCallback myTextDrawingCallback = new MyTextDrawingCallback();
@MagicConstant(intValues = {VERTICAL_SCROLLBAR_LEFT, VERTICAL_SCROLLBAR_RIGHT})
private int myScrollBarOrientation;
private boolean myMousePressedInsideSelection;
private boolean myUpdateCursor;
private int myCaretUpdateVShift;
@Nullable
private final Project myProject;
private long myMouseSelectionChangeTimestamp;
private int mySavedCaretOffsetForDNDUndoHack;
private final List<FocusChangeListener> myFocusListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private MyInputMethodHandler myInputMethodRequestsHandler;
private InputMethodRequests myInputMethodRequestsSwingWrapper;
private boolean myIsOneLineMode;
private boolean myIsRendererMode;
private VirtualFile myVirtualFile;
private boolean myIsColumnMode;
@Nullable
private Color myForcedBackground;
@Nullable
private Dimension myPreferredSize;
private final Alarm myMouseSelectionStateAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD);
private Runnable myMouseSelectionStateResetRunnable;
private boolean myEmbeddedIntoDialogWrapper;
private int myDragOnGutterSelectionStartLine = -1;
private RangeMarker myDraggedRange;
@NotNull
private final JPanel myHeaderPanel;
@Nullable
private MouseEvent myInitialMouseEvent;
private boolean myIgnoreMouseEventsConsecutiveToInitial;
private EditorDropHandler myDropHandler;
private Condition<RangeHighlighter> myHighlightingFilter;
@NotNull
private final IndentsModel myIndentsModel;
@Nullable
private CharSequence myPlaceholderText;
@Nullable
private TextAttributes myPlaceholderAttributes;
private boolean myShowPlaceholderWhenFocused;
private boolean myStickySelection;
private int myStickySelectionStart;
private boolean myScrollToCaret = true;
private boolean myPurePaintingMode;
private boolean myPaintSelection;
private final EditorSizeAdjustmentStrategy mySizeAdjustmentStrategy = new EditorSizeAdjustmentStrategy();
private final Disposable myDisposable = Disposer.newDisposable();
private List<CaretState> myCaretStateBeforeLastPress;
LogicalPosition myLastMousePressedLocation;
private VisualPosition myTargetMultiSelectionPosition;
private boolean myMultiSelectionInProgress;
private boolean myRectangularSelectionInProgress;
private boolean myLastPressCreatedCaret;
// Set when the selection (normal or block one) initiated by mouse drag becomes noticeable (at least one character is selected).
// Reset on mouse press event.
private boolean myCurrentDragIsSubstantial;
private CaretImpl myPrimaryCaret;
public final boolean myDisableRtl = Registry.is("editor.disable.rtl");
final EditorView myView;
private boolean myCharKeyPressed;
private boolean myNeedToSelectPreviousChar;
private boolean myDocumentChangeInProgress;
private boolean myErrorStripeNeedsRepaint;
private String myContextMenuGroupId = IdeActions.GROUP_BASIC_EDITOR_POPUP;
private boolean myUseEditorAntialiasing = true;
private final ImmediatePainter myImmediatePainter;
static {
ourCaretBlinkingCommand.start();
}
private volatile int myExpectedCaretOffset = -1;
private boolean myBackgroundImageSet;
EditorImpl(@NotNull Document document, boolean viewer, @Nullable Project project) {
assertIsDispatchThread();
myProject = project;
myDocument = (DocumentEx)document;
myScheme = createBoundColorSchemeDelegate(null);
myScrollPane = new MyScrollPane(); // create UI after scheme initialization
myIsViewer = viewer;
mySettings = new SettingsImpl(this, project);
if (!mySettings.isUseSoftWraps() && shouldSoftWrapsBeForced()) {
mySettings.setUseSoftWrapsQuiet();
putUserData(FORCED_SOFT_WRAPS, Boolean.TRUE);
}
MarkupModelEx documentMarkup = (MarkupModelEx)DocumentMarkupModel.forDocument(myDocument, myProject, true);
mySelectionModel = new SelectionModelImpl(this);
myMarkupModel = new EditorMarkupModelImpl(this);
myDocumentMarkupModel = new EditorFilteringMarkupModelEx(this, documentMarkup);
myFoldingModel = new FoldingModelImpl(this);
myCaretModel = new CaretModelImpl(this);
myCaretModel.initCarets();
myScrollingModel = new ScrollingModelImpl(this);
myInlayModel = new InlayModelImpl(this);
Disposer.register(myCaretModel, myInlayModel);
mySoftWrapModel = new SoftWrapModelImpl(this);
myCommandProcessor = CommandProcessor.getInstance();
myImmediatePainter = new ImmediatePainter(this);
if (myDocument instanceof DocumentImpl) {
myBulkUpdateListener = new EditorDocumentBulkUpdateAdapter();
((DocumentImpl)myDocument).addInternalBulkModeListener(myBulkUpdateListener);
}
else {
myBulkUpdateListener = null;
}
myMarkupModelListener = new MarkupModelListener() {
private boolean areRenderersInvolved(@NotNull RangeHighlighterEx highlighter) {
return highlighter.getCustomRenderer() != null ||
highlighter.getGutterIconRenderer() != null ||
highlighter.getLineMarkerRenderer() != null ||
highlighter.getLineSeparatorRenderer() != null;
}
@Override
public void afterAdded(@NotNull RangeHighlighterEx highlighter) {
attributesChanged(highlighter, areRenderersInvolved(highlighter), EditorUtil.attributesImpactFontStyleOrColor(highlighter.getTextAttributes()));
}
@Override
public void beforeRemoved(@NotNull RangeHighlighterEx highlighter) {
attributesChanged(highlighter, areRenderersInvolved(highlighter), EditorUtil.attributesImpactFontStyleOrColor(highlighter.getTextAttributes()));
}
@Override
public void attributesChanged(@NotNull RangeHighlighterEx highlighter, boolean renderersChanged, boolean fontStyleOrColorChanged) {
if (myDocument.isInBulkUpdate()) return; // bulkUpdateFinished() will repaint anything
if (renderersChanged) {
updateGutterSize();
}
boolean errorStripeNeedsRepaint = renderersChanged || highlighter.getErrorStripeMarkColor() != null;
if (myDocumentChangeInProgress) {
// postpone repaint request, as folding model can be in inconsistent state and so coordinate
// conversions might give incorrect results
myErrorStripeNeedsRepaint |= errorStripeNeedsRepaint;
return;
}
int textLength = myDocument.getTextLength();
int start = Math.min(Math.max(highlighter.getAffectedAreaStartOffset(), 0), textLength);
int end = Math.min(Math.max(highlighter.getAffectedAreaEndOffset(), 0), textLength);
int startLine = start == -1 ? 0 : myDocument.getLineNumber(start);
int endLine = end == -1 ? myDocument.getLineCount() : myDocument.getLineNumber(end);
if (start != end && fontStyleOrColorChanged) {
myView.invalidateRange(start, end);
}
if (!myFoldingModel.isInBatchFoldingOperation()) { // at the end of batch folding operation everything is repainted
repaintLines(Math.max(0, startLine - 1), Math.min(endLine + 1, getDocument().getLineCount()));
}
// optimization: there is no need to repaint error stripe if the highlighter is invisible on it
if (errorStripeNeedsRepaint) {
if (myFoldingModel.isInBatchFoldingOperation()) {
myErrorStripeNeedsRepaint = true;
}
else {
myMarkupModel.repaint(start, end);
}
}
updateCaretCursor();
}
};
getFilteredDocumentMarkupModel().addMarkupModelListener(myCaretModel, myMarkupModelListener);
getMarkupModel().addMarkupModelListener(myCaretModel, myMarkupModelListener);
myDocument.addDocumentListener(myFoldingModel, myCaretModel);
myDocument.addDocumentListener(myCaretModel, myCaretModel);
myDocument.addDocumentListener(new EditorDocumentAdapter(), myCaretModel);
myDocument.addDocumentListener(mySoftWrapModel, myCaretModel);
myFoldingModel.addListener(mySoftWrapModel, myCaretModel);
myInlayModel.addListener(myCaretModel, myCaretModel);
myIndentsModel = new IndentsModelImpl(this);
myCaretModel.addCaretListener(new CaretListener() {
@Nullable
private LightweightHint myCurrentHint;
@Nullable
private IndentGuideDescriptor myCurrentCaretGuide;
@Override
public void caretPositionChanged(CaretEvent e) {
if (myStickySelection) {
int selectionStart = Math.min(myStickySelectionStart, getDocument().getTextLength() - 1);
mySelectionModel.setSelection(selectionStart, myCaretModel.getVisualPosition(), myCaretModel.getOffset());
}
final IndentGuideDescriptor newGuide = myIndentsModel.getCaretIndentGuide();
if (!Comparing.equal(myCurrentCaretGuide, newGuide)) {
repaintGuide(newGuide);
repaintGuide(myCurrentCaretGuide);
myCurrentCaretGuide = newGuide;
if (myCurrentHint != null) {
myCurrentHint.hide();
myCurrentHint = null;
}
if (newGuide != null) {
final Rectangle visibleArea = getScrollingModel().getVisibleArea();
final int line = newGuide.startLine;
if (logicalLineToY(line) < visibleArea.y) {
TextRange textRange = new TextRange(myDocument.getLineStartOffset(line), myDocument.getLineEndOffset(line));
myCurrentHint = EditorFragmentComponent.showEditorFragmentHint(EditorImpl.this, textRange, false, false);
}
}
}
}
@Override
public void caretAdded(CaretEvent e) {
if (myPrimaryCaret != null) {
myPrimaryCaret.updateVisualPosition(); // repainting old primary caret's row background
}
repaintCaretRegion(e);
myPrimaryCaret = myCaretModel.getPrimaryCaret();
}
@Override
public void caretRemoved(CaretEvent e) {
repaintCaretRegion(e);
myPrimaryCaret = myCaretModel.getPrimaryCaret(); // repainting new primary caret's row background
myPrimaryCaret.updateVisualPosition();
}
});
myCaretCursor = new CaretCursor();
myFoldingModel.flushCaretShift();
myScrollBarOrientation = VERTICAL_SCROLLBAR_RIGHT;
mySoftWrapModel.addSoftWrapChangeListener(new SoftWrapChangeListenerAdapter() {
@Override
public void recalculationEnds() {
if (myCaretModel.isUpToDate()) {
myCaretModel.updateVisualPosition();
}
}
});
EditorHighlighter highlighter = new NullEditorHighlighter();
setHighlighter(highlighter);
myEditorComponent = new EditorComponentImpl(this);
myVerticalScrollBar = (MyScrollBar)myScrollPane.getVerticalScrollBar();
myVerticalScrollBar.setOpaque(false);
myPanel = new JPanel();
UIUtil.putClientProperty(myPanel, UIUtil.NOT_IN_HIERARCHY_COMPONENTS, (Iterable<JComponent>)() -> {
JComponent component = getPermanentHeaderComponent();
if (component != null && !component.isValid()) {
return Collections.singleton(component).iterator();
}
return ContainerUtil.emptyIterator();
});
myHeaderPanel = new MyHeaderPanel();
myGutterComponent = new EditorGutterComponentImpl(this);
initComponent();
myView = new EditorView(this);
myView.reinitSettings();
myInlayModel.addListener(new InlayModel.SimpleAdapter() {
@Override
public void onUpdated(@NotNull Inlay inlay) {
if (myDocument.isInEventsHandling() || myDocument.isInBulkUpdate()) return;
validateSize();
repaint(inlay.getOffset(), inlay.getOffset(), false);
}
}, myCaretModel);
if (UISettings.getInstance().getPresentationMode()) {
setFontSize(UISettings.getInstance().getPresentationModeFontSize());
}
myGutterComponent.updateSize();
Dimension preferredSize = getPreferredSize();
myEditorComponent.setSize(preferredSize);
updateCaretCursor();
if (SystemInfo.isJavaVersionAtLeast("1.8") && SystemInfo.isMacIntel64 && SystemInfo.isJetbrainsJvm && Registry.is("ide.mac.forceTouch")) {
new MacGestureSupportForEditor(getComponent());
}
}
public boolean shouldSoftWrapsBeForced() {
if (myProject != null && PsiDocumentManager.getInstance(myProject).isDocumentBlockedByPsi(myDocument)) {
// Disable checking for files in intermediate states - e.g. for files during refactoring.
return false;
}
int lineWidthLimit = Registry.intValue("editor.soft.wrap.force.limit");
for (int i = 0; i < myDocument.getLineCount(); i++) {
if (myDocument.getLineEndOffset(i) - myDocument.getLineStartOffset(i) > lineWidthLimit) {
return true;
}
}
return false;
}
@NotNull
static Color adjustThumbColor(@NotNull Color base, boolean dark) {
return dark ? ColorUtil.withAlpha(ColorUtil.shift(base, 1.35), 0.5) : ColorUtil.withAlpha(ColorUtil.shift(base, 0.68), 0.4);
}
boolean isDarkEnough() {
return ColorUtil.isDark(getBackgroundColor());
}
private void repaintCaretRegion(CaretEvent e) {
CaretImpl caretImpl = (CaretImpl)e.getCaret();
if (caretImpl != null) {
caretImpl.updateVisualPosition();
if (caretImpl.hasSelection()) {
repaint(caretImpl.getSelectionStart(), caretImpl.getSelectionEnd(), false);
}
}
}
@NotNull
@Override
public EditorColorsScheme createBoundColorSchemeDelegate(@Nullable final EditorColorsScheme customGlobalScheme) {
return new MyColorSchemeDelegate(customGlobalScheme);
}
private void repaintGuide(@Nullable IndentGuideDescriptor guide) {
if (guide != null) {
repaintLines(guide.startLine, guide.endLine);
}
}
@Override
public int getPrefixTextWidthInPixels() {
return (int)myView.getPrefixTextWidthInPixels();
}
@Override
public void setPrefixTextAndAttributes(@Nullable String prefixText, @Nullable TextAttributes attributes) {
mySoftWrapModel.recalculate();
myView.setPrefix(prefixText, attributes);
}
@Override
public boolean isPurePaintingMode() {
return myPurePaintingMode;
}
@Override
public void setPurePaintingMode(boolean enabled) {
myPurePaintingMode = enabled;
}
@Override
public void registerScrollBarRepaintCallback(@Nullable Consumer<Graphics> callback) {
myVerticalScrollBar.registerRepaintCallback(callback);
}
@Override
public int getExpectedCaretOffset() {
int expectedCaretOffset = myExpectedCaretOffset;
return expectedCaretOffset == -1 ? getCaretModel().getOffset() : expectedCaretOffset;
}
@Override
public void setContextMenuGroupId(@Nullable String groupId) {
myContextMenuGroupId = groupId;
}
@Nullable
@Override
public String getContextMenuGroupId() {
return myContextMenuGroupId;
}
@Override
public void setViewer(boolean isViewer) {
myIsViewer = isViewer;
}
@Override
public boolean isViewer() {
return myIsViewer || myIsRendererMode;
}
@Override
public boolean isRendererMode() {
return myIsRendererMode;
}
@Override
public void setRendererMode(boolean isRendererMode) {
myIsRendererMode = isRendererMode;
}
@Override
public void setFile(VirtualFile vFile) {
myVirtualFile = vFile;
reinitSettings();
}
@Override
public VirtualFile getVirtualFile() {
return myVirtualFile;
}
@Override
public void setSoftWrapAppliancePlace(@NotNull SoftWrapAppliancePlaces place) {
mySettings.setSoftWrapAppliancePlace(place);
}
@Override
@NotNull
public SelectionModelImpl getSelectionModel() {
return mySelectionModel;
}
@Override
@NotNull
public MarkupModelEx getMarkupModel() {
return myMarkupModel;
}
@Override
@NotNull
public MarkupModelEx getFilteredDocumentMarkupModel() {
return myDocumentMarkupModel;
}
@Override
@NotNull
public FoldingModelImpl getFoldingModel() {
return myFoldingModel;
}
@Override
@NotNull
public CaretModelImpl getCaretModel() {
return myCaretModel;
}
@Override
@NotNull
public ScrollingModelEx getScrollingModel() {
return myScrollingModel;
}
@Override
@NotNull
public SoftWrapModelImpl getSoftWrapModel() {
return mySoftWrapModel;
}
@NotNull
@Override
public InlayModelImpl getInlayModel() {
return myInlayModel;
}
@Override
@NotNull
public EditorSettings getSettings() {
assertReadAccess();
return mySettings;
}
public void resetSizes() {
myView.reset();
}
@Override
public void reinitSettings() {
assertIsDispatchThread();
for (EditorColorsScheme scheme = myScheme; scheme instanceof DelegateColorScheme; scheme = ((DelegateColorScheme)scheme).getDelegate()) {
if (scheme instanceof MyColorSchemeDelegate) {
((MyColorSchemeDelegate)scheme).updateGlobalScheme();
break;
}
}
boolean softWrapsUsedBefore = mySoftWrapModel.isSoftWrappingEnabled();
mySettings.reinitSettings();
mySoftWrapModel.reinitSettings();
myCaretModel.reinitSettings();
mySelectionModel.reinitSettings();
ourCaretBlinkingCommand.setBlinkCaret(mySettings.isBlinkCaret());
ourCaretBlinkingCommand.setBlinkPeriod(mySettings.getCaretBlinkPeriod());
myView.reinitSettings();
myFoldingModel.rebuild();
myInlayModel.reinitSettings();
if (softWrapsUsedBefore ^ mySoftWrapModel.isSoftWrappingEnabled()) {
validateSize();
}
myHighlighter.setColorScheme(myScheme);
myFoldingModel.refreshSettings();
myGutterComponent.reinitSettings();
myGutterComponent.revalidate();
myEditorComponent.repaint();
updateCaretCursor();
if (myInitialMouseEvent != null) {
myIgnoreMouseEventsConsecutiveToInitial = true;
}
myCaretModel.updateVisualPosition();
// make sure carets won't appear at invalid positions (e.g. on Tab width change)
for (Caret caret : getCaretModel().getAllCarets()) {
caret.moveToOffset(caret.getOffset());
}
}
/**
* To be called when editor was not disposed while it should
*/
void throwEditorNotDisposedError(@NonNls @NotNull final String msg) {
myTraceableDisposable.throwObjectNotDisposedError(msg);
}
/**
* In case of "editor not disposed error" use {@link #throwEditorNotDisposedError(String)}
*/
public void throwDisposalError(@NonNls @NotNull String msg) {
myTraceableDisposable.throwDisposalError(msg);
}
// EditorFactory.releaseEditor should be used to release editor
void release() {
assertIsDispatchThread();
if (isReleased) {
throwDisposalError("Double release of editor:");
}
myTraceableDisposable.kill(null);
isReleased = true;
mySizeAdjustmentStrategy.cancelAllRequests();
myFoldingModel.dispose();
mySoftWrapModel.release();
myMarkupModel.dispose();
myScrollingModel.dispose();
myGutterComponent.dispose();
myMousePressedEvent = null;
myMouseMovedEvent = null;
Disposer.dispose(myCaretModel);
Disposer.dispose(mySoftWrapModel);
Disposer.dispose(myView);
clearCaretThread();
myFocusListeners.clear();
myMouseListeners.clear();
myMouseMotionListeners.clear();
myEditorComponent.removeMouseListener(myMouseListener);
myGutterComponent.removeMouseListener(myMouseListener);
myEditorComponent.removeMouseMotionListener(myMouseMotionListener);
myGutterComponent.removeMouseMotionListener(myMouseMotionListener);
if (myBulkUpdateListener != null) {
((DocumentImpl)myDocument).removeInternalBulkModeListener(myBulkUpdateListener);
}
Disposer.dispose(myDisposable);
myVerticalScrollBar.setUI(null); // clear error panel's cached image
}
private void clearCaretThread() {
synchronized (ourCaretBlinkingCommand) {
if (ourCaretBlinkingCommand.myEditor == this) {
ourCaretBlinkingCommand.myEditor = null;
}
}
}
private void initComponent() {
myPanel.setLayout(new BorderLayout());
myPanel.add(myHeaderPanel, BorderLayout.NORTH);
myGutterComponent.setOpaque(true);
myScrollPane.setViewportView(myEditorComponent);
//myScrollPane.setBorder(null);
myScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
myScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
myScrollPane.setRowHeaderView(myGutterComponent);
myEditorComponent.setTransferHandler(new MyTransferHandler());
myEditorComponent.setAutoscrolls(true);
if (mayShowToolbar()) {
JLayeredPane layeredPane = new JBLayeredPane() {
@Override
public void doLayout() {
final Component[] components = getComponents();
final Rectangle r = getBounds();
for (Component c : components) {
if (c instanceof JScrollPane) {
c.setBounds(0, 0, r.width, r.height);
}
else {
final Dimension d = c.getPreferredSize();
int rightInsets = getVerticalScrollBar().getWidth() + (isMirrored() ? myGutterComponent.getWidth() : 0);
c.setBounds(r.width - d.width - rightInsets - 20, 20, d.width, d.height);
}
}
}
@Override
public Dimension getPreferredSize() {
return myScrollPane.getPreferredSize();
}
};
layeredPane.add(myScrollPane, JLayeredPane.DEFAULT_LAYER);
myPanel.add(layeredPane);
new ContextMenuImpl(layeredPane, myScrollPane, this);
}
else {
myPanel.add(myScrollPane);
}
myEditorComponent.addKeyListener(new KeyListener() {
@Override
public void keyPressed(@NotNull KeyEvent e) {
if (e.getKeyCode() >= KeyEvent.VK_A && e.getKeyCode() <= KeyEvent.VK_Z) {
myCharKeyPressed = true;
}
KeyboardInternationalizationNotificationManager.showNotification();
}
@Override
public void keyTyped(@NotNull KeyEvent event) {
myNeedToSelectPreviousChar = false;
if (event.isConsumed()) {
return;
}
if (processKeyTyped(event)) {
event.consume();
}
}
@Override
public void keyReleased(KeyEvent e) {
myCharKeyPressed = false;
}
});
myEditorComponent.addMouseListener(myMouseListener);
myGutterComponent.addMouseListener(myMouseListener);
myEditorComponent.addMouseMotionListener(myMouseMotionListener);
myGutterComponent.addMouseMotionListener(myMouseMotionListener);
myEditorComponent.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(@NotNull FocusEvent e) {
myCaretCursor.activate();
for (Caret caret : myCaretModel.getAllCarets()) {
int caretLine = caret.getLogicalPosition().line;
repaintLines(caretLine, caretLine);
}
fireFocusGained();
}
@Override
public void focusLost(@NotNull FocusEvent e) {
clearCaretThread();
for (Caret caret : myCaretModel.getAllCarets()) {
int caretLine = caret.getLogicalPosition().line;
repaintLines(caretLine, caretLine);
}
fireFocusLost();
}
});
UiNotifyConnector connector = new UiNotifyConnector(myEditorComponent, new Activatable.Adapter() {
@Override
public void showNotify() {
myGutterComponent.updateSizeOnShowNotify();
}
});
Disposer.register(getDisposable(), connector);
try {
final DropTarget dropTarget = myEditorComponent.getDropTarget();
if (dropTarget != null) { // might be null in headless environment
dropTarget.addDropTargetListener(new DropTargetAdapter() {
@Override
public void drop(@NotNull DropTargetDropEvent e) {
}
@Override
public void dragOver(@NotNull DropTargetDragEvent e) {
Point location = e.getLocation();
getCaretModel().moveToVisualPosition(getTargetPosition(location.x, location.y, true));
getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
requestFocus();
}
});
}
}
catch (TooManyListenersException e) {
LOG.error(e);
}
// update area available for soft wrapping on component shown/hidden
myPanel.addHierarchyListener(e -> mySoftWrapModel.getApplianceManager().updateAvailableArea());
myPanel.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(@NotNull ComponentEvent e) {
myMarkupModel.recalcEditorDimensions();
myMarkupModel.repaint(-1, -1);
}
});
}
private boolean mayShowToolbar() {
return !isEmbeddedIntoDialogWrapper() && !isOneLineMode() && ContextMenuImpl.mayShowToolbar(myDocument);
}
@Override
public void setFontSize(final int fontSize) {
setFontSize(fontSize, null);
}
/**
* Changes editor font size, attempting to keep a given point unmoved. If point is not given, top left screen corner is assumed.
*
* @param fontSize new font size
* @param zoomCenter zoom point, relative to viewport
*/
private void setFontSize(int fontSize, @Nullable Point zoomCenter) {
int oldFontSize = myScheme.getEditorFontSize();
Rectangle visibleArea = myScrollingModel.getVisibleArea();
Point zoomCenterRelative = zoomCenter == null ? new Point() : zoomCenter;
Point zoomCenterAbsolute = new Point(visibleArea.x + zoomCenterRelative.x, visibleArea.y + zoomCenterRelative.y);
LogicalPosition zoomCenterLogical = xyToLogicalPosition(zoomCenterAbsolute).withoutVisualPositionInfo();
int oldLineHeight = getLineHeight();
int intraLineOffset = zoomCenterAbsolute.y % oldLineHeight;
myScheme.setEditorFontSize(fontSize);
fontSize = myScheme.getEditorFontSize(); // resulting font size might be different due to applied min/max limits
myPropertyChangeSupport.firePropertyChange(PROP_FONT_SIZE, oldFontSize, fontSize);
// Update vertical scroll bar bounds if necessary (we had a problem that use increased editor font size and it was not possible
// to scroll to the bottom of the document).
myScrollPane.getViewport().invalidate();
Point shiftedZoomCenterAbsolute = logicalPositionToXY(zoomCenterLogical);
myScrollingModel.disableAnimation();
try {
myScrollingModel.scroll(visibleArea.x == 0 ? 0 : shiftedZoomCenterAbsolute.x - zoomCenterRelative.x, // stick to left border if it's visible
shiftedZoomCenterAbsolute.y - zoomCenterRelative.y + (intraLineOffset * getLineHeight() + oldLineHeight / 2) / oldLineHeight);
}
finally {
myScrollingModel.enableAnimation();
}
}
public int getFontSize() {
return myScheme.getEditorFontSize();
}
@NotNull
public ActionCallback type(@NotNull final String text) {
final ActionCallback result = new ActionCallback();
for (int i = 0; i < text.length(); i++) {
if (!processKeyTyped(text.charAt(i))) {
result.setRejected();
return result;
}
}
result.setDone();
return result;
}
private boolean processKeyTyped(char c) {
// [vova] This is patch for Mac OS X. Under Mac "input methods"
// is handled before our EventQueue consume upcoming KeyEvents.
IdeEventQueue queue = IdeEventQueue.getInstance();
if (queue.shouldNotTypeInEditor() || ProgressManager.getInstance().hasModalProgressIndicator()) {
return false;
}
FileDocumentManager manager = FileDocumentManager.getInstance();
final VirtualFile file = manager.getFile(myDocument);
if (file != null && !file.isValid()) {
return false;
}
DataContext context = getDataContext();
Graphics graphics = GraphicsUtil.safelyGetGraphics(myEditorComponent);
if (graphics != null) { // editor component is not showing
processKeyTypedImmediately(c, graphics, context);
graphics.dispose();
}
ActionManagerEx.getInstanceEx().fireBeforeEditorTyping(c, context);
EditorUIUtil.hideCursorInEditor(this);
processKeyTypedNormally(c, context);
return true;
}
void processKeyTypedImmediately(char c, Graphics graphics, DataContext dataContext) {
EditorActionPlan plan = new EditorActionPlan(this);
EditorActionManager.getInstance().getTypedAction().beforeActionPerformed(this, c, dataContext, plan);
myImmediatePainter.paint(graphics, plan);
}
void processKeyTypedNormally(char c, DataContext dataContext) {
EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext);
}
private void fireFocusLost() {
for (FocusChangeListener listener : myFocusListeners) {
listener.focusLost(this);
}
}
private void fireFocusGained() {
for (FocusChangeListener listener : myFocusListeners) {
listener.focusGained(this);
}
}
@Override
public void setHighlighter(@NotNull final EditorHighlighter highlighter) {
if (isReleased) return; // do not set highlighter to the released editor
assertIsDispatchThread();
final Document document = getDocument();
Disposer.dispose(myHighlighterDisposable);
document.addDocumentListener(highlighter);
myHighlighter = highlighter;
myHighlighterDisposable = () -> document.removeDocumentListener(highlighter);
Disposer.register(myDisposable, myHighlighterDisposable);
highlighter.setEditor(this);
highlighter.setText(document.getImmutableCharSequence());
if (!(highlighter instanceof EmptyEditorHighlighter)) {
EditorHighlighterCache.rememberEditorHighlighterForCachesOptimization(document, highlighter);
}
if (myPanel != null) {
reinitSettings();
}
}
@NotNull
@Override
public EditorHighlighter getHighlighter() {
assertReadAccess();
return myHighlighter;
}
@Override
@NotNull
public EditorComponentImpl getContentComponent() {
return myEditorComponent;
}
@NotNull
@Override
public EditorGutterComponentImpl getGutterComponentEx() {
return myGutterComponent;
}
@Override
public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) {
myPropertyChangeSupport.addPropertyChangeListener(listener);
}
@Override
public void addPropertyChangeListener(@NotNull final PropertyChangeListener listener, @NotNull Disposable parentDisposable) {
addPropertyChangeListener(listener);
Disposer.register(parentDisposable, () -> removePropertyChangeListener(listener));
}
@Override
public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) {
myPropertyChangeSupport.removePropertyChangeListener(listener);
}
@Override
public void setInsertMode(boolean mode) {
assertIsDispatchThread();
boolean oldValue = myIsInsertMode;
myIsInsertMode = mode;
myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, mode);
myCaretCursor.repaint();
}
@Override
public boolean isInsertMode() {
return myIsInsertMode;
}
@Override
public void setColumnMode(boolean mode) {
assertIsDispatchThread();
boolean oldValue = myIsColumnMode;
myIsColumnMode = mode;
myPropertyChangeSupport.firePropertyChange(PROP_COLUMN_MODE, oldValue, mode);
}
@Override
public boolean isColumnMode() {
return myIsColumnMode;
}
public int yToVisibleLine(int y) {
return myView.yToVisualLine(y);
}
@Override
@NotNull
public VisualPosition xyToVisualPosition(@NotNull Point p) {
return myView.xyToVisualPosition(p);
}
@Override
@NotNull
public VisualPosition xyToVisualPosition(@NotNull Point2D p) {
return myView.xyToVisualPosition(p);
}
@NotNull
Point2D offsetToXY(int offset, boolean leanTowardsLargerOffsets) {
return myView.offsetToXY(offset, leanTowardsLargerOffsets, false);
}
@Override
@NotNull
public VisualPosition offsetToVisualPosition(int offset) {
return offsetToVisualPosition(offset, false, false);
}
@Override
@NotNull
public VisualPosition offsetToVisualPosition(int offset, boolean leanForward, boolean beforeSoftWrap) {
return myView.offsetToVisualPosition(offset, leanForward, beforeSoftWrap);
}
@Override
@NotNull
public LogicalPosition offsetToLogicalPosition(int offset) {
return myView.offsetToLogicalPosition(offset);
}
@TestOnly
public void setCaretActive() {
synchronized (ourCaretBlinkingCommand) {
ourCaretBlinkingCommand.myEditor = this;
}
}
// optimization: do not do column calculations here since we are interested in line number only
public int offsetToVisualLine(int offset) {
return myView.offsetToVisualLine(offset, false);
}
public int visualLineStartOffset(int visualLine) {
return myView.visualLineToOffset(visualLine);
}
@Override
@NotNull
public LogicalPosition xyToLogicalPosition(@NotNull Point p) {
Point pp = p.x >= 0 && p.y >= 0 ? p : new Point(Math.max(p.x, 0), Math.max(p.y, 0));
return visualToLogicalPosition(xyToVisualPosition(pp));
}
private int logicalLineToY(int line) {
int visualLine = line < myDocument.getLineCount()
? offsetToVisualLine(myDocument.getLineStartOffset(line))
: logicalToVisualPosition(new LogicalPosition(line, 0)).line;
return visibleLineToY(visualLine);
}
@Override
@NotNull
public Point logicalPositionToXY(@NotNull LogicalPosition pos) {
VisualPosition visible = logicalToVisualPosition(pos);
return visualPositionToXY(visible);
}
@Override
@NotNull
public Point visualPositionToXY(@NotNull VisualPosition visible) {
Point2D point2D = myView.visualPositionToXY(visible);
return new Point((int)point2D.getX(), (int)point2D.getY());
}
@Override
@NotNull
public Point2D visualPositionToPoint2D(@NotNull VisualPosition visible) {
return myView.visualPositionToXY(visible);
}
/**
* Returns how much current line height bigger than the normal (16px)
* This method is used to scale editors elements such as gutter icons, folding elements, and others
*/
public float getScale() {
if (!Registry.is("editor.scale.gutter.icons")) return 1f;
float normLineHeight = getLineHeight() / myScheme.getLineSpacing(); // normalized, as for 1.0f line spacing
return normLineHeight / JBUI.scale(16f);
}
public int findNearestDirectionBoundary(int offset, boolean lookForward) {
return myView.findNearestDirectionBoundary(offset, lookForward);
}
public int visibleLineToY(int line) {
return myView.visualLineToY(line);
}
@Override
public void repaint(int startOffset, int endOffset) {
repaint(startOffset, endOffset, true);
}
void repaint(int startOffset, int endOffset, boolean invalidateTextLayout) {
if (myDocument.isInBulkUpdate()) {
return;
}
assertIsDispatchThread();
endOffset = Math.min(endOffset, myDocument.getTextLength());
if (invalidateTextLayout) {
myView.invalidateRange(startOffset, endOffset);
}
if (!isShowing()) {
return;
}
// We do repaint in case of equal offsets because there is a possible case that there is a soft wrap at the same offset and
// it does occupy particular amount of visual space that may be necessary to repaint.
if (startOffset <= endOffset) {
int startLine = myDocument.getLineNumber(startOffset);
int endLine = myDocument.getLineNumber(endOffset);
repaintLines(startLine, endLine);
}
}
private boolean isShowing() {
return myGutterComponent.isShowing();
}
private void repaintToScreenBottom(int startLine) {
Rectangle visibleArea = getScrollingModel().getVisibleArea();
int yStartLine = logicalLineToY(startLine);
int yEndLine = visibleArea.y + visibleArea.height;
myEditorComponent.repaintEditorComponent(visibleArea.x, yStartLine, visibleArea.x + visibleArea.width, yEndLine - yStartLine);
myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine);
((EditorMarkupModelImpl)getMarkupModel()).repaint(-1, -1);
}
/**
* Asks to repaint all logical lines from the given {@code [start; end]} range.
*
* @param startLine start logical line to repaint (inclusive)
* @param endLine end logical line to repaint (inclusive)
*/
private void repaintLines(int startLine, int endLine) {
if (!isShowing()) return;
Rectangle visibleArea = getScrollingModel().getVisibleArea();
int yStartLine = logicalLineToY(startLine);
int endVisLine = myDocument.getTextLength() <= 0 ? 0 : offsetToVisualLine(myDocument.getLineEndOffset(Math.min(myDocument.getLineCount() - 1, endLine)));
int height = endVisLine * getLineHeight() - yStartLine + getLineHeight() + 2;
myEditorComponent.repaintEditorComponent(visibleArea.x, yStartLine, visibleArea.x + visibleArea.width, height);
myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), height);
}
private void bulkUpdateStarted() {
myView.getPreferredSize(); // make sure size is calculated (in case it will be required while bulk mode is active)
myScrollingModel.onBulkDocumentUpdateStarted();
saveCaretRelativePosition();
myCaretModel.onBulkDocumentUpdateStarted();
mySoftWrapModel.onBulkDocumentUpdateStarted();
myFoldingModel.onBulkDocumentUpdateStarted();
}
private void bulkUpdateFinished() {
myFoldingModel.onBulkDocumentUpdateFinished();
mySoftWrapModel.onBulkDocumentUpdateFinished();
myView.reset();
myCaretModel.onBulkDocumentUpdateFinished();
setMouseSelectionState(MOUSE_SELECTION_STATE_NONE);
validateSize();
updateGutterSize();
repaintToScreenBottom(0);
updateCaretCursor();
if (!Boolean.TRUE.equals(getUserData(DISABLE_CARET_POSITION_KEEPING))) {
restoreCaretRelativePosition();
}
}
private void beforeChangedUpdate() {
ApplicationManager.getApplication().assertIsDispatchThread();
myDocumentChangeInProgress = true;
if (isStickySelection()) {
setStickySelection(false);
}
if (myDocument.isInBulkUpdate()) {
// Assuming that the job is done at bulk listener callback methods.
return;
}
saveCaretRelativePosition();
}
void invokeDelayedErrorStripeRepaint() {
if (myErrorStripeNeedsRepaint) {
myMarkupModel.repaint(-1, -1);
myErrorStripeNeedsRepaint = false;
}
}
private void changedUpdate(DocumentEvent e) {
myDocumentChangeInProgress = false;
if (myDocument.isInBulkUpdate()) return;
if (myErrorStripeNeedsRepaint) {
myMarkupModel.repaint(e.getOffset(), e.getOffset() + e.getNewLength());
myErrorStripeNeedsRepaint = false;
}
setMouseSelectionState(MOUSE_SELECTION_STATE_NONE);
validateSize();
int startLine = offsetToLogicalLine(e.getOffset());
int endLine = offsetToLogicalLine(e.getOffset() + e.getNewLength());
boolean painted = false;
if (myDocument.getTextLength() > 0) {
if (startLine != endLine || StringUtil.indexOf(e.getOldFragment(), '\n') != -1) {
myGutterComponent.clearLineToGutterRenderersCache();
}
if (countLineFeeds(e.getOldFragment()) != countLineFeeds(e.getNewFragment())) {
// Lines removed. Need to repaint till the end of the screen
repaintToScreenBottom(startLine);
painted = true;
}
}
updateCaretCursor();
if (!painted) {
repaintLines(startLine, endLine);
}
if (!Boolean.TRUE.equals(getUserData(DISABLE_CARET_POSITION_KEEPING)) &&
(getCaretModel().getOffset() < e.getOffset() || getCaretModel().getOffset() > e.getOffset() + e.getNewLength())) {
restoreCaretRelativePosition();
}
}
public void hideCursor() {
if (!myIsViewer && Registry.is("ide.hide.cursor.when.typing") && EMPTY_CURSOR != null && EMPTY_CURSOR != myEditorComponent.getCursor()) {
myEditorComponent.setCursor(EMPTY_CURSOR);
}
}
private void saveCaretRelativePosition() {
Rectangle visibleArea = getScrollingModel().getVisibleArea();
Point pos = visualPositionToXY(getCaretModel().getVisualPosition());
myCaretUpdateVShift = pos.y - visibleArea.y;
}
private void restoreCaretRelativePosition() {
Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition());
int scrollOffset = caretLocation.y - myCaretUpdateVShift;
getScrollingModel().disableAnimation();
getScrollingModel().scrollVertically(scrollOffset);
getScrollingModel().enableAnimation();
}
public boolean isScrollToCaret() {
return myScrollToCaret;
}
public void setScrollToCaret(boolean scrollToCaret) {
myScrollToCaret = scrollToCaret;
}
@NotNull
public Disposable getDisposable() {
return myDisposable;
}
private static int countLineFeeds(@NotNull CharSequence c) {
return StringUtil.countNewLines(c);
}
private boolean updatingSize; // accessed from EDT only
private void updateGutterSize() {
assertIsDispatchThread();
if (!updatingSize) {
updatingSize = true;
ApplicationManager.getApplication().invokeLater(() -> {
try {
if (!isDisposed()) {
myGutterComponent.updateSizeOnShowNotify();
}
}
finally {
updatingSize = false;
}
}, ModalityState.any(), __ -> isDisposed());
}
}
void validateSize() {
if (isReleased) return;
Dimension dim = getPreferredSize();
if (!dim.equals(myPreferredSize) && !myDocument.isInBulkUpdate()) {
dim = mySizeAdjustmentStrategy.adjust(dim, myPreferredSize, this);
if (!dim.equals(myPreferredSize)) {
myPreferredSize = dim;
updateGutterSize();
myEditorComponent.setSize(dim);
myEditorComponent.fireResized();
myMarkupModel.recalcEditorDimensions();
myMarkupModel.repaint(-1, -1);
}
}
}
void recalculateSizeAndRepaint() {
validateSize();
myEditorComponent.repaintEditorComponent();
}
@Override
@NotNull
public DocumentEx getDocument() {
return myDocument;
}
@Override
@NotNull
public JComponent getComponent() {
return myPanel;
}
@Override
public void addEditorMouseListener(@NotNull EditorMouseListener listener) {
myMouseListeners.add(listener);
}
@Override
public void removeEditorMouseListener(@NotNull EditorMouseListener listener) {
boolean success = myMouseListeners.remove(listener);
LOG.assertTrue(success || isReleased);
}
@Override
public void addEditorMouseMotionListener(@NotNull EditorMouseMotionListener listener) {
myMouseMotionListeners.add(listener);
}
@Override
public void removeEditorMouseMotionListener(@NotNull EditorMouseMotionListener listener) {
boolean success = myMouseMotionListeners.remove(listener);
LOG.assertTrue(success || isReleased);
}
@Override
public boolean isStickySelection() {
return myStickySelection;
}
@Override
public void setStickySelection(boolean enable) {
myStickySelection = enable;
if (enable) {
myStickySelectionStart = getCaretModel().getOffset();
}
else {
mySelectionModel.removeSelection();
}
}
@Override
public boolean isDisposed() {
return isReleased;
}
public void stopDumbLater() {
if (ApplicationManager.getApplication().isUnitTestMode()) return;
ApplicationManager.getApplication().invokeLater(this::stopDumb, ModalityState.current(), __ -> isDisposed());
}
private void stopDumb() {
putUserData(BUFFER, null);
}
/**
* {@link #stopDumbLater} or {@link #stopDumb} must be performed in finally
*/
public void startDumb() {
if (ApplicationManager.getApplication().isUnitTestMode()) return;
Rectangle rect = ((JViewport)myEditorComponent.getParent()).getViewRect();
BufferedImage image = UIUtil.createImage(myEditorComponent, rect.width, rect.height, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = image.createGraphics();
graphics.translate(-rect.x, -rect.y);
graphics.setClip(rect.x, rect.y, rect.width, rect.height);
myEditorComponent.paintComponent(graphics);
graphics.dispose();
putUserData(BUFFER, image);
}
void paint(@NotNull Graphics2D g) {
Rectangle clip = g.getClipBounds();
if (clip == null) {
return;
}
if (Registry.is("editor.dumb.mode.available")) {
final BufferedImage buffer = getUserData(BUFFER);
if (buffer != null) {
final Rectangle rect = getContentComponent().getVisibleRect();
UIUtil.drawImage(g, buffer, null, rect.x, rect.y);
return;
}
}
if (isReleased) {
g.setColor(getDisposedBackground());
g.fillRect(clip.x, clip.y, clip.width, clip.height);
return;
}
if (myUpdateCursor) {
setCursorPosition();
myUpdateCursor = false;
}
if (myProject != null && myProject.isDisposed()) return;
myView.paint(g);
}
Color getDisposedBackground() {
return new JBColor(new Color(128, 255, 128), new Color(128, 255, 128));
}
@NotNull
@Override
public IndentsModel getIndentsModel() {
return myIndentsModel;
}
@Override
public void setHeaderComponent(JComponent header) {
myHeaderPanel.removeAll();
header = header == null ? getPermanentHeaderComponent() : header;
if (header != null) {
myHeaderPanel.add(header);
}
myHeaderPanel.revalidate();
}
@Override
public boolean hasHeaderComponent() {
JComponent header = getHeaderComponent();
return header != null && header != getPermanentHeaderComponent();
}
@Override
@Nullable
public JComponent getPermanentHeaderComponent() {
return getUserData(PERMANENT_HEADER);
}
@Override
public void setPermanentHeaderComponent(@Nullable JComponent component) {
putUserData(PERMANENT_HEADER, component);
}
@Override
@Nullable
public JComponent getHeaderComponent() {
if (myHeaderPanel.getComponentCount() > 0) {
return (JComponent)myHeaderPanel.getComponent(0);
}
return null;
}
@Override
public void setBackgroundColor(Color color) {
myScrollPane.setBackground(color);
if (getBackgroundIgnoreForced().equals(color)) {
myForcedBackground = null;
return;
}
myForcedBackground = color;
}
@NotNull
Color getForegroundColor() {
return myScheme.getDefaultForeground();
}
@NotNull
@Override
public Color getBackgroundColor() {
if (myForcedBackground != null) return myForcedBackground;
return getBackgroundIgnoreForced();
}
@NotNull
@Override
public TextDrawingCallback getTextDrawingCallback() {
return myTextDrawingCallback;
}
@Override
public void setPlaceholder(@Nullable CharSequence text) {
myPlaceholderText = text;
}
@Override
public void setPlaceholderAttributes(@Nullable TextAttributes attributes) {
myPlaceholderAttributes = attributes;
}
public TextAttributes getPlaceholderAttributes() {
return myPlaceholderAttributes;
}
public CharSequence getPlaceholder() {
return myPlaceholderText;
}
@Override
public void setShowPlaceholderWhenFocused(boolean show) {
myShowPlaceholderWhenFocused = show;
}
public boolean getShowPlaceholderWhenFocused() {
return myShowPlaceholderWhenFocused;
}
Color getBackgroundColor(@NotNull final TextAttributes attributes) {
final Color attrColor = attributes.getBackgroundColor();
return Comparing.equal(attrColor, myScheme.getDefaultBackground()) ? getBackgroundColor() : attrColor;
}
@NotNull
private Color getBackgroundIgnoreForced() {
Color color = myScheme.getDefaultBackground();
if (myDocument.isWritable()) {
return color;
}
Color readOnlyColor = myScheme.getColor(EditorColors.READONLY_BACKGROUND_COLOR);
return readOnlyColor != null ? readOnlyColor : color;
}
@Nullable
public TextRange getComposedTextRange() {
return myInputMethodRequestsHandler == null || myInputMethodRequestsHandler.composedText == null ? null : myInputMethodRequestsHandler.composedTextRange;
}
@Override
public int getMaxWidthInRange(int startOffset, int endOffset) {
return myView.getMaxWidthInRange(startOffset, endOffset);
}
public boolean isPaintSelection() {
return myPaintSelection || !isOneLineMode() || IJSwingUtilities.hasFocus(getContentComponent());
}
public void setPaintSelection(boolean paintSelection) {
myPaintSelection = paintSelection;
}
@Override
@NotNull
@NonNls
public String dumpState() {
return "allow caret inside tab: " +
mySettings.isCaretInsideTabs() +
", allow caret after line end: " +
mySettings.isVirtualSpace() +
", soft wraps: " +
(mySoftWrapModel.isSoftWrappingEnabled() ? "on" : "off") +
", caret model: " +
getCaretModel().dumpState() +
", soft wraps data: " +
getSoftWrapModel().dumpState() +
"\n\nfolding data: " +
getFoldingModel().dumpState() +
(myDocument instanceof DocumentImpl ? "\n\ndocument info: " + ((DocumentImpl)myDocument).dumpState() : "") +
"\nfont preferences: " +
myScheme.getFontPreferences() +
"\npure painting mode: " +
myPurePaintingMode +
"\ninsets: " +
myEditorComponent.getInsets() +
(myView == null ? "" : "\nview: " + myView.dumpState());
}
@Nullable
public CaretRectangle[] getCaretLocations(boolean onlyIfShown) {
return myCaretCursor.getCaretLocations(onlyIfShown);
}
public int getAscent() {
return myView.getAscent();
}
@Override
public int getLineHeight() {
return myView.getLineHeight();
}
public int getDescent() {
return myView.getDescent();
}
@NotNull
public FontMetrics getFontMetrics(@JdkConstants.FontStyle int fontType) {
EditorFontType ft;
if (fontType == Font.PLAIN) {
ft = EditorFontType.PLAIN;
}
else if (fontType == Font.BOLD) {
ft = EditorFontType.BOLD;
}
else if (fontType == Font.ITALIC) {
ft = EditorFontType.ITALIC;
}
else if (fontType == (Font.BOLD | Font.ITALIC)) {
ft = EditorFontType.BOLD_ITALIC;
}
else {
LOG.error("Unknown font type: " + fontType);
ft = EditorFontType.PLAIN;
}
return myEditorComponent.getFontMetrics(myScheme.getFont(ft));
}
public int getPreferredHeight() {
return isReleased ? 0 : myView.getPreferredHeight();
}
public Dimension getPreferredSize() {
return isReleased
? new Dimension()
: Registry.is("idea.true.smooth.scrolling.dynamic.scrollbars")
? new Dimension(getPreferredWidthOfVisibleLines(), myView.getPreferredHeight())
: myView.getPreferredSize();
}
/* When idea.true.smooth.scrolling=true, this method is used to compute width of currently visible line range
rather than width of the whole document.
As transparent scrollbars, by definition, prevent blit-acceleration of scrolling, and we really need blit-acceleration
because not all hardware can render pixel-by-pixel scrolling with acceptable FPS without it (we now have 4K-5K displays, you know).
To have both the hardware acceleration and the transparent scrollbars we need to completely redesign JViewport machinery to support
independent layers, which is (probably) possible, but it's a rather cumbersome task.
Another approach is to make scrollbars opaque, but only in the editor (as editor is a slow-to-draw component with large screen area).
This is what "true smooth scrolling" option currently does. Interestingly, making the vertical scrollbar opaque might actually be
a good thing because on modern displays (size, aspect ratio) code rarely extends beyond the right screen edge, and even
when it does, its mixing with the navigation bar only reduces intelligibility of both the navigation bar and the code itself.
Horizontal scrollbar is another story - a single long line of text forces horizontal scrollbar in the whole document,
and in that case "transparent" scrollbar has some merits. However, instead of using transparency, we can hide horizontal
scrollbar altogether when it's not needed for currently visible content. In a sense, this approach is superior,
as even "transparent" scrollbar is only semi-transparent (thus we may prefer "on-demand" scrollbar in the general case).
Hiding the horizontal scrollbar also solves another issue - when both scrollbars are visible, vertical scrolling with
a high-precision touchpad can result in unintentional horizontal shifts (because of the touchpad sensitivity).
When visible content fully fits horizontally (i.e. in most cases), hiding the unneeded scrollbar
reliably prevents the horizontal "jitter".
Keep in mind that this functionality is experimental and may need more polishing.
In principle, we can apply this method to other components by defining, for example,
VariableWidth interface and supporting it in JBScrollPane. */
private int getPreferredWidthOfVisibleLines() {
Rectangle area = getScrollingModel().getVisibleArea();
VisualPosition begin = xyToVisualPosition(area.getLocation());
VisualPosition end = xyToVisualPosition(new Point(area.x + area.width, area.y + area.height));
return Math.max(myView.getPreferredWidth(begin.line, end.line), getScrollingWidth());
}
/* Returns the width of current horizontal scrolling state.
Complements the getPreferredWidthOfVisibleLines() method to allows to retain horizontal
scrolling position that is beyond the width of currently visible lines. */
private int getScrollingWidth() {
JScrollBar scrollbar = myScrollPane.getHorizontalScrollBar();
if (scrollbar != null) {
BoundedRangeModel model = scrollbar.getModel();
if (model != null) {
return model.getValue() + model.getExtent();
}
}
return 0;
}
@NotNull
@Override
public Dimension getContentSize() {
return myView.getPreferredSize();
}
@NotNull
@Override
public JScrollPane getScrollPane() {
return myScrollPane;
}
@Override
public void setBorder(Border border) {
myScrollPane.setBorder(border);
}
@Override
public Insets getInsets() {
return myScrollPane.getInsets();
}
@Override
public int logicalPositionToOffset(@NotNull LogicalPosition pos) {
return myView.logicalPositionToOffset(pos);
}
/**
* @return information about total number of lines that can be viewed by user. I.e. this is a number of all document
* lines (considering that single logical document line may be represented on multiple visual lines because of
* soft wraps appliance) minus number of folded lines
*/
public int getVisibleLineCount() {
return getVisibleLogicalLinesCount() + getSoftWrapModel().getSoftWrapsIntroducedLinesNumber();
}
/**
* @return number of visible logical lines. Generally, that is a total logical lines number minus number of folded lines
*/
private int getVisibleLogicalLinesCount() {
return getDocument().getLineCount() - myFoldingModel.getTotalNumberOfFoldedLines();
}
@Override
@NotNull
public VisualPosition logicalToVisualPosition(@NotNull LogicalPosition logicalPos) {
return myView.logicalToVisualPosition(logicalPos, false);
}
@Override
@NotNull
public LogicalPosition visualToLogicalPosition(@NotNull VisualPosition visiblePos) {
return myView.visualToLogicalPosition(visiblePos);
}
private int offsetToLogicalLine(int offset) {
int textLength = myDocument.getTextLength();
if (textLength == 0) return 0;
if (offset > textLength || offset < 0) {
throw new IndexOutOfBoundsException("Wrong offset: " + offset + " textLength: " + textLength);
}
int lineIndex = myDocument.getLineNumber(offset);
LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount());
return lineIndex;
}
@Override
public int calcColumnNumber(int offset, int lineIndex) {
return myView.offsetToLogicalPosition(offset).column;
}
private VisualPosition getTargetPosition(int x, int y, boolean trimToLineWidth) {
if (myDocument.getLineCount() == 0) {
return new VisualPosition(0, 0);
}
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
int visualLineCount = getVisibleLineCount();
if (yToVisibleLine(y) >= visualLineCount) {
y = visibleLineToY(Math.max(0, visualLineCount - 1));
}
VisualPosition visualPosition = xyToVisualPosition(new Point(x, y));
if (trimToLineWidth && !mySettings.isVirtualSpace()) {
LogicalPosition logicalPosition = visualToLogicalPosition(visualPosition);
LogicalPosition lineEndPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logicalPosition.line));
if (logicalPosition.column > lineEndPosition.column) {
visualPosition = logicalToVisualPosition(lineEndPosition.leanForward(true));
}
else if (mySoftWrapModel.isInsideSoftWrap(visualPosition)) {
VisualPosition beforeSoftWrapPosition = myView.logicalToVisualPosition(logicalPosition, true);
if (visualPosition.line == beforeSoftWrapPosition.line) {
visualPosition = beforeSoftWrapPosition;
}
else {
visualPosition = myView.logicalToVisualPosition(logicalPosition, false);
}
}
}
return visualPosition;
}
private boolean checkIgnore(@NotNull MouseEvent e) {
if (!myIgnoreMouseEventsConsecutiveToInitial) {
myInitialMouseEvent = null;
return false;
}
if (myInitialMouseEvent != null && (e.getComponent() != myInitialMouseEvent.getComponent() || !e.getPoint().equals(myInitialMouseEvent.getPoint()))) {
myIgnoreMouseEventsConsecutiveToInitial = false;
myInitialMouseEvent = null;
return false;
}
myIgnoreMouseEventsConsecutiveToInitial = false;
myInitialMouseEvent = null;
e.consume();
return true;
}
private void processMouseReleased(@NotNull MouseEvent e) {
if (checkIgnore(e)) return;
if (e.getSource() == myGutterComponent && !(myMousePressedEvent != null && myMousePressedEvent.isConsumed())) {
myGutterComponent.mouseReleased(e);
}
if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) {
return;
}
// if (myMousePressedInsideSelection) getSelectionModel().removeSelection();
final FoldRegion region = getFoldingModel().getFoldingPlaceholderAt(e.getPoint());
if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) {
getFoldingModel().runBatchFoldingOperation(() -> {
myFoldingModel.flushCaretShift();
region.setExpanded(true);
});
// The call below is performed because gutter's height is not updated sometimes, i.e. it sticks to the value that corresponds
// to the situation when fold region is collapsed. That causes bottom of the gutter to not be repainted and that looks really ugly.
myGutterComponent.updateSize();
}
// The general idea is to check if the user performed 'caret position change click' (left click most of the time) inside selection
// and, in the case of the positive answer, clear selection. Please note that there is a possible case that mouse click
// is performed inside selection but it triggers context menu. We don't want to drop the selection then.
if (myMousePressedEvent != null &&
myMousePressedEvent.getClickCount() == 1 &&
myMousePressedInsideSelection &&
!myMousePressedEvent.isShiftDown() &&
!myMousePressedEvent.isPopupTrigger() &&
!isToggleCaretEvent(myMousePressedEvent) &&
!isCreateRectangularSelectionEvent(myMousePressedEvent)) {
getSelectionModel().removeSelection();
}
}
@NotNull
@Override
public DataContext getDataContext() {
return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent()));
}
@NotNull
private DataContext getProjectAwareDataContext(@NotNull final DataContext original) {
if (CommonDataKeys.PROJECT.getData(original) == myProject) return original;
return dataId -> {
if (CommonDataKeys.PROJECT.is(dataId)) {
return myProject;
}
return original.getData(dataId);
};
}
private boolean isInsideGutterWhitespaceArea(@NotNull MouseEvent e) {
EditorMouseEventArea area = getMouseEventArea(e);
return area == EditorMouseEventArea.FOLDING_OUTLINE_AREA && myGutterComponent.convertX(e.getX()) > myGutterComponent.getWhitespaceSeparatorOffset();
}
@Override
public EditorMouseEventArea getMouseEventArea(@NotNull MouseEvent e) {
if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA;
int x = myGutterComponent.convertX(e.getX());
return myGutterComponent.getEditorMouseAreaByOffset(x);
}
private void requestFocus() {
final IdeFocusManager focusManager = IdeFocusManager.getInstance(myProject);
if (focusManager.getFocusOwner() != myEditorComponent) { //IDEA-64501
focusManager.requestFocus(myEditorComponent, true);
}
}
private void validateMousePointer(@NotNull MouseEvent e) {
if (e.getSource() == myGutterComponent) {
myGutterComponent.validateMousePointer(e);
}
else {
myGutterComponent.setActiveFoldRegion(null);
if (getSelectionModel().hasSelection() && (e.getModifiersEx() & (InputEvent.BUTTON1_DOWN_MASK | InputEvent.BUTTON2_DOWN_MASK)) == 0) {
int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint()));
if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) {
myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
return;
}
}
if (!IdeGlassPaneImpl.hasPreProcessedCursor(myEditorComponent)) {
myEditorComponent.setCursor(UIUtil.getTextCursor(getBackgroundColor()));
}
}
}
private void runMouseDraggedCommand(@NotNull final MouseEvent e) {
if (myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) {
return;
}
myCommandProcessor.executeCommand(myProject, () -> processMouseDragged(e), "", MOUSE_DRAGGED_GROUP, UndoConfirmationPolicy.DEFAULT, getDocument());
}
private void processMouseDragged(@NotNull MouseEvent e) {
if (!JBSwingUtilities.isLeftMouseButton(e) && !JBSwingUtilities.isMiddleMouseButton(e)) {
return;
}
EditorMouseEventArea eventArea = getMouseEventArea(e);
if (eventArea == EditorMouseEventArea.ANNOTATIONS_AREA) return;
if (eventArea == EditorMouseEventArea.LINE_MARKERS_AREA || eventArea == EditorMouseEventArea.FOLDING_OUTLINE_AREA && !isInsideGutterWhitespaceArea(e)) {
// The general idea is that we don't want to change caret position on gutter marker area click (e.g. on setting a breakpoint)
// but do want to allow bulk selection on gutter marker mouse drag. However, when a drag is performed, the first event is
// a 'mouse pressed' event, that's why we remember target line on 'mouse pressed' processing and use that information on
// further dragging (if any).
if (myDragOnGutterSelectionStartLine >= 0) {
mySelectionModel.removeSelection();
myCaretModel.moveToOffset(myDragOnGutterSelectionStartLine < myDocument.getLineCount()
? myDocument.getLineStartOffset(myDragOnGutterSelectionStartLine)
: myDocument.getTextLength());
}
myDragOnGutterSelectionStartLine = -1;
}
boolean columnSelectionDragEvent = isColumnSelectionDragEvent(e);
boolean toggleCaretEvent = isToggleCaretEvent(e);
boolean addRectangularSelectionEvent = isAddRectangularSelectionEvent(e);
boolean columnSelectionDrag = isColumnMode() && !myLastPressCreatedCaret || columnSelectionDragEvent;
if (!columnSelectionDragEvent && toggleCaretEvent && !myLastPressCreatedCaret) {
return; // ignoring drag after removing a caret
}
Rectangle visibleArea = getScrollingModel().getVisibleArea();
int x = e.getX();
if (e.getSource() == myGutterComponent) {
x = 0;
}
int dx = 0;
if (x < visibleArea.x && visibleArea.x > 0) {
dx = x - visibleArea.x;
}
else {
if (x > visibleArea.x + visibleArea.width) {
dx = x - visibleArea.x - visibleArea.width;
}
}
int dy = 0;
int y = e.getY();
if (y < visibleArea.y && visibleArea.y > 0) {
dy = y - visibleArea.y;
}
else {
if (y > visibleArea.y + visibleArea.height) {
dy = y - visibleArea.y - visibleArea.height;
}
}
if (dx == 0 && dy == 0) {
myScrollingTimer.stop();
SelectionModel selectionModel = getSelectionModel();
Caret leadCaret = getLeadCaret();
int oldSelectionStart = leadCaret.getLeadSelectionOffset();
VisualPosition oldVisLeadSelectionStart = leadCaret.getLeadSelectionPosition();
int oldCaretOffset = getCaretModel().getOffset();
boolean multiCaretSelection = columnSelectionDrag || toggleCaretEvent;
VisualPosition newVisualCaret = getTargetPosition(x, y, !multiCaretSelection);
LogicalPosition newLogicalCaret = visualToLogicalPosition(newVisualCaret);
if (multiCaretSelection) {
myMultiSelectionInProgress = true;
myRectangularSelectionInProgress = columnSelectionDrag || addRectangularSelectionEvent;
myTargetMultiSelectionPosition = xyToVisualPosition(new Point(Math.max(x, 0), Math.max(y, 0)));
}
else {
getCaretModel().moveToVisualPosition(newVisualCaret);
}
int newCaretOffset = getCaretModel().getOffset();
newVisualCaret = getCaretModel().getVisualPosition();
int caretShift = newCaretOffset - mySavedSelectionStart;
if (myMousePressedEvent != null &&
getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA &&
getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.LINE_NUMBERS_AREA) {
selectionModel.setSelection(oldSelectionStart, newCaretOffset);
}
else {
if (multiCaretSelection) {
if (myLastMousePressedLocation != null && (myCurrentDragIsSubstantial || !newLogicalCaret.equals(myLastMousePressedLocation))) {
createSelectionTill(newLogicalCaret);
blockActionsIfNeeded(e, myLastMousePressedLocation, newLogicalCaret);
}
}
else {
if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) {
if (caretShift < 0) {
int newSelection = newCaretOffset;
if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) {
newSelection = myCaretModel.getWordAtCaretStart();
}
else {
if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) {
newSelection = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)));
}
}
if (newSelection < 0) newSelection = newCaretOffset;
selectionModel.setSelection(mySavedSelectionEnd, newSelection);
getCaretModel().moveToOffset(newSelection);
}
else {
int newSelection = newCaretOffset;
if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) {
newSelection = myCaretModel.getWordAtCaretEnd();
}
else {
if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) {
newSelection = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)));
}
}
if (newSelection < 0) newSelection = newCaretOffset;
selectionModel.setSelection(mySavedSelectionStart, newSelection);
getCaretModel().moveToOffset(newSelection);
}
cancelAutoResetForMouseSelectionState();
return;
}
if (!myMousePressedInsideSelection) {
// There is a possible case that lead selection position should be adjusted in accordance with the mouse move direction.
// E.g. consider situation when user selects the whole line by clicking at 'line numbers' area. 'Line end' is considered
// to be lead selection point then. However, when mouse is dragged down we want to consider 'line start' to be
// lead selection point.
if ((myMousePressArea == EditorMouseEventArea.LINE_NUMBERS_AREA || myMousePressArea == EditorMouseEventArea.LINE_MARKERS_AREA) &&
selectionModel.hasSelection()) {
if (newCaretOffset >= selectionModel.getSelectionEnd()) {
oldSelectionStart = selectionModel.getSelectionStart();
oldVisLeadSelectionStart = selectionModel.getSelectionStartPosition();
}
else if (newCaretOffset <= selectionModel.getSelectionStart()) {
oldSelectionStart = selectionModel.getSelectionEnd();
oldVisLeadSelectionStart = selectionModel.getSelectionEndPosition();
}
}
if (oldVisLeadSelectionStart != null) {
setSelectionAndBlockActions(e, oldVisLeadSelectionStart, oldSelectionStart, newVisualCaret, newCaretOffset);
}
else {
setSelectionAndBlockActions(e, oldSelectionStart, newCaretOffset);
}
cancelAutoResetForMouseSelectionState();
}
else {
if (caretShift != 0) {
if (myMousePressedEvent != null) {
if (mySettings.isDndEnabled()) {
boolean isCopy = UIUtil.isControlKeyDown(e) || isViewer() || !getDocument().isWritable();
mySavedCaretOffsetForDNDUndoHack = oldCaretOffset;
getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, isCopy ? TransferHandler.COPY : TransferHandler.MOVE);
}
else {
selectionModel.removeSelection();
}
}
}
}
}
}
}
else {
myScrollingTimer.start(dx, dy);
onSubstantialDrag(e);
}
}
private void clearDnDContext() {
if (myDraggedRange != null) {
myDraggedRange.dispose();
myDraggedRange = null;
}
myGutterComponent.myDnDInProgress = false;
}
private void createSelectionTill(@NotNull LogicalPosition targetPosition) {
List<CaretState> caretStates = new ArrayList<>(myCaretStateBeforeLastPress);
if (myRectangularSelectionInProgress) {
caretStates.addAll(EditorModificationUtil.calcBlockSelectionState(this, myLastMousePressedLocation, targetPosition));
}
else {
LogicalPosition selectionStart = myLastMousePressedLocation;
LogicalPosition selectionEnd = targetPosition;
if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) {
int newCaretOffset = logicalPositionToOffset(targetPosition);
if (newCaretOffset < mySavedSelectionStart) {
selectionStart = offsetToLogicalPosition(mySavedSelectionEnd);
if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) {
targetPosition = selectionEnd = visualToLogicalPosition(new VisualPosition(offsetToVisualLine(newCaretOffset), 0));
}
}
else {
selectionStart = offsetToLogicalPosition(mySavedSelectionStart);
int selectionEndOffset = Math.max(newCaretOffset, mySavedSelectionEnd);
if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) {
targetPosition = selectionEnd = offsetToLogicalPosition(selectionEndOffset);
}
else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) {
targetPosition = selectionEnd = visualToLogicalPosition(new VisualPosition(offsetToVisualLine(selectionEndOffset) + 1, 0));
}
}
cancelAutoResetForMouseSelectionState();
}
caretStates.add(new CaretState(targetPosition, selectionStart, selectionEnd));
}
myCaretModel.setCaretsAndSelections(caretStates);
}
private Caret getLeadCaret() {
List<Caret> allCarets = myCaretModel.getAllCarets();
Caret firstCaret = allCarets.get(0);
if (firstCaret == myCaretModel.getPrimaryCaret()) {
return allCarets.get(allCarets.size() - 1);
}
return firstCaret;
}
private void setSelectionAndBlockActions(@NotNull MouseEvent mouseDragEvent, int startOffset, int endOffset) {
mySelectionModel.setSelection(startOffset, endOffset);
if (myCurrentDragIsSubstantial || startOffset != endOffset) {
onSubstantialDrag(mouseDragEvent);
}
}
private void setSelectionAndBlockActions(@NotNull MouseEvent mouseDragEvent,
VisualPosition startPosition,
int startOffset,
VisualPosition endPosition,
int endOffset) {
mySelectionModel.setSelection(startPosition, startOffset, endPosition, endOffset);
if (myCurrentDragIsSubstantial || startOffset != endOffset || !Comparing.equal(startPosition, endPosition)) {
onSubstantialDrag(mouseDragEvent);
}
}
private void blockActionsIfNeeded(@NotNull MouseEvent mouseDragEvent, @NotNull LogicalPosition startPosition, @NotNull LogicalPosition endPosition) {
if (myCurrentDragIsSubstantial || !startPosition.equals(endPosition)) {
onSubstantialDrag(mouseDragEvent);
}
}
private void onSubstantialDrag(@NotNull MouseEvent mouseDragEvent) {
IdeEventQueue.getInstance().blockNextEvents(mouseDragEvent, IdeEventQueue.BlockMode.ACTIONS);
myCurrentDragIsSubstantial = true;
}
private static class RepaintCursorCommand implements Runnable {
private long mySleepTime = 500;
private boolean myIsBlinkCaret = true;
@Nullable
private EditorImpl myEditor;
private ScheduledFuture<?> mySchedulerHandle;
public void start() {
if (mySchedulerHandle != null) {
mySchedulerHandle.cancel(false);
}
mySchedulerHandle = EdtExecutorService.getScheduledExecutorInstance().scheduleWithFixedDelay(this, mySleepTime, mySleepTime, TimeUnit.MILLISECONDS);
}
private void setBlinkPeriod(int blinkPeriod) {
mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10;
start();
}
private void setBlinkCaret(boolean value) {
myIsBlinkCaret = value;
}
@Override
public void run() {
if (myEditor != null) {
CaretCursor activeCursor = myEditor.myCaretCursor;
long time = System.currentTimeMillis();
time -= activeCursor.myStartTime;
if (time > mySleepTime) {
boolean toRepaint = true;
if (myIsBlinkCaret) {
activeCursor.myIsShown = !activeCursor.myIsShown;
}
else {
toRepaint = !activeCursor.myIsShown;
activeCursor.myIsShown = true;
}
if (toRepaint) {
activeCursor.repaint();
}
}
}
}
}
void updateCaretCursor() {
myUpdateCursor = true;
}
private void setCursorPosition() {
final List<CaretRectangle> caretPoints = new ArrayList<>();
for (Caret caret : getCaretModel().getAllCarets()) {
boolean isRtl = caret.isAtRtlLocation();
VisualPosition caretPosition = caret.getVisualPosition();
Point pos1 = visualPositionToXY(caretPosition.leanRight(!isRtl));
Point pos2 = visualPositionToXY(new VisualPosition(caretPosition.line, Math.max(0, caretPosition.column + (isRtl ? -1 : 1)), isRtl));
int width = Math.abs(pos2.x - pos1.x);
if (!isRtl && myInlayModel.hasInlineElementAt(caretPosition)) {
width = Math.min(width, myView.getPlainSpaceWidth());
}
caretPoints.add(new CaretRectangle(pos1, width, caret, isRtl));
}
myCaretCursor.setPositions(caretPoints.toArray(new CaretRectangle[caretPoints.size()]));
}
@Override
public boolean setCaretVisible(boolean b) {
boolean old = myCaretCursor.isActive();
if (b) {
myCaretCursor.activate();
}
else {
myCaretCursor.passivate();
}
return old;
}
@Override
public boolean setCaretEnabled(boolean enabled) {
boolean old = myCaretCursor.isEnabled();
myCaretCursor.setEnabled(enabled);
return old;
}
@Override
public void addFocusListener(@NotNull FocusChangeListener listener) {
myFocusListeners.add(listener);
}
@Override
public void addFocusListener(@NotNull FocusChangeListener listener, @NotNull Disposable parentDisposable) {
ContainerUtil.add(listener, myFocusListeners, parentDisposable);
}
@Override
@Nullable
public Project getProject() {
return myProject;
}
@Override
public boolean isOneLineMode() {
return myIsOneLineMode;
}
@Override
public boolean isEmbeddedIntoDialogWrapper() {
return myEmbeddedIntoDialogWrapper;
}
@Override
public void setEmbeddedIntoDialogWrapper(boolean b) {
assertIsDispatchThread();
myEmbeddedIntoDialogWrapper = b;
myScrollPane.setFocusable(!b);
myEditorComponent.setFocusCycleRoot(!b);
myEditorComponent.setFocusable(b);
}
@Override
public void setOneLineMode(boolean isOneLineMode) {
myIsOneLineMode = isOneLineMode;
getScrollPane().setInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, null);
reinitSettings();
}
public static class CaretRectangle {
public final Point myPoint;
public final int myWidth;
public final Caret myCaret;
public final boolean myIsRtl;
private CaretRectangle(Point point, int width, Caret caret, boolean isRtl) {
myPoint = point;
myWidth = Math.max(width, 2);
myCaret = caret;
myIsRtl = isRtl;
}
}
class CaretCursor {
private CaretRectangle[] myLocations;
private boolean myEnabled;
@SuppressWarnings("FieldAccessedSynchronizedAndUnsynchronized")
private boolean myIsShown;
private long myStartTime;
private CaretCursor() {
myLocations = new CaretRectangle[]{new CaretRectangle(new Point(0, 0), 0, null, false)};
setEnabled(true);
}
public boolean isEnabled() {
return myEnabled;
}
public void setEnabled(boolean enabled) {
myEnabled = enabled;
}
private void activate() {
final boolean blink = mySettings.isBlinkCaret();
final int blinkPeriod = mySettings.getCaretBlinkPeriod();
synchronized (ourCaretBlinkingCommand) {
ourCaretBlinkingCommand.myEditor = EditorImpl.this;
ourCaretBlinkingCommand.setBlinkCaret(blink);
ourCaretBlinkingCommand.setBlinkPeriod(blinkPeriod);
myIsShown = true;
}
}
public boolean isActive() {
synchronized (ourCaretBlinkingCommand) {
return myIsShown;
}
}
private void passivate() {
synchronized (ourCaretBlinkingCommand) {
myIsShown = false;
}
}
private void setPositions(CaretRectangle[] locations) {
myStartTime = System.currentTimeMillis();
myLocations = locations;
myIsShown = true;
}
private void repaint() {
myView.repaintCarets();
}
@Nullable
CaretRectangle[] getCaretLocations(boolean onlyIfShown) {
if (onlyIfShown && (!isEnabled() || !myIsShown || isRendererMode() || !IJSwingUtilities.hasFocus(getContentComponent()))) return null;
return myLocations;
}
}
private class ScrollingTimer {
private Timer myTimer;
private static final int TIMER_PERIOD = 100;
private static final int CYCLE_SIZE = 20;
private int myXCycles;
private int myYCycles;
private int myDx;
private int myDy;
private int xPassedCycles;
private int yPassedCycles;
private void start(int dx, int dy) {
myDx = 0;
myDy = 0;
if (dx > 0) {
myXCycles = CYCLE_SIZE / dx + 1;
myDx = 1 + dx / CYCLE_SIZE;
}
else {
if (dx < 0) {
myXCycles = -CYCLE_SIZE / dx + 1;
myDx = -1 + dx / CYCLE_SIZE;
}
}
if (dy > 0) {
myYCycles = CYCLE_SIZE / dy + 1;
myDy = 1 + dy / CYCLE_SIZE;
}
else {
if (dy < 0) {
myYCycles = -CYCLE_SIZE / dy + 1;
myDy = -1 + dy / CYCLE_SIZE;
}
}
if (myTimer != null) {
return;
}
myTimer = UIUtil.createNamedTimer("Editor scroll timer", TIMER_PERIOD, e -> {
if (isDisposed()) {
stop();
return;
}
myCommandProcessor.executeCommand(myProject, new DocumentRunnable(myDocument, myProject) {
@Override
public void run() {
int oldSelectionStart = mySelectionModel.getLeadSelectionOffset();
VisualPosition caretPosition = myMultiSelectionInProgress ? myTargetMultiSelectionPosition : getCaretModel().getVisualPosition();
int column = caretPosition.column;
xPassedCycles++;
if (xPassedCycles >= myXCycles) {
xPassedCycles = 0;
column += myDx;
}
int line = caretPosition.line;
yPassedCycles++;
if (yPassedCycles >= myYCycles) {
yPassedCycles = 0;
line += myDy;
}
line = Math.max(0, line);
column = Math.max(0, column);
VisualPosition pos = new VisualPosition(line, column);
if (!myMultiSelectionInProgress) {
getCaretModel().moveToVisualPosition(pos);
getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
}
int newCaretOffset = getCaretModel().getOffset();
int caretShift = newCaretOffset - mySavedSelectionStart;
if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) {
if (caretShift < 0) {
int newSelection = newCaretOffset;
if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) {
newSelection = myCaretModel.getWordAtCaretStart();
}
else {
if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) {
newSelection = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)));
}
}
if (newSelection < 0) newSelection = newCaretOffset;
mySelectionModel.setSelection(validateOffset(mySavedSelectionEnd), newSelection);
getCaretModel().moveToOffset(newSelection);
}
else {
int newSelection = newCaretOffset;
if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) {
newSelection = myCaretModel.getWordAtCaretEnd();
}
else {
if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) {
newSelection = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)));
}
}
if (newSelection < 0) newSelection = newCaretOffset;
mySelectionModel.setSelection(validateOffset(mySavedSelectionStart), newSelection);
getCaretModel().moveToOffset(newSelection);
}
return;
}
if (myMultiSelectionInProgress && myLastMousePressedLocation != null) {
myTargetMultiSelectionPosition = pos;
LogicalPosition newLogicalPosition = visualToLogicalPosition(pos);
getScrollingModel().scrollTo(newLogicalPosition, ScrollType.RELATIVE);
createSelectionTill(newLogicalPosition);
}
else {
mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset());
}
}
}, EditorBundle.message("move.cursor.command.name"), DocCommandGroupId.noneGroupId(getDocument()), UndoConfirmationPolicy.DEFAULT, getDocument());
});
myTimer.start();
}
private void stop() {
if (myTimer != null) {
myTimer.stop();
myTimer = null;
}
}
private int validateOffset(int offset) {
if (offset < 0) return 0;
if (offset > myDocument.getTextLength()) return myDocument.getTextLength();
return offset;
}
}
private static final Field decrButtonField = ReflectionUtil.getDeclaredField(BasicScrollBarUI.class, "decrButton");
private static final Field incrButtonField = ReflectionUtil.getDeclaredField(BasicScrollBarUI.class, "incrButton");
class MyScrollBar extends JBScrollBar implements IdeGlassPane.TopComponent {
private Consumer<Graphics> myRepaintCallback;
private MyScrollBar(@JdkConstants.AdjustableOrientation int orientation) {
super(orientation);
}
@Override
public boolean canBePreprocessed(MouseEvent e) {
return JBScrollPane.canBePreprocessed(e, this);
}
@Override
public void setUI(ScrollBarUI ui) {
super.setUI(ui);
setOpaque(false);
}
@Override
public void paint(Graphics g) {
super.paint(g);
if (myRepaintCallback != null) {
myRepaintCallback.consume(g);
}
}
@Override
public boolean isOpaque() {
return true;
}
@Override
public int getUnitIncrement(int direction) {
JViewport vp = myScrollPane.getViewport();
Rectangle vr = vp.getViewRect();
return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction);
}
@Override
public int getBlockIncrement(int direction) {
JViewport vp = myScrollPane.getViewport();
Rectangle vr = vp.getViewRect();
return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction);
}
private void registerRepaintCallback(@Nullable Consumer<Graphics> callback) {
myRepaintCallback = callback;
}
}
private MyEditable getViewer() {
if (myEditable == null) {
myEditable = new MyEditable();
}
return myEditable;
}
@Override
public CopyProvider getCopyProvider() {
return getViewer();
}
@Override
public CutProvider getCutProvider() {
return getViewer();
}
@Override
public PasteProvider getPasteProvider() {
return getViewer();
}
@Override
public DeleteProvider getDeleteProvider() {
return getViewer();
}
private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider, DumbAware {
@Override
public void performCopy(@NotNull DataContext dataContext) {
executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext);
}
@Override
public boolean isCopyEnabled(@NotNull DataContext dataContext) {
return true;
}
@Override
public boolean isCopyVisible(@NotNull DataContext dataContext) {
return getSelectionModel().hasSelection(true);
}
@Override
public void performCut(@NotNull DataContext dataContext) {
executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext);
}
@Override
public boolean isCutEnabled(@NotNull DataContext dataContext) {
return !isViewer();
}
@Override
public boolean isCutVisible(@NotNull DataContext dataContext) {
return isCutEnabled(dataContext) && getSelectionModel().hasSelection(true);
}
@Override
public void performPaste(@NotNull DataContext dataContext) {
executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext);
}
@Override
public boolean isPastePossible(@NotNull DataContext dataContext) {
// Copy of isPasteEnabled. See interface method javadoc.
return !isViewer();
}
@Override
public boolean isPasteEnabled(@NotNull DataContext dataContext) {
return !isViewer();
}
@Override
public void deleteElement(@NotNull DataContext dataContext) {
executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext);
}
@Override
public boolean canDeleteElement(@NotNull DataContext dataContext) {
return !isViewer();
}
private void executeAction(@NotNull String actionId, @NotNull DataContext dataContext) {
EditorAction action = (EditorAction)ActionManager.getInstance().getAction(actionId);
if (action != null) {
action.actionPerformed(EditorImpl.this, dataContext);
}
}
}
@Override
public void setColorsScheme(@NotNull EditorColorsScheme scheme) {
assertIsDispatchThread();
myScheme = scheme;
reinitSettings();
}
@Override
@NotNull
public EditorColorsScheme getColorsScheme() {
return myScheme;
}
static void assertIsDispatchThread() {
ApplicationManager.getApplication().assertIsDispatchThread();
}
private static void assertReadAccess() {
ApplicationManager.getApplication().assertReadAccessAllowed();
}
@Override
public void setVerticalScrollbarOrientation(int type) {
assertIsDispatchThread();
int currentHorOffset = myScrollingModel.getHorizontalScrollOffset();
myScrollBarOrientation = type;
if (type == VERTICAL_SCROLLBAR_LEFT) {
myScrollPane.setLayout(new LeftHandScrollbarLayout());
}
else {
myScrollPane.setLayout(new ScrollPaneLayout());
}
myScrollingModel.scrollHorizontally(currentHorOffset);
}
@Override
public void setVerticalScrollbarVisible(boolean b) {
myScrollPane.setVerticalScrollBarPolicy(b ? ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS : ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
}
@Override
public void setHorizontalScrollbarVisible(boolean b) {
myScrollPane.setHorizontalScrollBarPolicy(b ? ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED : ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
}
@Override
public int getVerticalScrollbarOrientation() {
return myScrollBarOrientation;
}
public boolean isMirrored() {
return myScrollBarOrientation != EditorEx.VERTICAL_SCROLLBAR_RIGHT;
}
@NotNull
MyScrollBar getVerticalScrollBar() {
return myVerticalScrollBar;
}
JPanel getPanel() {
return myPanel;
}
@NotNull
MyScrollBar getHorizontalScrollBar() {
return (MyScrollBar)myScrollPane.getHorizontalScrollBar();
}
@MouseSelectionState
private int getMouseSelectionState() {
return myMouseSelectionState;
}
private void setMouseSelectionState(@MouseSelectionState int mouseSelectionState) {
if (getMouseSelectionState() == mouseSelectionState) return;
myMouseSelectionState = mouseSelectionState;
myMouseSelectionChangeTimestamp = System.currentTimeMillis();
myMouseSelectionStateAlarm.cancelAllRequests();
if (myMouseSelectionState != MOUSE_SELECTION_STATE_NONE) {
if (myMouseSelectionStateResetRunnable == null) {
myMouseSelectionStateResetRunnable = () -> resetMouseSelectionState(null);
}
myMouseSelectionStateAlarm.addRequest(myMouseSelectionStateResetRunnable, Registry.intValue("editor.mouseSelectionStateResetTimeout"),
ModalityState.stateForComponent(myEditorComponent));
}
}
private void resetMouseSelectionState(@Nullable MouseEvent event) {
setMouseSelectionState(MOUSE_SELECTION_STATE_NONE);
MouseEvent e = event != null ? event : myMouseMovedEvent;
if (e != null) {
validateMousePointer(e);
}
}
private void cancelAutoResetForMouseSelectionState() {
myMouseSelectionStateAlarm.cancelAllRequests();
}
void replaceInputMethodText(@NotNull InputMethodEvent e) {
if (isReleased) return;
getInputMethodRequests();
myInputMethodRequestsHandler.replaceInputMethodText(e);
}
void inputMethodCaretPositionChanged(@NotNull InputMethodEvent e) {
if (isReleased) return;
getInputMethodRequests();
myInputMethodRequestsHandler.setInputMethodCaretPosition(e);
}
@NotNull
InputMethodRequests getInputMethodRequests() {
if (myInputMethodRequestsHandler == null) {
myInputMethodRequestsHandler = new MyInputMethodHandler();
myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler);
}
return myInputMethodRequestsSwingWrapper;
}
@Override
public boolean processKeyTyped(@NotNull KeyEvent e) {
if (e.getID() != KeyEvent.KEY_TYPED) return false;
char c = e.getKeyChar();
if (UIUtil.isReallyTypedEvent(e)) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction
processKeyTyped(c);
return true;
}
else {
return false;
}
}
void beforeModalityStateChanged() {
myScrollingModel.beforeModalityStateChanged();
}
EditorDropHandler getDropHandler() {
return myDropHandler;
}
public void setDropHandler(@NotNull EditorDropHandler dropHandler) {
myDropHandler = dropHandler;
}
public void setHighlightingFilter(@Nullable Condition<RangeHighlighter> filter) {
if (myHighlightingFilter == filter) return;
Condition<RangeHighlighter> oldFilter = myHighlightingFilter;
myHighlightingFilter = filter;
for (RangeHighlighter highlighter : myDocumentMarkupModel.getDelegate().getAllHighlighters()) {
boolean oldAvailable = oldFilter == null || oldFilter.value(highlighter);
boolean newAvailable = filter == null || filter.value(highlighter);
if (oldAvailable != newAvailable) {
myMarkupModelListener
.attributesChanged((RangeHighlighterEx)highlighter, true, EditorUtil.attributesImpactFontStyleOrColor(highlighter.getTextAttributes()));
}
}
}
boolean isHighlighterAvailable(@NotNull RangeHighlighter highlighter) {
return myHighlightingFilter == null || myHighlightingFilter.value(highlighter);
}
private static class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests {
private final InputMethodRequests myDelegate;
private MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) {
myDelegate = delegate;
}
@NotNull
@Override
public Rectangle getTextLocation(final TextHitInfo offset) {
return execute(() -> myDelegate.getTextLocation(offset));
}
@Override
public TextHitInfo getLocationOffset(final int x, final int y) {
return execute(() -> myDelegate.getLocationOffset(x, y));
}
@Override
public int getInsertPositionOffset() {
return execute(myDelegate::getInsertPositionOffset);
}
@NotNull
@Override
public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, final AttributedCharacterIterator.Attribute[] attributes) {
return execute(() -> myDelegate.getCommittedText(beginIndex, endIndex, attributes));
}
@Override
public int getCommittedTextLength() {
return execute(myDelegate::getCommittedTextLength);
}
@Override
@Nullable
public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) {
return null;
}
@Override
public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) {
return execute(() -> myDelegate.getSelectedText(attributes));
}
private static <T> T execute(final Computable<T> computable) {
return UIUtil.invokeAndWaitIfNeeded(computable);
}
}
private class MyInputMethodHandler implements InputMethodRequests {
private String composedText;
private ProperTextRange composedTextRange;
@NotNull
@Override
public Rectangle getTextLocation(TextHitInfo offset) {
Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition());
Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight()));
Point p = getContentComponent().getLocationOnScreen();
r.translate(p.x, p.y);
return r;
}
@Override
@Nullable
public TextHitInfo getLocationOffset(int x, int y) {
if (composedText != null) {
Point p = getContentComponent().getLocationOnScreen();
p.x = x - p.x;
p.y = y - p.y;
int pos = logicalPositionToOffset(xyToLogicalPosition(p));
if (composedTextRange.containsOffset(pos)) {
return TextHitInfo.leading(pos - composedTextRange.getStartOffset());
}
}
return null;
}
@Override
public int getInsertPositionOffset() {
int composedStartIndex = 0;
int composedEndIndex = 0;
if (composedText != null) {
composedStartIndex = composedTextRange.getStartOffset();
composedEndIndex = composedTextRange.getEndOffset();
}
int caretIndex = getCaretModel().getOffset();
if (caretIndex < composedStartIndex) {
return caretIndex;
}
if (caretIndex < composedEndIndex) {
return composedStartIndex;
}
return caretIndex - (composedEndIndex - composedStartIndex);
}
private String getText(int startIdx, int endIdx) {
if (startIdx >= 0 && endIdx > startIdx) {
CharSequence chars = getDocument().getImmutableCharSequence();
return chars.subSequence(startIdx, endIdx).toString();
}
return "";
}
@NotNull
@Override
public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, AttributedCharacterIterator.Attribute[] attributes) {
int composedStartIndex = 0;
int composedEndIndex = 0;
if (composedText != null) {
composedStartIndex = composedTextRange.getStartOffset();
composedEndIndex = composedTextRange.getEndOffset();
}
String committed;
if (beginIndex < composedStartIndex) {
if (endIndex <= composedStartIndex) {
committed = getText(beginIndex, endIndex - beginIndex);
}
else {
int firstPartLength = composedStartIndex - beginIndex;
committed = getText(beginIndex, firstPartLength) + getText(composedEndIndex, endIndex - beginIndex - firstPartLength);
}
}
else {
committed = getText(beginIndex + composedEndIndex - composedStartIndex, endIndex - beginIndex);
}
return new AttributedString(committed).getIterator();
}
@Override
public int getCommittedTextLength() {
int length = getDocument().getTextLength();
if (composedText != null) {
length -= composedText.length();
}
return length;
}
@Override
@Nullable
public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) {
return null;
}
@Override
@Nullable
public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) {
if (myCharKeyPressed) {
myNeedToSelectPreviousChar = true;
}
String text = getSelectionModel().getSelectedText();
return text == null ? null : new AttributedString(text).getIterator();
}
private void createComposedString(int composedIndex, @NotNull AttributedCharacterIterator text) {
StringBuffer strBuf = new StringBuffer();
// create attributed string with no attributes
for (char c = text.setIndex(composedIndex); c != CharacterIterator.DONE; c = text.next()) {
strBuf.append(c);
}
composedText = new String(strBuf);
}
private void setInputMethodCaretPosition(@NotNull InputMethodEvent e) {
if (composedText != null) {
int dot = composedTextRange.getStartOffset();
TextHitInfo caretPos = e.getCaret();
if (caretPos != null) {
dot += caretPos.getInsertionIndex();
}
getCaretModel().moveToOffset(dot);
getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
}
}
private void runUndoTransparent(@NotNull final Runnable runnable) {
CommandProcessor.getInstance().runUndoTransparentAction(() -> CommandProcessor.getInstance()
.executeCommand(myProject, () -> ApplicationManager.getApplication().runWriteAction(runnable), "", getDocument(), UndoConfirmationPolicy.DEFAULT,
getDocument()));
}
private void replaceInputMethodText(@NotNull InputMethodEvent e) {
if (myNeedToSelectPreviousChar &&
SystemInfo.isMac &&
(Registry.is("ide.mac.pressAndHold.brute.workaround") ||
Registry.is("ide.mac.pressAndHold.workaround") && (e.getCommittedCharacterCount() > 0 || e.getCaret() == null))) {
// This is required to support input of accented characters using press-and-hold method (http://support.apple.com/kb/PH11264).
// JDK currently properly supports this functionality only for TextComponent/JTextComponent descendants.
// For our editor component we need this workaround.
// After https://bugs.openjdk.java.net/browse/JDK-8074882 is fixed, this workaround should be replaced with a proper solution.
myNeedToSelectPreviousChar = false;
getCaretModel().runForEachCaret(caret -> {
int caretOffset = caret.getOffset();
if (caretOffset > 0) {
caret.setSelection(caretOffset - 1, caretOffset);
}
});
}
int commitCount = e.getCommittedCharacterCount();
AttributedCharacterIterator text = e.getText();
// old composed text deletion
final Document doc = getDocument();
if (composedText != null) {
if (!isViewer() && doc.isWritable()) {
runUndoTransparent(() -> {
int docLength = doc.getTextLength();
ProperTextRange range = composedTextRange.intersection(new TextRange(0, docLength));
if (range != null) {
doc.deleteString(range.getStartOffset(), range.getEndOffset());
}
});
}
composedText = null;
}
if (text != null) {
text.first();
// committed text insertion
if (commitCount > 0) {
//noinspection ForLoopThatDoesntUseLoopVariable
for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) {
if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction
processKeyTyped(c);
}
}
}
// new composed text insertion
if (!isViewer() && doc.isWritable()) {
int composedTextIndex = text.getIndex();
if (composedTextIndex < text.getEndIndex()) {
createComposedString(composedTextIndex, text);
runUndoTransparent(() -> EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false));
composedTextRange = ProperTextRange.from(getCaretModel().getOffset(), composedText.length());
}
}
}
}
}
private class MyMouseAdapter extends MouseAdapter {
@Override
public void mousePressed(@NotNull MouseEvent e) {
requestFocus();
runMousePressedCommand(e);
}
@Override
public void mouseReleased(@NotNull MouseEvent e) {
myMousePressArea = null;
myLastMousePressedLocation = null;
runMouseReleasedCommand(e);
if (!e.isConsumed() &&
myMousePressedEvent != null &&
!myMousePressedEvent.isConsumed() &&
Math.abs(e.getX() - myMousePressedEvent.getX()) < EditorUtil.getSpaceWidth(Font.PLAIN, EditorImpl.this) &&
Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) {
runMouseClickedCommand(e);
}
}
@Override
public void mouseEntered(@NotNull MouseEvent e) {
runMouseEnteredCommand(e);
}
@Override
public void mouseExited(@NotNull MouseEvent e) {
runMouseExitedCommand(e);
EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e));
if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) {
myGutterComponent.mouseExited(e);
}
TooltipController.getInstance().cancelTooltip(FOLDING_TOOLTIP_GROUP, e, true);
}
private void runMousePressedCommand(@NotNull final MouseEvent e) {
myLastMousePressedLocation = xyToLogicalPosition(e.getPoint());
myCaretStateBeforeLastPress = isToggleCaretEvent(e) ? myCaretModel.getCaretsAndSelections() : Collections.emptyList();
myCurrentDragIsSubstantial = false;
clearDnDContext();
myMousePressedEvent = e;
EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e));
myExpectedCaretOffset = logicalPositionToOffset(myLastMousePressedLocation);
try {
for (EditorMouseListener mouseListener : myMouseListeners) {
mouseListener.mousePressed(event);
}
}
finally {
myExpectedCaretOffset = -1;
}
if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA ||
event.getArea() == EditorMouseEventArea.FOLDING_OUTLINE_AREA && !isInsideGutterWhitespaceArea(e)) {
myDragOnGutterSelectionStartLine = EditorUtil.yPositionToLogicalLine(EditorImpl.this, e);
}
// On some systems (for example on Linux) popup trigger is MOUSE_PRESSED event.
// But this trigger is always consumed by popup handler. In that case we have to
// also move caret.
if (event.isConsumed() && !(event.getMouseEvent().isPopupTrigger() || event.getArea() == EditorMouseEventArea.EDITING_AREA)) {
return;
}
if (myCommandProcessor != null) {
Runnable runnable = () -> {
if (processMousePressed(e) && myProject != null && !myProject.isDefault()) {
IdeDocumentHistory.getInstance(myProject).includeCurrentCommandAsNavigation();
}
};
myCommandProcessor.executeCommand(myProject, runnable, "", DocCommandGroupId.noneGroupId(getDocument()), UndoConfirmationPolicy.DEFAULT, getDocument());
}
else {
processMousePressed(e);
}
invokePopupIfNeeded(event);
}
private void runMouseClickedCommand(@NotNull final MouseEvent e) {
EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e));
for (EditorMouseListener listener : myMouseListeners) {
listener.mouseClicked(event);
if (event.isConsumed()) {
e.consume();
return;
}
}
}
private void runMouseReleasedCommand(@NotNull final MouseEvent e) {
myMultiSelectionInProgress = false;
myDragOnGutterSelectionStartLine = -1;
if (e.isConsumed()) {
return;
}
myScrollingTimer.stop();
EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e));
invokePopupIfNeeded(event);
if (event.isConsumed()) {
return;
}
for (EditorMouseListener listener : myMouseListeners) {
listener.mouseReleased(event);
if (event.isConsumed()) {
e.consume();
return;
}
}
if (myCommandProcessor != null) {
Runnable runnable = () -> processMouseReleased(e);
myCommandProcessor.executeCommand(myProject, runnable, "", DocCommandGroupId.noneGroupId(getDocument()), UndoConfirmationPolicy.DEFAULT, getDocument());
}
else {
processMouseReleased(e);
}
}
private void runMouseEnteredCommand(@NotNull MouseEvent e) {
EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e));
for (EditorMouseListener listener : myMouseListeners) {
listener.mouseEntered(event);
if (event.isConsumed()) {
e.consume();
return;
}
}
}
private void runMouseExitedCommand(@NotNull MouseEvent e) {
EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e));
for (EditorMouseListener listener : myMouseListeners) {
listener.mouseExited(event);
if (event.isConsumed()) {
e.consume();
return;
}
}
}
private boolean processMousePressed(@NotNull final MouseEvent e) {
myInitialMouseEvent = e;
if (myMouseSelectionState != MOUSE_SELECTION_STATE_NONE &&
System.currentTimeMillis() - myMouseSelectionChangeTimestamp > Registry.intValue("editor.mouseSelectionStateResetTimeout")) {
resetMouseSelectionState(e);
}
int x = e.getX();
int y = e.getY();
if (x < 0) x = 0;
if (y < 0) y = 0;
final EditorMouseEventArea eventArea = getMouseEventArea(e);
myMousePressArea = eventArea;
if (eventArea == EditorMouseEventArea.FOLDING_OUTLINE_AREA) {
final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y);
if (range != null) {
final boolean expansion = !range.isExpanded();
int scrollShift = y - getScrollingModel().getVerticalScrollOffset();
Runnable processor = () -> {
myFoldingModel.flushCaretShift();
range.setExpanded(expansion);
if (e.isAltDown()) {
for (FoldRegion region : myFoldingModel.getAllFoldRegions()) {
if (region.getStartOffset() >= range.getStartOffset() && region.getEndOffset() <= range.getEndOffset()) {
region.setExpanded(expansion);
}
}
}
};
getFoldingModel().runBatchFoldingOperation(processor);
y = myGutterComponent.getHeadCenterY(range);
getScrollingModel().scrollVertically(y - scrollShift);
myGutterComponent.updateSize();
validateMousePointer(e);
e.consume();
return false;
}
}
if (e.getSource() == myGutterComponent) {
if (eventArea == EditorMouseEventArea.LINE_MARKERS_AREA ||
eventArea == EditorMouseEventArea.ANNOTATIONS_AREA ||
eventArea == EditorMouseEventArea.LINE_NUMBERS_AREA) {
if (!tweakSelectionIfNecessary(e)) {
myGutterComponent.mousePressed(e);
}
if (e.isConsumed()) return false;
}
x = 0;
}
int oldSelectionStart = mySelectionModel.getLeadSelectionOffset();
final int oldStart = mySelectionModel.getSelectionStart();
final int oldEnd = mySelectionModel.getSelectionEnd();
boolean toggleCaret = e.getSource() != myGutterComponent && isToggleCaretEvent(e);
boolean lastPressCreatedCaret = myLastPressCreatedCaret;
if (e.getClickCount() == 1) {
myLastPressCreatedCaret = false;
}
// Don't move caret on mouse press above gutter line markers area (a place where break points, 'override', 'implements' etc icons
// are drawn) and annotations area. E.g. we don't want to change caret position if a user sets new break point (clicks
// at 'line markers' area).
boolean insideEditorRelatedAreas =
eventArea == EditorMouseEventArea.LINE_NUMBERS_AREA || eventArea == EditorMouseEventArea.EDITING_AREA || isInsideGutterWhitespaceArea(e);
if (insideEditorRelatedAreas) {
VisualPosition visualPosition = getTargetPosition(x, y, true);
LogicalPosition pos = visualToLogicalPosition(visualPosition);
if (toggleCaret) {
Caret caret = getCaretModel().getCaretAt(visualPosition);
if (e.getClickCount() == 1) {
if (caret == null) {
myLastPressCreatedCaret = getCaretModel().addCaret(visualPosition) != null;
}
else {
getCaretModel().removeCaret(caret);
}
}
else if (e.getClickCount() == 3 && lastPressCreatedCaret) {
getCaretModel().moveToVisualPosition(visualPosition);
}
}
else if (e.getSource() != myGutterComponent && isCreateRectangularSelectionEvent(e)) {
CaretState anchorCaretState = myCaretModel.getCaretsAndSelections().get(0);
LogicalPosition anchor = Objects.equals(anchorCaretState.getCaretPosition(), anchorCaretState.getSelectionStart())
? anchorCaretState.getSelectionEnd()
: anchorCaretState.getSelectionStart();
if (anchor == null) anchor = myCaretModel.getLogicalPosition();
mySelectionModel.setBlockSelection(anchor, pos);
}
else {
getCaretModel().removeSecondaryCarets();
getCaretModel().moveToVisualPosition(visualPosition);
}
}
if (e.isPopupTrigger()) return false;
requestFocus();
int caretOffset = getCaretModel().getOffset();
int newStart = mySelectionModel.getSelectionStart();
int newEnd = mySelectionModel.getSelectionEnd();
myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y));
myMousePressedInsideSelection =
mySelectionModel.hasSelection() && caretOffset >= mySelectionModel.getSelectionStart() && caretOffset <= mySelectionModel.getSelectionEnd();
boolean isNavigation = oldStart == oldEnd && newStart == newEnd && oldStart != newStart;
if (getMouseEventArea(e) == EditorMouseEventArea.LINE_NUMBERS_AREA && e.getClickCount() == 1) {
mySelectionModel.selectLineAtCaret();
setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED);
mySavedSelectionStart = mySelectionModel.getSelectionStart();
mySavedSelectionEnd = mySelectionModel.getSelectionEnd();
return isNavigation;
}
if (insideEditorRelatedAreas) {
if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) {
if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) {
if (caretOffset < mySavedSelectionStart) {
mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset);
}
else {
mySelectionModel.setSelection(mySavedSelectionStart, caretOffset);
}
}
else {
int startToUse = oldSelectionStart;
if (mySelectionModel.isUnknownDirection() && caretOffset > startToUse) {
startToUse = Math.min(oldStart, oldEnd);
}
mySelectionModel.setSelection(startToUse, caretOffset);
}
}
else {
if (!myMousePressedInsideSelection && getSelectionModel().hasSelection()) {
setMouseSelectionState(MOUSE_SELECTION_STATE_NONE);
mySelectionModel.setSelection(caretOffset, caretOffset);
}
else {
if (e.getButton() == MouseEvent.BUTTON1 &&
(eventArea == EditorMouseEventArea.EDITING_AREA || eventArea == EditorMouseEventArea.LINE_NUMBERS_AREA) &&
(!toggleCaret || lastPressCreatedCaret)) {
switch (e.getClickCount()) {
case 2:
selectWordAtCaret(mySettings.isMouseClickSelectionHonorsCamelWords() && mySettings.isCamelWords());
break;
case 3:
if (HONOR_CAMEL_HUMPS_ON_TRIPLE_CLICK && mySettings.isCamelWords()) {
// We want to differentiate between triple and quadruple clicks when 'select by camel humps' is on. The former
// is assumed to select 'hump' while the later points to the whole word.
selectWordAtCaret(false);
break;
}
//noinspection fallthrough
case 4:
mySelectionModel.selectLineAtCaret();
setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED);
mySavedSelectionStart = mySelectionModel.getSelectionStart();
mySavedSelectionEnd = mySelectionModel.getSelectionEnd();
mySelectionModel.setUnknownDirection(true);
break;
}
}
}
}
}
return isNavigation;
}
}
private static boolean isColumnSelectionDragEvent(@NotNull MouseEvent e) {
return e.isAltDown() && !e.isShiftDown() && !e.isControlDown() && !e.isMetaDown();
}
private static boolean isToggleCaretEvent(@NotNull MouseEvent e) {
return isMouseActionEvent(e, IdeActions.ACTION_EDITOR_ADD_OR_REMOVE_CARET) || isAddRectangularSelectionEvent(e);
}
private static boolean isAddRectangularSelectionEvent(@NotNull MouseEvent e) {
return isMouseActionEvent(e, IdeActions.ACTION_EDITOR_ADD_RECTANGULAR_SELECTION_ON_MOUSE_DRAG);
}
private static boolean isCreateRectangularSelectionEvent(@NotNull MouseEvent e) {
return isMouseActionEvent(e, IdeActions.ACTION_EDITOR_CREATE_RECTANGULAR_SELECTION);
}
private static boolean isMouseActionEvent(@NotNull MouseEvent e, String actionId) {
KeymapManager keymapManager = KeymapManager.getInstance();
if (keymapManager == null) return false;
Keymap keymap = keymapManager.getActiveKeymap();
if (keymap == null) return false;
MouseShortcut mouseShortcut = KeymapUtil.createMouseShortcut(e);
String[] mappedActions = keymap.getActionIds(mouseShortcut);
if (!ArrayUtil.contains(actionId, mappedActions)) return false;
if (mappedActions.length < 2) return true;
ActionManager actionManager = ActionManager.getInstance();
for (String mappedActionId : mappedActions) {
if (actionId.equals(mappedActionId)) continue;
AnAction action = actionManager.getAction(mappedActionId);
AnActionEvent actionEvent =
AnActionEvent.createFromAnAction(action, e, ActionPlaces.MAIN_MENU, DataManager.getInstance().getDataContext(e.getComponent()));
if (ActionUtil.lastUpdateAndCheckDumb(action, actionEvent, false)) return false;
}
return true;
}
private void selectWordAtCaret(boolean honorCamelCase) {
mySelectionModel.selectWordAtCaret(honorCamelCase);
setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED);
mySavedSelectionStart = mySelectionModel.getSelectionStart();
mySavedSelectionEnd = mySelectionModel.getSelectionEnd();
getCaretModel().moveToOffset(mySavedSelectionEnd);
}
/**
* Allows to answer if given event should tweak editor selection.
*
* @param e event for occurred mouse action
* @return {@code true} if action that produces given event will trigger editor selection change; {@code false} otherwise
*/
private boolean tweakSelectionEvent(@NotNull MouseEvent e) {
return getSelectionModel().hasSelection() &&
e.getButton() == MouseEvent.BUTTON1 &&
e.isShiftDown() &&
getMouseEventArea(e) == EditorMouseEventArea.LINE_NUMBERS_AREA;
}
/**
* Checks if editor selection should be changed because of click at the given point at gutter and proceeds if necessary.
* <p>
* The main idea is that selection can be changed during left mouse clicks on the gutter line numbers area with hold
* {@code Shift} button. The selection should be adjusted if necessary.
*
* @param e event for mouse click on gutter area
* @return {@code true} if editor's selection is changed because of the click; {@code false} otherwise
*/
private boolean tweakSelectionIfNecessary(@NotNull MouseEvent e) {
if (!tweakSelectionEvent(e)) {
return false;
}
int startSelectionOffset = getSelectionModel().getSelectionStart();
int startVisLine = offsetToVisualLine(startSelectionOffset);
int endSelectionOffset = getSelectionModel().getSelectionEnd();
int endVisLine = offsetToVisualLine(endSelectionOffset - 1);
int clickVisLine = yToVisibleLine(e.getPoint().y);
if (clickVisLine < startVisLine) {
// Expand selection at backward direction.
int startOffset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(clickVisLine, 0)));
getSelectionModel().setSelection(startOffset, endSelectionOffset);
getCaretModel().moveToOffset(startOffset);
}
else if (clickVisLine > endVisLine) {
// Expand selection at forward direction.
int endLineOffset = EditorUtil.getVisualLineEndOffset(this, clickVisLine);
getSelectionModel().setSelection(getSelectionModel().getSelectionStart(), endLineOffset);
getCaretModel().moveToOffset(endLineOffset, true);
}
else if (startVisLine == endVisLine) {
// Remove selection
getSelectionModel().removeSelection();
}
else {
// Reduce selection in backward direction.
if (getSelectionModel().getLeadSelectionOffset() == endSelectionOffset) {
if (clickVisLine == startVisLine) {
clickVisLine++;
}
int startOffset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(clickVisLine, 0)));
getSelectionModel().setSelection(startOffset, endSelectionOffset);
getCaretModel().moveToOffset(startOffset);
}
else {
// Reduce selection is forward direction.
if (clickVisLine == endVisLine) {
clickVisLine--;
}
int endLineOffset = EditorUtil.getVisualLineEndOffset(this, clickVisLine);
getSelectionModel().setSelection(startSelectionOffset, endLineOffset);
getCaretModel().moveToOffset(endLineOffset);
}
}
e.consume();
return true;
}
boolean useEditorAntialiasing() {
return myUseEditorAntialiasing;
}
public void setUseEditorAntialiasing(boolean value) {
myUseEditorAntialiasing = value;
}
private static final TooltipGroup FOLDING_TOOLTIP_GROUP = new TooltipGroup("FOLDING_TOOLTIP_GROUP", 10);
private class MyMouseMotionListener implements MouseMotionListener {
@Override
public void mouseDragged(@NotNull MouseEvent e) {
if (myDraggedRange != null || myGutterComponent.myDnDInProgress) return; // on Mac we receive events even if drag-n-drop is in progress
validateMousePointer(e);
((TransactionGuardImpl)TransactionGuard.getInstance()).performUserActivity(() -> runMouseDraggedCommand(e));
EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e));
if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) {
myGutterComponent.mouseDragged(e);
}
for (EditorMouseMotionListener listener : myMouseMotionListeners) {
listener.mouseDragged(event);
}
}
@Override
public void mouseMoved(@NotNull MouseEvent e) {
if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) {
if (myMousePressedEvent != null && myMousePressedEvent.getComponent() == e.getComponent()) {
Point lastPoint = myMousePressedEvent.getPoint();
Point point = e.getPoint();
int deadZone = Registry.intValue("editor.mouseSelectionStateResetDeadZone");
if (Math.abs(lastPoint.x - point.x) >= deadZone || Math.abs(lastPoint.y - point.y) >= deadZone) {
resetMouseSelectionState(e);
}
}
else {
validateMousePointer(e);
}
}
else {
validateMousePointer(e);
}
myMouseMovedEvent = e;
EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e));
if (e.getSource() == myGutterComponent) {
myGutterComponent.mouseMoved(e);
}
if (event.getArea() == EditorMouseEventArea.EDITING_AREA) {
FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint());
TooltipController controller = TooltipController.getInstance();
if (fold != null && !fold.shouldNeverExpand()) {
DocumentFragment range = createDocumentFragment(fold);
final Point p = SwingUtilities.convertPoint((Component)e.getSource(), e.getPoint(), getComponent().getRootPane().getLayeredPane());
controller.showTooltip(EditorImpl.this, p, new DocumentFragmentTooltipRenderer(range), false, FOLDING_TOOLTIP_GROUP);
}
else {
controller.cancelTooltip(FOLDING_TOOLTIP_GROUP, e, true);
}
}
for (EditorMouseMotionListener listener : myMouseMotionListeners) {
listener.mouseMoved(event);
}
}
@NotNull
private DocumentFragment createDocumentFragment(@NotNull FoldRegion fold) {
final FoldingGroup group = fold.getGroup();
final int foldStart = fold.getStartOffset();
if (group != null) {
final int endOffset = myFoldingModel.getEndOffset(group);
if (offsetToVisualLine(endOffset) == offsetToVisualLine(foldStart)) {
return new DocumentFragment(myDocument, foldStart, endOffset);
}
}
final int oldEnd = fold.getEndOffset();
return new DocumentFragment(myDocument, foldStart, oldEnd);
}
}
private class MyColorSchemeDelegate extends DelegateColorScheme {
private final FontPreferencesImpl myFontPreferences = new FontPreferencesImpl();
private final FontPreferencesImpl myConsoleFontPreferences = new FontPreferencesImpl();
private final Map<TextAttributesKey, TextAttributes> myOwnAttributes = ContainerUtilRt.newHashMap();
private final Map<ColorKey, Color> myOwnColors = ContainerUtilRt.newHashMap();
private final EditorColorsScheme myCustomGlobalScheme;
private Map<EditorFontType, Font> myFontsMap;
private int myMaxFontSize = EditorFontsConstants.getMaxEditorFontSize();
private int myFontSize = -1;
private int myConsoleFontSize = -1;
private String myFaceName;
private MyColorSchemeDelegate(@Nullable EditorColorsScheme globalScheme) {
super(globalScheme == null ? EditorColorsManager.getInstance().getGlobalScheme() : globalScheme);
myCustomGlobalScheme = globalScheme;
updateGlobalScheme();
}
private void reinitFonts() {
EditorColorsScheme delegate = getDelegate();
String editorFontName = getEditorFontName();
int editorFontSize = getEditorFontSize();
updatePreferences(myFontPreferences, editorFontName, editorFontSize, delegate == null ? null : delegate.getFontPreferences());
String consoleFontName = getConsoleFontName();
int consoleFontSize = getConsoleFontSize();
updatePreferences(myConsoleFontPreferences, consoleFontName, consoleFontSize, delegate == null ? null : delegate.getConsoleFontPreferences());
myFontsMap = new EnumMap<>(EditorFontType.class);
myFontsMap.put(EditorFontType.PLAIN, new Font(editorFontName, Font.PLAIN, editorFontSize));
myFontsMap.put(EditorFontType.BOLD, new Font(editorFontName, Font.BOLD, editorFontSize));
myFontsMap.put(EditorFontType.ITALIC, new Font(editorFontName, Font.ITALIC, editorFontSize));
myFontsMap.put(EditorFontType.BOLD_ITALIC, new Font(editorFontName, Font.BOLD | Font.ITALIC, editorFontSize));
myFontsMap.put(EditorFontType.CONSOLE_PLAIN, new Font(consoleFontName, Font.PLAIN, consoleFontSize));
myFontsMap.put(EditorFontType.CONSOLE_BOLD, new Font(consoleFontName, Font.BOLD, consoleFontSize));
myFontsMap.put(EditorFontType.CONSOLE_ITALIC, new Font(consoleFontName, Font.ITALIC, consoleFontSize));
myFontsMap.put(EditorFontType.CONSOLE_BOLD_ITALIC, new Font(consoleFontName, Font.BOLD | Font.ITALIC, consoleFontSize));
}
private void updatePreferences(FontPreferencesImpl preferences, String fontName, int fontSize, FontPreferences delegatePreferences) {
preferences.clear();
preferences.register(fontName, fontSize);
if (delegatePreferences != null) {
boolean first = true; //skip delegate's primary font
for (String font : delegatePreferences.getRealFontFamilies()) {
if (!first) {
preferences.register(font, fontSize);
}
first = false;
}
}
preferences.setUseLigatures(delegatePreferences != null && delegatePreferences.useLigatures());
}
private void reinitFontsAndSettings() {
reinitFonts();
reinitSettings();
}
@Override
public TextAttributes getAttributes(TextAttributesKey key) {
if (myOwnAttributes.containsKey(key)) return myOwnAttributes.get(key);
return getDelegate().getAttributes(key);
}
@Override
public void setAttributes(@NotNull TextAttributesKey key, TextAttributes attributes) {
myOwnAttributes.put(key, attributes);
}
@Nullable
@Override
public Color getColor(ColorKey key) {
if (myOwnColors.containsKey(key)) return myOwnColors.get(key);
return getDelegate().getColor(key);
}
@Override
public void setColor(ColorKey key, Color color) {
myOwnColors.put(key, color);
// These two are here because those attributes are cached and I do not whant the clients to call editor's reinit
// settings in this case.
myCaretModel.reinitSettings();
mySelectionModel.reinitSettings();
}
@Override
public int getEditorFontSize() {
if (myFontSize == -1) {
return getDelegate().getEditorFontSize();
}
return myFontSize;
}
@Override
public void setEditorFontSize(int fontSize) {
if (fontSize < MIN_FONT_SIZE) fontSize = MIN_FONT_SIZE;
if (fontSize > myMaxFontSize) fontSize = myMaxFontSize;
if (fontSize == myFontSize) return;
myFontSize = fontSize;
reinitFontsAndSettings();
}
@NotNull
@Override
public FontPreferences getFontPreferences() {
return myFontPreferences.getEffectiveFontFamilies().isEmpty() ? getDelegate().getFontPreferences() : myFontPreferences;
}
@Override
public void setFontPreferences(@NotNull FontPreferences preferences) {
if (Comparing.equal(preferences, myFontPreferences)) return;
preferences.copyTo(myFontPreferences);
reinitFontsAndSettings();
}
@NotNull
@Override
public FontPreferences getConsoleFontPreferences() {
return myConsoleFontPreferences.getEffectiveFontFamilies().isEmpty() ? getDelegate().getConsoleFontPreferences() : myConsoleFontPreferences;
}
@Override
public void setConsoleFontPreferences(@NotNull FontPreferences preferences) {
if (Comparing.equal(preferences, myConsoleFontPreferences)) return;
preferences.copyTo(myConsoleFontPreferences);
reinitFontsAndSettings();
}
@Override
public String getEditorFontName() {
if (myFaceName == null) {
return getDelegate().getEditorFontName();
}
return myFaceName;
}
@Override
public void setEditorFontName(String fontName) {
if (Comparing.equal(fontName, myFaceName)) return;
myFaceName = fontName;
reinitFontsAndSettings();
}
@Override
public Font getFont(EditorFontType key) {
if (myFontsMap != null) {
Font font = myFontsMap.get(key);
if (font != null) return font;
}
return getDelegate().getFont(key);
}
@Override
public void setFont(EditorFontType key, Font font) {
if (myFontsMap == null) {
reinitFontsAndSettings();
}
myFontsMap.put(key, font);
reinitSettings();
}
@Override
@Nullable
public EditorColorsScheme clone() {
return null;
}
private void updateGlobalScheme() {
setDelegate(myCustomGlobalScheme == null ? EditorColorsManager.getInstance().getGlobalScheme() : myCustomGlobalScheme);
}
@Override
public void setDelegate(@NotNull EditorColorsScheme delegate) {
super.setDelegate(delegate);
int globalFontSize = getDelegate().getEditorFontSize();
myMaxFontSize = Math.max(EditorFontsConstants.getMaxEditorFontSize(), globalFontSize);
reinitFonts();
}
@Override
public void setConsoleFontSize(int fontSize) {
myConsoleFontSize = fontSize;
reinitFontsAndSettings();
}
@Override
public int getConsoleFontSize() {
return myConsoleFontSize == -1 ? super.getConsoleFontSize() : myConsoleFontSize;
}
}
private static class ExplosionPainter extends AbstractPainter {
private final Point myExplosionLocation;
private final Image myImage;
private static final long TIME_PER_FRAME = 30;
private final int myWidth;
private final int myHeight;
private long lastRepaintTime = System.currentTimeMillis();
private int frameIndex;
private static final int TOTAL_FRAMES = 8;
private final AtomicBoolean nrp = new AtomicBoolean(true);
ExplosionPainter(final Point explosionLocation, Image image) {
myExplosionLocation = new Point(explosionLocation.x, explosionLocation.y);
myImage = image;
myWidth = myImage.getWidth(null);
myHeight = myImage.getHeight(null);
}
@Override
public void executePaint(Component component, Graphics2D g) {
if (!nrp.get()) return;
long currentTimeMillis = System.currentTimeMillis();
float alpha = 1 - (float)frameIndex / TOTAL_FRAMES;
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
Image scaledImage = ImageUtil.scaleImage(myImage, alpha);
int x = myExplosionLocation.x - scaledImage.getWidth(null) / 2;
int y = myExplosionLocation.y - scaledImage.getHeight(null) / 2;
if (currentTimeMillis - lastRepaintTime < TIME_PER_FRAME) {
UIUtil.drawImage(g, scaledImage, x, y, null);
JobScheduler.getScheduler().schedule(() -> component.repaint(x, y, myWidth, myHeight), TIME_PER_FRAME, TimeUnit.MILLISECONDS);
return;
}
lastRepaintTime = currentTimeMillis;
frameIndex++;
UIUtil.drawImage(g, scaledImage, x, y, null);
if (frameIndex == TOTAL_FRAMES) {
nrp.set(false);
IdeGlassPane glassPane = IdeGlassPaneUtil.find(component);
ApplicationManager.getApplication().invokeLater(() -> glassPane.removePainter(this));
component.repaint(x, y, myWidth, myHeight);
}
component.repaint(x, y, myWidth, myHeight);
}
@Override
public boolean needsRepaint() {
return nrp.get();
}
}
static boolean handleDrop(@NotNull EditorImpl editor, @NotNull final Transferable t, int dropAction) {
final EditorDropHandler dropHandler = editor.getDropHandler();
if (Registry.is("debugger.click.disable.breakpoints")) {
try {
if (t.isDataFlavorSupported(GutterDraggableObject.flavor)) {
Object attachedObject = t.getTransferData(GutterDraggableObject.flavor);
if (attachedObject instanceof GutterIconRenderer) {
GutterDraggableObject object = ((GutterIconRenderer)attachedObject).getDraggableObject();
if (object != null) {
object.remove();
Point mouseLocationOnScreen = MouseInfo.getPointerInfo().getLocation();
JComponent editorComponent = editor.getComponent();
Point editorComponentLocationOnScreen = editorComponent.getLocationOnScreen();
IdeGlassPaneUtil.installPainter(editorComponent, new ExplosionPainter(
new Point(mouseLocationOnScreen.x - editorComponentLocationOnScreen.x, mouseLocationOnScreen.y - editorComponentLocationOnScreen.y),
editor.getGutterComponentEx().getDragImage((GutterIconRenderer)attachedObject)), editor.getDisposable());
return true;
}
}
}
}
catch (UnsupportedFlavorException | IOException e) {
LOG.warn(e);
}
}
if (dropHandler != null && dropHandler.canHandleDrop(t.getTransferDataFlavors())) {
dropHandler.handleDrop(t, editor.getProject(), null, dropAction);
return true;
}
final int caretOffset = editor.getCaretModel().getOffset();
if (editor.myDraggedRange != null && editor.myDraggedRange.getStartOffset() <= caretOffset && caretOffset < editor.myDraggedRange.getEndOffset()) {
return false;
}
if (editor.myDraggedRange != null) {
editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack);
}
CommandProcessor.getInstance().executeCommand(editor.myProject, () -> {
try {
editor.getSelectionModel().removeSelection();
final int offset;
if (editor.myDraggedRange != null) {
editor.getCaretModel().moveToOffset(caretOffset);
offset = caretOffset;
}
else {
offset = editor.getCaretModel().getOffset();
}
if (editor.getDocument().getRangeGuard(offset, offset) != null) {
return;
}
editor.putUserData(LAST_PASTED_REGION, null);
EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE);
LOG.assertTrue(pasteHandler instanceof EditorTextInsertHandler, String.valueOf(pasteHandler));
((EditorTextInsertHandler)pasteHandler).execute(editor, editor.getDataContext(), () -> t);
TextRange range = editor.getUserData(LAST_PASTED_REGION);
if (range != null) {
editor.getCaretModel().moveToOffset(range.getStartOffset());
editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset());
}
}
catch (Exception exception) {
LOG.error(exception);
}
}, EditorBundle.message("paste.command.name"), DND_COMMAND_KEY, UndoConfirmationPolicy.DEFAULT, editor.getDocument());
return true;
}
private static class MyTransferHandler extends TransferHandler {
private static EditorImpl getEditor(@NotNull JComponent comp) {
EditorComponentImpl editorComponent = (EditorComponentImpl)comp;
return editorComponent.getEditor();
}
@Override
public boolean importData(TransferSupport support) {
Component comp = support.getComponent();
if (!(comp instanceof JComponent)) return false;
return handleDrop(getEditor((JComponent)comp), support.getTransferable(), support.getDropAction());
}
@Override
public boolean canImport(@NotNull JComponent comp, @NotNull DataFlavor[] transferFlavors) {
Editor editor = getEditor(comp);
final EditorDropHandler dropHandler = ((EditorImpl)editor).getDropHandler();
if (dropHandler != null && dropHandler.canHandleDrop(transferFlavors)) {
return true;
}
//should be used a better representation class
if (Registry.is("debugger.click.disable.breakpoints") && ArrayUtil.contains(GutterDraggableObject.flavor, transferFlavors)) {
return true;
}
if (editor.isViewer()) return false;
int offset = editor.getCaretModel().getOffset();
if (editor.getDocument().getRangeGuard(offset, offset) != null) return false;
return ArrayUtil.contains(DataFlavor.stringFlavor, transferFlavors);
}
@Override
@Nullable
protected Transferable createTransferable(JComponent c) {
EditorImpl editor = getEditor(c);
String s = editor.getSelectionModel().getSelectedText();
if (s == null) return null;
int selectionStart = editor.getSelectionModel().getSelectionStart();
int selectionEnd = editor.getSelectionModel().getSelectionEnd();
editor.myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd);
return new StringSelection(s);
}
@Override
public int getSourceActions(@NotNull JComponent c) {
return COPY_OR_MOVE;
}
@Override
protected void exportDone(@NotNull final JComponent source, @Nullable Transferable data, int action) {
if (data == null) return;
final Component last = DnDManager.getInstance().getLastDropHandler();
if (last != null && !(last instanceof EditorComponentImpl) && !(last instanceof EditorGutterComponentImpl)) return;
final EditorImpl editor = getEditor(source);
if (action == MOVE && !editor.isViewer() && editor.myDraggedRange != null) {
((TransactionGuardImpl)TransactionGuard.getInstance()).performUserActivity(() -> removeDraggedOutFragment(editor));
}
editor.clearDnDContext();
}
private static void removeDraggedOutFragment(EditorImpl editor) {
if (!FileDocumentManager.getInstance().requestWriting(editor.getDocument(), editor.getProject())) {
return;
}
CommandProcessor.getInstance().executeCommand(editor.myProject, () -> ApplicationManager.getApplication().runWriteAction(() -> {
Document doc = editor.getDocument();
doc.startGuardedBlockChecking();
try {
doc.deleteString(editor.myDraggedRange.getStartOffset(), editor.myDraggedRange.getEndOffset());
}
catch (ReadOnlyFragmentModificationException e) {
EditorActionManager.getInstance().getReadonlyFragmentModificationHandler(doc).handle(e);
}
finally {
doc.stopGuardedBlockChecking();
}
}), EditorBundle.message("move.selection.command.name"), DND_COMMAND_KEY, UndoConfirmationPolicy.DEFAULT, editor.getDocument());
}
}
private class EditorDocumentAdapter implements PrioritizedDocumentListener {
@Override
public void beforeDocumentChange(@NotNull DocumentEvent e) {
beforeChangedUpdate();
}
@Override
public void documentChanged(@NotNull DocumentEvent e) {
changedUpdate(e);
}
@Override
public int getPriority() {
return EditorDocumentPriorities.EDITOR_DOCUMENT_ADAPTER;
}
}
private class EditorDocumentBulkUpdateAdapter implements DocumentBulkUpdateListener {
@Override
public void updateStarted(@NotNull Document doc) {
if (doc != getDocument()) return;
bulkUpdateStarted();
}
@Override
public void updateFinished(@NotNull Document doc) {
if (doc != getDocument()) return;
bulkUpdateFinished();
}
}
@Override
@NotNull
public EditorGutter getGutter() {
return getGutterComponentEx();
}
@Override
public int calcColumnNumber(@NotNull CharSequence text, int start, int offset, int tabSize) {
return myView.offsetToLogicalPosition(offset).column;
}
public boolean isInDistractionFreeMode() {
return EditorUtil.isRealFileEditor(this) && (Registry.is("editor.distraction.free.mode") || isInPresentationMode());
}
boolean isInPresentationMode() {
return UISettings.getInstance().getPresentationMode() && EditorUtil.isRealFileEditor(this);
}
@Override
public void putInfo(@NotNull Map<String, String> info) {
final VisualPosition visual = getCaretModel().getVisualPosition();
info.put("caret", visual.getLine() + ":" + visual.getColumn());
}
private void invokePopupIfNeeded(EditorMouseEvent event) {
if (myContextMenuGroupId != null && event.getArea() == EditorMouseEventArea.EDITING_AREA && event.getMouseEvent().isPopupTrigger() && !event.isConsumed()) {
String contextMenuGroupId = myContextMenuGroupId;
Inlay inlay = myInlayModel.getElementAt(event.getMouseEvent().getPoint());
if (inlay != null) {
String inlayContextMenuGroupId = inlay.getRenderer().getContextMenuGroupId();
if (inlayContextMenuGroupId != null) contextMenuGroupId = inlayContextMenuGroupId;
}
AnAction action = CustomActionsSchema.getInstance().getCorrectedAction(contextMenuGroupId);
if (action instanceof ActionGroup) {
ActionPopupMenu popupMenu = ActionManager.getInstance().createActionPopupMenu(ActionPlaces.EDITOR_POPUP, (ActionGroup)action);
MouseEvent e = event.getMouseEvent();
final Component c = e.getComponent();
if (c != null && c.isShowing()) {
popupMenu.getComponent().show(c, e.getX(), e.getY());
}
e.consume();
}
}
}
@TestOnly
public void validateState() {
myView.validateState();
mySoftWrapModel.validateState();
myFoldingModel.validateState();
myCaretModel.validateState();
myInlayModel.validateState();
}
private class MyScrollPane extends JBScrollPane {
private MyScrollPane() {
super(0);
setupCorners();
}
@Override
public void setUI(ScrollPaneUI ui) {
super.setUI(ui);
// disable standard Swing keybindings
setInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, null);
}
@Override
public void layout() {
if (isInDistractionFreeMode()) {
// re-calc gutter extra size after editor size is set
// & layout once again to avoid blinking
myGutterComponent.updateSize(true, true);
}
super.layout();
}
@Override
protected void processMouseWheelEvent(@NotNull MouseWheelEvent e) {
if (mySettings.isWheelFontChangeEnabled()) {
if (EditorUtil.isChangeFontSize(e)) {
int size = myScheme.getEditorFontSize() - e.getWheelRotation();
if (size >= MIN_FONT_SIZE) {
setFontSize(size, SwingUtilities.convertPoint(this, e.getPoint(), getViewport()));
}
return;
}
}
super.processMouseWheelEvent(e);
}
@NotNull
@Override
public JScrollBar createVerticalScrollBar() {
return new MyScrollBar(Adjustable.VERTICAL);
}
@Override
protected void setupCorners() {
super.setupCorners();
setBorder(new TablessBorder());
}
}
private class TablessBorder extends SideBorder {
private TablessBorder() {
super(JBColor.border(), SideBorder.ALL);
}
@Override
public void paintBorder(@NotNull Component c, @NotNull Graphics g, int x, int y, int width, int height) {
if (c instanceof JComponent) {
Insets insets = ((JComponent)c).getInsets();
if (insets.left > 0) {
super.paintBorder(c, g, x, y, width, height);
}
else {
g.setColor(UIUtil.getPanelBackground());
g.fillRect(x, y, width, 1);
g.setColor(Gray._50.withAlpha(90));
g.fillRect(x, y, width, 1);
}
}
}
@NotNull
@Override
public Insets getBorderInsets(Component c) {
Container splitters = SwingUtilities.getAncestorOfClass(EditorsSplitters.class, c);
boolean thereIsSomethingAbove =
!SystemInfo.isMac || UISettings.getInstance().getShowMainToolbar() || UISettings.getInstance().getShowNavigationBar() || toolWindowIsNotEmpty();
//noinspection ConstantConditions
Component header = myHeaderPanel == null ? null : ArrayUtil.getFirstElement(myHeaderPanel.getComponents());
boolean paintTop = thereIsSomethingAbove && header == null && UISettings.getInstance().getEditorTabPlacement() != SwingConstants.TOP;
return splitters == null ? super.getBorderInsets(c) : new Insets(paintTop ? 1 : 0, 0, 0, 0);
}
private boolean toolWindowIsNotEmpty() {
if (myProject == null) return false;
ToolWindowManagerEx m = ToolWindowManagerEx.getInstanceEx(myProject);
return m != null && !m.getIdsOn(ToolWindowAnchor.TOP).isEmpty();
}
@Override
public boolean isBorderOpaque() {
return true;
}
}
private class MyHeaderPanel extends JPanel {
private int myOldHeight;
private MyHeaderPanel() {
super(new BorderLayout());
}
@Override
public void revalidate() {
myOldHeight = getHeight();
super.revalidate();
}
@Override
protected void validateTree() {
int height = myOldHeight;
super.validateTree();
height -= getHeight();
if (height != 0) {
myVerticalScrollBar.setValue(myVerticalScrollBar.getValue() - height);
}
myOldHeight = getHeight();
}
}
private class MyTextDrawingCallback implements TextDrawingCallback {
@Override
public void drawChars(@NotNull Graphics g, @NotNull char[] data, int start, int end, int x, int y, Color color, @NotNull FontInfo fontInfo) {
myView.drawChars(g, data, start, end, x, y, color, fontInfo);
}
}
private static class NullEditorHighlighter extends EmptyEditorHighlighter {
private static final TextAttributes NULL_ATTRIBUTES = new TextAttributes();
public NullEditorHighlighter() {
super(NULL_ATTRIBUTES);
}
@Override
public void setAttributes(TextAttributes attributes) {
}
@Override
public void setColorScheme(@NotNull EditorColorsScheme scheme) {
}
}
}