mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-10 09:39:37 +07:00
7108 lines
261 KiB
Java
7108 lines
261 KiB
Java
/*
|
|
* Copyright 2000-2015 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.OptionsConstants;
|
|
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.diagnostic.LogMessageEx;
|
|
import com.intellij.ide.dnd.DnDManager;
|
|
import com.intellij.ide.ui.UISettings;
|
|
import com.intellij.openapi.Disposable;
|
|
import com.intellij.openapi.actionSystem.ActionManager;
|
|
import com.intellij.openapi.actionSystem.CommonDataKeys;
|
|
import com.intellij.openapi.actionSystem.DataContext;
|
|
import com.intellij.openapi.actionSystem.IdeActions;
|
|
import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
|
|
import com.intellij.openapi.actionSystem.impl.MouseGestureManager;
|
|
import com.intellij.openapi.application.ApplicationManager;
|
|
import com.intellij.util.concurrency.ThreadingAssertions;
|
|
import com.intellij.util.concurrency.ThreadingAssertions;
|
|
import com.intellij.openapi.application.ModalityState;
|
|
import com.intellij.openapi.command.CommandProcessor;
|
|
import com.intellij.openapi.command.UndoConfirmationPolicy;
|
|
import com.intellij.openapi.diagnostic.Logger;
|
|
import com.intellij.openapi.editor.colors.impl.DelegateColorScheme;
|
|
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.softwrap.SoftWrapDrawingType;
|
|
import com.intellij.openapi.editor.impl.view.EditorView;
|
|
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.KeymapUtil;
|
|
import com.intellij.openapi.progress.ProgressManager;
|
|
import com.intellij.openapi.project.Project;
|
|
import com.intellij.openapi.ui.Queryable;
|
|
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.ToolWindowAnchor;
|
|
import com.intellij.openapi.wm.ex.ToolWindowManagerEx;
|
|
import com.intellij.openapi.wm.impl.IdeBackgroundUtil;
|
|
import com.intellij.psi.PsiDocumentManager;
|
|
import com.intellij.psi.PsiFile;
|
|
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
|
|
import com.intellij.ui.components.JBLayeredPane;
|
|
import com.intellij.ui.components.JBScrollBar;
|
|
import com.intellij.ui.components.JBScrollPane;
|
|
import com.intellij.util.containers.ContainerUtil;
|
|
import com.intellij.util.containers.ContainerUtilRt;
|
|
import com.intellij.util.messages.MessageBusConnection;
|
|
import com.intellij.util.text.CharArrayCharSequence;
|
|
import com.intellij.util.text.CharArrayUtil;
|
|
import com.intellij.util.ui.update.Activatable;
|
|
import com.intellij.util.ui.update.UiNotifyConnector;
|
|
import gnu.trove.TIntArrayList;
|
|
import gnu.trove.TIntFunction;
|
|
import gnu.trove.TIntHashSet;
|
|
import gnu.trove.TIntIntHashMap;
|
|
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.border.Border;
|
|
import javax.swing.plaf.ScrollBarUI;
|
|
import javax.swing.plaf.basic.BasicScrollBarUI;
|
|
import java.awt.datatransfer.DataFlavor;
|
|
import java.awt.datatransfer.StringSelection;
|
|
import java.awt.datatransfer.Transferable;
|
|
import java.awt.dnd.DropTarget;
|
|
import java.awt.dnd.DropTargetAdapter;
|
|
import java.awt.dnd.DropTargetDragEvent;
|
|
import java.awt.dnd.DropTargetDropEvent;
|
|
import java.awt.font.TextHitInfo;
|
|
import java.awt.im.InputMethodRequests;
|
|
import java.awt.image.BufferedImage;
|
|
import java.beans.PropertyChangeListener;
|
|
import java.beans.PropertyChangeSupport;
|
|
import java.lang.reflect.Field;
|
|
import java.lang.reflect.InvocationTargetException;
|
|
import java.text.AttributedCharacterIterator;
|
|
import java.text.AttributedString;
|
|
import java.text.CharacterIterator;
|
|
import java.util.List;
|
|
import java.util.concurrent.ScheduledFuture;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
public final class XXXXX extends UserDataHolderBase implements EditorEx, HighlighterClient, Queryable, Dumpable {
|
|
private static final boolean isOracleRetina = UIUtil.isRetina() && SystemInfo.isOracleJvm;
|
|
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";
|
|
public static final Key<JComponent> PERMANENT_HEADER = Key.create("PERMANENT_HEADER");
|
|
public static final Key<Boolean> DO_DOCUMENT_UPDATE_TEST = Key.create("DoDocumentUpdateTest");
|
|
public static final Key<Boolean> FORCED_SOFT_WRAPS = Key.create("forced.soft.wraps");
|
|
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");
|
|
public static final Color CURSOR_FOREGROUND_LIGHT = Gray._255;
|
|
public static final Color CURSOR_FOREGROUND_DARK = Gray._0;
|
|
@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(new Throwable());
|
|
private int myLinePaintersWidth = 0;
|
|
|
|
static {
|
|
ComplementaryFontsRegistry.getFontAbleToDisplay(' ', 0, 0, UIManager.getFont("Label.font").getFamily()); // load costly font info
|
|
}
|
|
|
|
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 int myCharHeight = -1;
|
|
private int myLineHeight = -1;
|
|
private int myDescent = -1;
|
|
|
|
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 = false;
|
|
|
|
@Nullable private MouseEvent myMousePressedEvent = null;
|
|
@Nullable private MouseEvent myMouseMovedEvent = null;
|
|
|
|
/**
|
|
* Holds information about area where mouse was pressed.
|
|
*/
|
|
@Nullable private EditorMouseEventArea myMousePressArea;
|
|
private int mySavedSelectionStart = -1;
|
|
private int mySavedSelectionEnd = -1;
|
|
private int myLastColumnNumber = 0;
|
|
|
|
private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this);
|
|
private MyEditable myEditable;
|
|
|
|
@NotNull
|
|
private EditorColorsScheme myScheme;
|
|
private ArrowPainter myTabPainter;
|
|
private boolean myIsViewer;
|
|
@NotNull private final SelectionModelImpl mySelectionModel;
|
|
@NotNull private final EditorMarkupModelImpl myMarkupModel;
|
|
@NotNull private final FoldingModelImpl myFoldingModel;
|
|
@NotNull private final ScrollingModelImpl myScrollingModel;
|
|
@NotNull private final CaretModelImpl myCaretModel;
|
|
@NotNull private final SoftWrapModelImpl mySoftWrapModel;
|
|
|
|
@NotNull private static final RepaintCursorCommand ourCaretBlinkingCommand = new RepaintCursorCommand();
|
|
private MessageBusConnection myConnection;
|
|
|
|
private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE;
|
|
@Nullable private FoldRegion myMouseSelectedRegion = null;
|
|
|
|
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 FontMetrics myPlainFontMetrics;
|
|
private FontMetrics myBoldFontMetrics;
|
|
private FontMetrics myItalicFontMetrics;
|
|
private FontMetrics myBoldItalicFontMetrics;
|
|
|
|
private static final int CACHED_CHARS_BUFFER_SIZE = 300;
|
|
|
|
private final ArrayList<CachedFontContent> myFontCache = new ArrayList<CachedFontContent>();
|
|
@Nullable private FontInfo myCurrentFontType = null;
|
|
|
|
private final EditorSizeContainer mySizeContainer = new EditorSizeContainer();
|
|
|
|
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 = false;
|
|
@Nullable private Color myForcedBackground = null;
|
|
@Nullable private Dimension myPreferredSize;
|
|
private int myVirtualPageHeight;
|
|
|
|
private final Alarm myMouseSelectionStateAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD);
|
|
private Runnable myMouseSelectionStateResetRunnable;
|
|
|
|
private boolean myEmbeddedIntoDialogWrapper;
|
|
@Nullable private CachedFontContent myLastCache;
|
|
private int myDragOnGutterSelectionStartLine = -1;
|
|
private RangeMarker myDraggedRange;
|
|
|
|
private boolean mySoftWrapsChanged;
|
|
|
|
// transient fields used during painting
|
|
private VisualPosition mySelectionStartPosition;
|
|
private VisualPosition mySelectionEndPosition;
|
|
|
|
private Color myLastBackgroundColor = null;
|
|
private Point myLastBackgroundPosition = null;
|
|
private int myLastBackgroundWidth;
|
|
private static final boolean ourIsUnitTestMode = ApplicationManager.getApplication().isUnitTestMode();
|
|
@NotNull private final JPanel myHeaderPanel;
|
|
|
|
@Nullable private MouseEvent myInitialMouseEvent;
|
|
private boolean myIgnoreMouseEventsConsecutiveToInitial;
|
|
|
|
private EditorDropHandler myDropHandler;
|
|
|
|
private char[] myPrefixText;
|
|
private TextAttributes myPrefixAttributes;
|
|
private int myPrefixWidthInPixels;
|
|
@NotNull private final IndentsModel myIndentsModel;
|
|
|
|
@Nullable
|
|
private CharSequence myPlaceholderText;
|
|
private int myLastPaintedPlaceholderWidth;
|
|
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;
|
|
private 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");
|
|
public final boolean myUseNewRendering = Registry.is("editor.new.rendering");
|
|
final EditorView myView;
|
|
|
|
private boolean myCharKeyPressed;
|
|
private boolean myNeedToSelectPreviousChar;
|
|
|
|
private boolean myDocumentChangeInProgress;
|
|
private boolean myErrorStripeNeedsRepaint;
|
|
|
|
static {
|
|
ourCaretBlinkingCommand.start();
|
|
}
|
|
|
|
private int myExpectedCaretOffset = -1;
|
|
|
|
EditorImpl(@NotNull Document document, boolean viewer, @Nullable Project project) {
|
|
assertIsDispatchThread();
|
|
myProject = project;
|
|
myDocument = (DocumentEx)document;
|
|
if (myDocument instanceof DocumentImpl) {
|
|
((DocumentImpl)myDocument).requestTabTracking();
|
|
}
|
|
myScheme = createBoundColorSchemeDelegate(null);
|
|
initTabPainter();
|
|
myIsViewer = viewer;
|
|
mySettings = new SettingsImpl(this, project);
|
|
if (shouldSoftWrapsBeForced()) {
|
|
mySettings.setUseSoftWrapsQuiet();
|
|
putUserData(FORCED_SOFT_WRAPS, Boolean.TRUE);
|
|
}
|
|
|
|
mySelectionModel = new SelectionModelImpl(this);
|
|
myMarkupModel = new EditorMarkupModelImpl(this);
|
|
myFoldingModel = new FoldingModelImpl(this);
|
|
myCaretModel = new CaretModelImpl(this);
|
|
mySoftWrapModel = new SoftWrapModelImpl(this);
|
|
if (!myUseNewRendering) mySizeContainer.reset();
|
|
|
|
myCommandProcessor = CommandProcessor.getInstance();
|
|
|
|
if (project != null) {
|
|
myConnection = project.getMessageBus().connect();
|
|
myConnection.subscribe(DocumentBulkUpdateListener.TOPIC, new EditorDocumentBulkUpdateAdapter());
|
|
}
|
|
|
|
MarkupModelListener markupModelListener = 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));
|
|
}
|
|
|
|
@Override
|
|
public void beforeRemoved(@NotNull RangeHighlighterEx highlighter) {
|
|
attributesChanged(highlighter, areRenderersInvolved(highlighter));
|
|
}
|
|
|
|
@Override
|
|
public void attributesChanged(@NotNull RangeHighlighterEx highlighter, boolean renderersChanged) {
|
|
if (myDocument.isInBulkUpdate()) return; // bulkUpdateFinished() will repaint anything
|
|
|
|
if (myUseNewRendering && renderersChanged) {
|
|
updateGutterSize();
|
|
}
|
|
|
|
boolean errorStripeNeedsRepaint = renderersChanged || highlighter.getErrorStripeMarkColor() != null;
|
|
if (myUseNewRendering && 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();
|
|
|
|
clearTextWidthCache();
|
|
|
|
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);
|
|
TextAttributes attributes = highlighter.getTextAttributes();
|
|
if (myUseNewRendering && start != end && attributes != null && attributes.getFontType() != Font.PLAIN) {
|
|
myView.invalidateRange(start, end);
|
|
}
|
|
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) {
|
|
((EditorMarkupModelImpl)getMarkupModel()).repaint(start, end);
|
|
}
|
|
|
|
if (!myUseNewRendering && renderersChanged) {
|
|
updateGutterSize();
|
|
}
|
|
|
|
updateCaretCursor();
|
|
}
|
|
};
|
|
|
|
((MarkupModelEx)DocumentMarkupModel.forDocument(myDocument, myProject, true)).addMarkupModelListener(myCaretModel, markupModelListener);
|
|
getMarkupModel().addMarkupModelListener(myCaretModel, markupModelListener);
|
|
|
|
myDocument.addDocumentListener(myFoldingModel, myCaretModel);
|
|
myDocument.addDocumentListener(myCaretModel, myCaretModel);
|
|
myDocument.addDocumentListener(mySelectionModel, myCaretModel);
|
|
|
|
myDocument.addDocumentListener(new EditorDocumentAdapter(), myCaretModel);
|
|
myDocument.addDocumentListener(mySoftWrapModel, myCaretModel);
|
|
|
|
myFoldingModel.addListener(mySoftWrapModel, myCaretModel);
|
|
|
|
myIndentsModel = new IndentsModelImpl(this);
|
|
myCaretModel.addCaretListener(new CaretListener() {
|
|
@Nullable private LightweightHint myCurrentHint = null;
|
|
@Nullable private IndentGuideDescriptor myCurrentCaretGuide = null;
|
|
|
|
@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();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void softWrapsChanged() {
|
|
mySoftWrapsChanged = true;
|
|
}
|
|
});
|
|
|
|
if (!myUseNewRendering) {
|
|
mySoftWrapModel.addVisualSizeChangeListener(new VisualSizeChangeListener() {
|
|
@Override
|
|
public void onLineWidthsChange(int startLine, int oldEndLine, int newEndLine, @NotNull TIntIntHashMap lineWidths) {
|
|
mySizeContainer.update(startLine, newEndLine, oldEndLine);
|
|
for (int i = startLine; i <= newEndLine; i++) {
|
|
if (lineWidths.contains(i)) {
|
|
int width = lineWidths.get(i);
|
|
if (width >= 0) {
|
|
mySizeContainer.updateLineWidthIfNecessary(i, width);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
EditorHighlighter highlighter = new EmptyEditorHighlighter(myScheme.getAttributes(HighlighterColors.TEXT));
|
|
setHighlighter(highlighter);
|
|
|
|
myEditorComponent = new EditorComponentImpl(this);
|
|
myScrollPane = new MyScrollPane();
|
|
myVerticalScrollBar = (MyScrollBar)myScrollPane.getVerticalScrollBar();
|
|
myVerticalScrollBar.setOpaque(false);
|
|
myPanel = new JPanel();
|
|
|
|
myView = myUseNewRendering ? new EditorView(this) : null;
|
|
|
|
UIUtil.putClientProperty(
|
|
myPanel, JBSwingUtilities.NOT_IN_HIERARCHY_COMPONENTS, new Iterable<JComponent>() {
|
|
@Override
|
|
public Iterator<JComponent> iterator() {
|
|
JComponent component = getPermanentHeaderComponent();
|
|
if (component != null && !component.isValid()) {
|
|
return Collections.singleton(component).iterator();
|
|
}
|
|
return ContainerUtil.emptyIterator();
|
|
}
|
|
});
|
|
|
|
myHeaderPanel = new MyHeaderPanel();
|
|
myGutterComponent = new EditorGutterComponentImpl(this);
|
|
initComponent();
|
|
myScrollingModel = new ScrollingModelImpl(this);
|
|
if (UISettings.getInstance().getPresentationMode()) {
|
|
setFontSize(UISettings.getInstance().PRESENTATION_MODE_FONT_SIZE);
|
|
}
|
|
|
|
myGutterComponent.setLineNumberAreaWidthFunction(new TIntFunction() {
|
|
@Override
|
|
public int execute(int lineNumber) {
|
|
return getFontMetrics(Font.PLAIN).stringWidth(Integer.toString(lineNumber + 1));
|
|
}
|
|
});
|
|
myGutterComponent.updateSize();
|
|
Dimension preferredSize = getPreferredSize();
|
|
myEditorComponent.setSize(preferredSize);
|
|
|
|
updateCaretCursor();
|
|
|
|
// This hacks context layout problem where editor appears scrolled to the right just after it is created.
|
|
if (!ourIsUnitTestMode) {
|
|
UiNotifyConnector.doWhenFirstShown(myEditorComponent, new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (!isDisposed() && !myScrollingModel.isScrollingNow()) {
|
|
myScrollingModel.disableAnimation();
|
|
myScrollingModel.scrollHorizontally(0);
|
|
myScrollingModel.enableAnimation();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private boolean shouldSoftWrapsBeForced() {
|
|
if (myUseNewRendering || mySettings.isUseSoftWraps() ||
|
|
// Disable checking for files in intermediate states - e.g. for files during refactoring.
|
|
(myProject != null && PsiDocumentManager.getInstance(myProject).isDocumentBlockedByPsi(myDocument))) {
|
|
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 myUseNewRendering ? (int)myView.getPrefixTextWidthInPixels() : myPrefixWidthInPixels;
|
|
}
|
|
|
|
@Override
|
|
public void setPrefixTextAndAttributes(@Nullable String prefixText, @Nullable TextAttributes attributes) {
|
|
myPrefixText = prefixText == null ? null : prefixText.toCharArray();
|
|
myPrefixAttributes = attributes;
|
|
myPrefixWidthInPixels = 0;
|
|
if (myPrefixText != null) {
|
|
for (char c : myPrefixText) {
|
|
LOG.assertTrue(myPrefixAttributes != null);
|
|
if (myPrefixAttributes != null) {
|
|
myPrefixWidthInPixels += EditorUtil.charWidth(c, myPrefixAttributes.getFontType(), this);
|
|
}
|
|
}
|
|
}
|
|
mySoftWrapModel.recalculate();
|
|
if (myUseNewRendering) myView.setPrefix(prefixText, attributes);
|
|
}
|
|
|
|
@Override
|
|
public boolean isPurePaintingMode() {
|
|
return myPurePaintingMode;
|
|
}
|
|
|
|
@Override
|
|
public void setPurePaintingMode(boolean enabled) {
|
|
myPurePaintingMode = enabled;
|
|
}
|
|
|
|
@Override
|
|
public void registerScrollBarRepaintCallback(@Nullable ButtonlessScrollBarUI.ScrollbarRepaintCallback callback) {
|
|
myVerticalScrollBar.registerRepaintCallback(callback);
|
|
}
|
|
|
|
@Override
|
|
public int getExpectedCaretOffset() {
|
|
return myExpectedCaretOffset == -1 ? getCaretModel().getOffset() : myExpectedCaretOffset;
|
|
}
|
|
|
|
@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 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;
|
|
}
|
|
|
|
@Override
|
|
@NotNull
|
|
public EditorSettings getSettings() {
|
|
assertReadAccess();
|
|
return mySettings;
|
|
}
|
|
|
|
public void resetSizes() {
|
|
if (myUseNewRendering) {
|
|
myView.reset();
|
|
}
|
|
else {
|
|
mySizeContainer.reset();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void reinitSettings() {
|
|
assertIsDispatchThread();
|
|
clearSettingsCache();
|
|
|
|
reinitDocumentIndentOptions();
|
|
|
|
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());
|
|
if (myUseNewRendering) {
|
|
myView.reinitSettings();
|
|
}
|
|
else {
|
|
mySizeContainer.reset();
|
|
}
|
|
myFoldingModel.rebuild();
|
|
|
|
if (softWrapsUsedBefore ^ mySoftWrapModel.isSoftWrappingEnabled()) {
|
|
mySizeContainer.reset();
|
|
validateSize();
|
|
}
|
|
|
|
myHighlighter.setColorScheme(myScheme);
|
|
myFoldingModel.refreshSettings();
|
|
|
|
myGutterComponent.reinitSettings();
|
|
myGutterComponent.revalidate();
|
|
|
|
myEditorComponent.repaint();
|
|
|
|
initTabPainter();
|
|
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());
|
|
}
|
|
}
|
|
|
|
private void clearSettingsCache() {
|
|
myCharHeight = -1;
|
|
myLineHeight = -1;
|
|
myDescent = -1;
|
|
myPlainFontMetrics = null;
|
|
|
|
clearTextWidthCache();
|
|
}
|
|
|
|
private void reinitDocumentIndentOptions() {
|
|
if (myProject != null && !myProject.isDisposed()) {
|
|
CodeStyleSettingsManager.updateDocumentIndentOptions(myProject, myDocument);
|
|
}
|
|
}
|
|
|
|
private void initTabPainter() {
|
|
myTabPainter = new ArrowPainter(
|
|
ColorProvider.byColorsScheme(myScheme, EditorColors.WHITESPACES_COLOR),
|
|
new Computable.PredefinedValueComputable<Integer>(EditorUtil.getSpaceWidth(Font.PLAIN, this)),
|
|
new Computable<Integer>() {
|
|
@Override
|
|
public Integer compute() {
|
|
return getCharHeight();
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
public void throwDisposalError(@NonNls @NotNull String msg) {
|
|
myTraceableDisposable.throwDisposalError(msg);
|
|
}
|
|
|
|
public void release() {
|
|
assertIsDispatchThread();
|
|
if (isReleased) {
|
|
throwDisposalError("Double release of editor:");
|
|
}
|
|
myTraceableDisposable.kill(null);
|
|
|
|
isReleased = true;
|
|
clearSettingsCache();
|
|
|
|
myFoldingModel.dispose();
|
|
mySoftWrapModel.release();
|
|
myMarkupModel.dispose();
|
|
|
|
myScrollingModel.dispose();
|
|
myGutterComponent.dispose();
|
|
myMousePressedEvent = null;
|
|
myMouseMovedEvent = null;
|
|
Disposer.dispose(myCaretModel);
|
|
Disposer.dispose(mySoftWrapModel);
|
|
if (myUseNewRendering) Disposer.dispose(myView);
|
|
clearCaretThread();
|
|
|
|
myFocusListeners.clear();
|
|
myMouseListeners.clear();
|
|
myMouseMotionListeners.clear();
|
|
|
|
if (myConnection != null) {
|
|
myConnection.disconnect();
|
|
}
|
|
if (myDocument instanceof DocumentImpl) {
|
|
((DocumentImpl)myDocument).giveUpTabTracking();
|
|
}
|
|
Disposer.dispose(myDisposable);
|
|
}
|
|
|
|
private void clearCaretThread() {
|
|
synchronized (ourCaretBlinkingCommand) {
|
|
if (ourCaretBlinkingCommand.myEditor == this) {
|
|
ourCaretBlinkingCommand.myEditor = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static boolean firstCharTyped = true;
|
|
|
|
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);
|
|
|
|
/* Default mode till 1.4.0
|
|
* myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE);
|
|
*/
|
|
|
|
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();
|
|
final MyScrollBar scrollBar = getVerticalScrollBar();
|
|
c.setBounds(r.width - d.width - scrollBar.getWidth() - 30, 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(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;
|
|
}
|
|
});
|
|
|
|
MyMouseAdapter mouseAdapter = new MyMouseAdapter();
|
|
myEditorComponent.addMouseListener(mouseAdapter);
|
|
myGutterComponent.addMouseListener(mouseAdapter);
|
|
|
|
MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener();
|
|
myEditorComponent.addMouseMotionListener(mouseMotionListener);
|
|
myGutterComponent.addMouseMotionListener(mouseMotionListener);
|
|
|
|
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() {
|
|
@Override
|
|
public void showNotify() {
|
|
myGutterComponent.updateSize();
|
|
}
|
|
});
|
|
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().moveToLogicalPosition(getLogicalPositionForScreenPos(location.x, location.y, true));
|
|
getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
catch (TooManyListenersException e) {
|
|
LOG.error(e);
|
|
}
|
|
|
|
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(final 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);
|
|
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.scrollToOffsets(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();
|
|
|
|
ApplicationManager.getApplication().runWriteAction(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
for (int i = 0; i < text.length(); i++) {
|
|
if (!processKeyTyped(text.charAt(i))) {
|
|
result.setRejected();
|
|
return;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
ActionManagerEx actionManager = ActionManagerEx.getInstanceEx();
|
|
DataContext dataContext = getDataContext();
|
|
actionManager.fireBeforeEditorTyping(c, dataContext);
|
|
MacUIUtil.hideCursor();
|
|
EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext);
|
|
|
|
return true;
|
|
}
|
|
|
|
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) {
|
|
assertIsDispatchThread();
|
|
final Document document = getDocument();
|
|
Disposer.dispose(myHighlighterDisposable);
|
|
|
|
document.addDocumentListener(highlighter);
|
|
myHighlighter = highlighter;
|
|
myHighlighterDisposable = new Disposable() {
|
|
@Override
|
|
public void dispose() {
|
|
document.removeDocumentListener(highlighter);
|
|
}
|
|
};
|
|
Disposer.register(myDisposable, myHighlighterDisposable);
|
|
highlighter.setEditor(this);
|
|
highlighter.setText(document.getImmutableCharSequence());
|
|
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, new Disposable() {
|
|
@Override
|
|
public void dispose() {
|
|
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 yPositionToVisibleLine(int y) {
|
|
if (myUseNewRendering) return myView.yToVisualLine(y);
|
|
assert y >= 0 : y;
|
|
return y / getLineHeight();
|
|
}
|
|
|
|
@Override
|
|
@NotNull
|
|
public VisualPosition xyToVisualPosition(@NotNull Point p) {
|
|
if (myUseNewRendering) return myView.xyToVisualPosition(p);
|
|
int line = yPositionToVisibleLine(Math.max(p.y, 0));
|
|
int px = p.x;
|
|
if (line == 0 && myPrefixText != null) {
|
|
px -= myPrefixWidthInPixels;
|
|
}
|
|
if (px < 0) {
|
|
px = 0;
|
|
}
|
|
|
|
int textLength = myDocument.getTextLength();
|
|
LogicalPosition logicalPosition = visualToLogicalPosition(new VisualPosition(line, 0));
|
|
int offset = logicalPositionToOffset(logicalPosition);
|
|
int plainSpaceSize = EditorUtil.getSpaceWidth(Font.PLAIN, this);
|
|
|
|
if (offset >= textLength) return new VisualPosition(line, EditorUtil.columnsNumber(px, plainSpaceSize));
|
|
|
|
// There is a possible case that starting logical line is split by soft-wraps and it's part after the split should be drawn.
|
|
// We mark that we're under such circumstances then.
|
|
boolean activeSoftWrapProcessed = logicalPosition.softWrapLinesOnCurrentLogicalLine <= 0;
|
|
|
|
CharSequence text = myDocument.getImmutableCharSequence();
|
|
|
|
LogicalPosition endLogicalPosition = visualToLogicalPosition(new VisualPosition(line + 1, 0));
|
|
int endOffset = logicalPositionToOffset(endLogicalPosition);
|
|
|
|
if (offset > endOffset) {
|
|
LogMessageEx.error(LOG, "Detected invalid (x; y)->VisualPosition processing", String.format(
|
|
"Given point: %s, mapped to visual line %d. Visual(%d; %d) is mapped to "
|
|
+ "logical position '%s' which is mapped to offset %d (start offset). Visual(%d; %d) is mapped to logical '%s' which is mapped "
|
|
+ "to offset %d (end offset). State: %s",
|
|
p, line, line, 0, logicalPosition, offset, line + 1, 0, endLogicalPosition, endOffset, dumpState()
|
|
));
|
|
return new VisualPosition(line, EditorUtil.columnsNumber(px, plainSpaceSize));
|
|
}
|
|
IterationState state = new IterationState(this, offset, endOffset, false);
|
|
|
|
int fontType = state.getMergedAttributes().getFontType();
|
|
|
|
int x = 0;
|
|
int charWidth;
|
|
boolean onSoftWrapDrawing = false;
|
|
char c = ' ';
|
|
int prevX = 0;
|
|
int column = 0;
|
|
outer:
|
|
while (true) {
|
|
charWidth = -1;
|
|
if (offset >= textLength) {
|
|
break;
|
|
}
|
|
|
|
if (offset >= state.getEndOffset()) {
|
|
state.advance();
|
|
fontType = state.getMergedAttributes().getFontType();
|
|
}
|
|
|
|
SoftWrap softWrap = mySoftWrapModel.getSoftWrap(offset);
|
|
if (softWrap != null) {
|
|
if (activeSoftWrapProcessed) {
|
|
prevX = x;
|
|
charWidth = getSoftWrapModel().getMinDrawingWidthInPixels(SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED);
|
|
x += charWidth;
|
|
if (x >= px) {
|
|
onSoftWrapDrawing = true;
|
|
}
|
|
else {
|
|
column++;
|
|
}
|
|
break;
|
|
}
|
|
else {
|
|
CharSequence softWrapText = softWrap.getText();
|
|
for (int i = 1/*Assuming line feed is located at the first position*/; i < softWrapText.length(); i++) {
|
|
c = softWrapText.charAt(i);
|
|
prevX = x;
|
|
charWidth = charToVisibleWidth(c, fontType, x);
|
|
x += charWidth;
|
|
if (x >= px) {
|
|
break outer;
|
|
}
|
|
column += EditorUtil.columnsNumber(c, x, prevX, plainSpaceSize);
|
|
}
|
|
|
|
// Process 'after soft wrap' sign.
|
|
prevX = x;
|
|
charWidth = mySoftWrapModel.getMinDrawingWidthInPixels(SoftWrapDrawingType.AFTER_SOFT_WRAP);
|
|
x += charWidth;
|
|
if (x >= px) {
|
|
onSoftWrapDrawing = true;
|
|
break;
|
|
}
|
|
column++;
|
|
activeSoftWrapProcessed = true;
|
|
}
|
|
}
|
|
FoldRegion region = state.getCurrentFold();
|
|
if (region != null) {
|
|
char[] placeholder = region.getPlaceholderText().toCharArray();
|
|
for (char aPlaceholder : placeholder) {
|
|
c = aPlaceholder;
|
|
x += EditorUtil.charWidth(c, fontType, this);
|
|
if (x >= px) {
|
|
break outer;
|
|
}
|
|
column++;
|
|
}
|
|
offset = region.getEndOffset();
|
|
}
|
|
else {
|
|
prevX = x;
|
|
c = text.charAt(offset);
|
|
if (c == '\n') {
|
|
break;
|
|
}
|
|
charWidth = charToVisibleWidth(c, fontType, x);
|
|
x += charWidth;
|
|
|
|
if (x >= px) {
|
|
break;
|
|
}
|
|
column += EditorUtil.columnsNumber(c, x, prevX, plainSpaceSize);
|
|
|
|
offset++;
|
|
}
|
|
}
|
|
|
|
if (charWidth < 0) {
|
|
charWidth = EditorUtil.charWidth(c, fontType, this);
|
|
}
|
|
|
|
if (charWidth < 0) {
|
|
charWidth = plainSpaceSize;
|
|
}
|
|
|
|
if (x >= px && c == '\t' && !onSoftWrapDrawing) {
|
|
if (mySettings.isCaretInsideTabs()) {
|
|
column += (px - prevX) / plainSpaceSize;
|
|
if ((px - prevX) % plainSpaceSize > plainSpaceSize / 2) column++;
|
|
}
|
|
else if ((x - px) * 2 < x - prevX) {
|
|
column += EditorUtil.columnsNumber(c, x, prevX, plainSpaceSize);
|
|
}
|
|
}
|
|
else {
|
|
if (x >= px) {
|
|
if (c != '\n' && (x - px) * 2 < charWidth) column++;
|
|
}
|
|
else {
|
|
int diff = px - x;
|
|
column += diff / plainSpaceSize;
|
|
if (diff % plainSpaceSize * 2 >= plainSpaceSize) {
|
|
column++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return new VisualPosition(line, column);
|
|
}
|
|
|
|
/**
|
|
* Allows to answer how much width requires given char to be represented on a screen.
|
|
*
|
|
* @param c target character
|
|
* @param fontType font type to use for representation of the given character
|
|
* @param currentX current <code>'x'</code> position on a line where given character should be displayed
|
|
* @return width required to represent given char with the given settings on a screen;
|
|
* <code>'0'</code> if given char is a line break
|
|
*/
|
|
private int charToVisibleWidth(char c, @JdkConstants.FontStyle int fontType, int currentX) {
|
|
if (c == '\n') {
|
|
return 0;
|
|
}
|
|
|
|
if (c == '\t') {
|
|
return EditorUtil.nextTabStop(currentX, this) - currentX;
|
|
}
|
|
return EditorUtil.charWidth(c, fontType, this);
|
|
}
|
|
|
|
@NotNull
|
|
public Point offsetToXY(int offset, boolean leanTowardsLargerOffsets) {
|
|
return myUseNewRendering ? myView.offsetToXY(offset, leanTowardsLargerOffsets) :
|
|
visualPositionToXY(offsetToVisualPosition(offset, leanTowardsLargerOffsets));
|
|
}
|
|
|
|
@Override
|
|
@NotNull
|
|
public VisualPosition offsetToVisualPosition(int offset) {
|
|
return offsetToVisualPosition(offset, false);
|
|
}
|
|
|
|
@Override
|
|
@NotNull
|
|
public VisualPosition offsetToVisualPosition(int offset, boolean leanForward) {
|
|
if (myUseNewRendering) return myView.offsetToVisualPosition(offset, leanForward);
|
|
return logicalToVisualPosition(offsetToLogicalPosition(offset));
|
|
}
|
|
|
|
@Override
|
|
@NotNull
|
|
public LogicalPosition offsetToLogicalPosition(int offset) {
|
|
return offsetToLogicalPosition(offset, true);
|
|
}
|
|
|
|
@NotNull
|
|
@Override
|
|
public LogicalPosition offsetToLogicalPosition(int offset, boolean softWrapAware) {
|
|
if (myUseNewRendering) return myView.offsetToLogicalPosition(offset);
|
|
if (softWrapAware) {
|
|
return mySoftWrapModel.offsetToLogicalPosition(offset);
|
|
}
|
|
int line = offsetToLogicalLine(offset);
|
|
int column = calcColumnNumber(offset, line, false, myDocument.getImmutableCharSequence());
|
|
return new LogicalPosition(line, column);
|
|
}
|
|
|
|
@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) {
|
|
if (myUseNewRendering) return myView.offsetToVisualLine(offset);
|
|
int textLength = getDocument().getTextLength();
|
|
if (offset >= textLength) {
|
|
return Math.max(0, getVisibleLineCount() - 1); // lines are 0 based
|
|
}
|
|
int line = offsetToLogicalLine(offset);
|
|
int lineStartOffset = line >= myDocument.getLineCount() ? myDocument.getTextLength() : myDocument.getLineStartOffset(line);
|
|
|
|
int result = logicalToVisualLine(line);
|
|
|
|
// There is a possible case that logical line that contains target offset is soft-wrapped (represented in more than one visual
|
|
// line). Hence, we need to perform necessary adjustments to the visual line that is used to show logical line start if necessary.
|
|
int i = getSoftWrapModel().getSoftWrapIndex(lineStartOffset);
|
|
if (i < 0) {
|
|
i = -i - 1;
|
|
}
|
|
List<? extends SoftWrap> softWraps = getSoftWrapModel().getRegisteredSoftWraps();
|
|
for (; i < softWraps.size(); i++) {
|
|
SoftWrap softWrap = softWraps.get(i);
|
|
if (softWrap.getStart() > offset) {
|
|
break;
|
|
}
|
|
result++; // Assuming that every soft wrap contains only one virtual line feed symbol
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private int logicalToVisualLine(int line) {
|
|
assertReadAccess();
|
|
return logicalToVisualPosition(new LogicalPosition(line, 0)).line;
|
|
}
|
|
|
|
@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) {
|
|
VisualPosition visible = logicalToVisualPosition(new LogicalPosition(line, 0));
|
|
return visibleLineToY(visible.line);
|
|
}
|
|
|
|
@Override
|
|
@NotNull
|
|
public Point logicalPositionToXY(@NotNull LogicalPosition pos) {
|
|
VisualPosition visible = logicalToVisualPosition(pos);
|
|
return visualPositionToXY(visible);
|
|
}
|
|
|
|
@Override
|
|
@NotNull
|
|
public Point visualPositionToXY(@NotNull VisualPosition visible) {
|
|
if (myUseNewRendering) return myView.visualPositionToXY(visible);
|
|
int y = visibleLineToY(visible.line);
|
|
LogicalPosition logical = visualToLogicalPosition(new VisualPosition(visible.line, 0));
|
|
int logLine = logical.line;
|
|
|
|
int lineStartOffset = -1;
|
|
int reserved = 0;
|
|
int column = visible.column;
|
|
|
|
if (logical.softWrapLinesOnCurrentLogicalLine > 0) {
|
|
int linesToSkip = logical.softWrapLinesOnCurrentLogicalLine;
|
|
List<? extends SoftWrap> softWraps = getSoftWrapModel().getSoftWrapsForLine(logLine);
|
|
for (SoftWrap softWrap : softWraps) {
|
|
if (myFoldingModel.isOffsetCollapsed(softWrap.getStart()) && myFoldingModel.isOffsetCollapsed(softWrap.getStart() - 1)) {
|
|
continue;
|
|
}
|
|
linesToSkip--; // Assuming here that every soft wrap has exactly one line feed
|
|
if (linesToSkip > 0) {
|
|
continue;
|
|
}
|
|
lineStartOffset = softWrap.getStart();
|
|
int widthInColumns = softWrap.getIndentInColumns();
|
|
int widthInPixels = softWrap.getIndentInPixels();
|
|
if (widthInColumns <= column) {
|
|
column -= widthInColumns;
|
|
reserved = widthInPixels;
|
|
}
|
|
else {
|
|
char[] softWrapChars = softWrap.getChars();
|
|
int i = CharArrayUtil.lastIndexOf(softWrapChars, '\n', 0, softWrapChars.length);
|
|
int start = 0;
|
|
if (i >= 0) {
|
|
start = i + 1;
|
|
}
|
|
return new Point(EditorUtil.textWidth(this, softWrap.getText(), start, column + 1, Font.PLAIN, 0), y);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (logLine < 0) {
|
|
lineStartOffset = 0;
|
|
}
|
|
else if (lineStartOffset < 0) {
|
|
if (logLine >= myDocument.getLineCount()) {
|
|
lineStartOffset = myDocument.getTextLength();
|
|
}
|
|
else {
|
|
lineStartOffset = myDocument.getLineStartOffset(logLine);
|
|
}
|
|
}
|
|
|
|
int x = getTabbedTextWidth(lineStartOffset, column, reserved);
|
|
return new Point(x, y);
|
|
}
|
|
|
|
private int calcEndOffset(int startOffset, int visualColumn) {
|
|
FoldRegion[] regions = myFoldingModel.fetchTopLevel();
|
|
if (regions == null) {
|
|
return startOffset + visualColumn;
|
|
}
|
|
|
|
int low = 0;
|
|
int high = regions.length - 1;
|
|
int i = -1;
|
|
|
|
while (low <= high) {
|
|
int mid = low + high >>> 1;
|
|
FoldRegion midVal = regions[mid];
|
|
|
|
if (midVal.getStartOffset() <= startOffset && midVal.getEndOffset() > startOffset) {
|
|
i = mid;
|
|
break;
|
|
}
|
|
|
|
if (midVal.getStartOffset() < startOffset) {
|
|
low = mid + 1;
|
|
}
|
|
else if (midVal.getStartOffset() > startOffset) {
|
|
high = mid - 1;
|
|
}
|
|
}
|
|
if (i < 0) {
|
|
i = low;
|
|
}
|
|
|
|
int result = startOffset;
|
|
int columnsToProcess = visualColumn;
|
|
for (; i < regions.length; i++) {
|
|
FoldRegion region = regions[i];
|
|
|
|
// Process text between the last fold region end and current fold region start.
|
|
int nonFoldTextColumnsNumber = region.getStartOffset() - result;
|
|
if (nonFoldTextColumnsNumber >= columnsToProcess) {
|
|
return result + columnsToProcess;
|
|
}
|
|
columnsToProcess -= nonFoldTextColumnsNumber;
|
|
|
|
// Process fold region.
|
|
int placeHolderLength = region.getPlaceholderText().length();
|
|
if (placeHolderLength >= columnsToProcess) {
|
|
return region.getEndOffset();
|
|
}
|
|
result = region.getEndOffset();
|
|
columnsToProcess -= placeHolderLength;
|
|
}
|
|
return result + columnsToProcess;
|
|
}
|
|
|
|
public int findNearestDirectionBoundary(int offset, boolean lookForward) {
|
|
return myUseNewRendering ? myView.findNearestDirectionBoundary(offset, lookForward) : -1;
|
|
}
|
|
|
|
// TODO: tabbed text width is additive, it should be possible to have buckets, containing arguments / values to start with
|
|
private final int[] myLastStartOffsets = new int[2];
|
|
private final int[] myLastTargetColumns = new int[myLastStartOffsets.length];
|
|
private final int[] myLastXOffsets = new int[myLastStartOffsets.length];
|
|
private final int[] myLastXs = new int[myLastStartOffsets.length];
|
|
private int myCurrentCachePosition;
|
|
private int myLastCacheHits;
|
|
private int myTotalRequests; // todo remove
|
|
|
|
private int getTabbedTextWidth(int startOffset, int targetColumn, int xOffset) {
|
|
int x = xOffset;
|
|
if (startOffset == 0 && myPrefixText != null) {
|
|
x += myPrefixWidthInPixels;
|
|
}
|
|
if (targetColumn <= 0) return x;
|
|
|
|
++myTotalRequests;
|
|
for (int i = 0; i < myLastStartOffsets.length; ++i) {
|
|
if (startOffset == myLastStartOffsets[i] && targetColumn == myLastTargetColumns[i] && xOffset == myLastXOffsets[i]) {
|
|
++myLastCacheHits;
|
|
if ((myLastCacheHits & 0xFFF) == 0) { // todo remove
|
|
PsiFile file = myProject != null ? PsiDocumentManager.getInstance(myProject).getCachedPsiFile(myDocument) : null;
|
|
LOG.info("Cache hits:" + myLastCacheHits + ", total requests:" +
|
|
myTotalRequests + "," + (file != null ? file.getViewProvider().getVirtualFile() : null));
|
|
}
|
|
return myLastXs[i];
|
|
}
|
|
}
|
|
|
|
int offset = startOffset;
|
|
CharSequence text = myDocument.getImmutableCharSequence();
|
|
int textLength = myDocument.getTextLength();
|
|
|
|
// We need to calculate max offset to provide to the IterationState here based on the given start offset and target
|
|
// visual column. The problem is there is a possible case that there is a collapsed fold region at the target interval,
|
|
// so, we can't just use 'startOffset + targetColumn' as a max end offset.
|
|
IterationState state = new IterationState(this, startOffset, calcEndOffset(startOffset, targetColumn), false);
|
|
int fontType = state.getMergedAttributes().getFontType();
|
|
int plainSpaceSize = EditorUtil.getSpaceWidth(Font.PLAIN, this);
|
|
|
|
int column = 0;
|
|
outer:
|
|
while (column < targetColumn) {
|
|
if (offset >= textLength) break;
|
|
|
|
if (offset >= state.getEndOffset()) {
|
|
state.advance();
|
|
fontType = state.getMergedAttributes().getFontType();
|
|
}
|
|
// We need to consider 'before soft wrap drawing'.
|
|
SoftWrap softWrap = getSoftWrapModel().getSoftWrap(offset);
|
|
if (softWrap != null && offset > startOffset) {
|
|
column++;
|
|
x += getSoftWrapModel().getMinDrawingWidthInPixels(SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED);
|
|
// Assuming that first soft wrap symbol is line feed or all soft wrap symbols before the first line feed are spaces.
|
|
break;
|
|
}
|
|
|
|
FoldRegion region = state.getCurrentFold();
|
|
|
|
if (region != null) {
|
|
char[] placeholder = region.getPlaceholderText().toCharArray();
|
|
for (char aPlaceholder : placeholder) {
|
|
x += EditorUtil.charWidth(aPlaceholder, fontType, this);
|
|
column++;
|
|
if (column >= targetColumn) break outer;
|
|
}
|
|
offset = region.getEndOffset();
|
|
}
|
|
else {
|
|
char c = text.charAt(offset);
|
|
if (c == '\n') {
|
|
break;
|
|
}
|
|
if (c == '\t') {
|
|
int prevX = x;
|
|
x = EditorUtil.nextTabStop(x, this);
|
|
int columnDiff = (x - prevX) / plainSpaceSize;
|
|
if ((x - prevX) % plainSpaceSize > 0) {
|
|
// There is a possible case that tabulation symbol takes more than one visual column to represent and it's shown at
|
|
// soft-wrapped line. Soft wrap sign width may be not divisible by space size, hence, part of tabulation symbol represented
|
|
// as a separate visual column may take less space than space width.
|
|
columnDiff++;
|
|
}
|
|
column += columnDiff;
|
|
}
|
|
else {
|
|
x += EditorUtil.charWidth(c, fontType, this);
|
|
column++;
|
|
}
|
|
offset++;
|
|
}
|
|
}
|
|
|
|
if (column != targetColumn) {
|
|
x += EditorUtil.getSpaceWidth(fontType, this) * (targetColumn - column);
|
|
}
|
|
|
|
myLastTargetColumns[myCurrentCachePosition] = targetColumn;
|
|
myLastStartOffsets[myCurrentCachePosition] = startOffset;
|
|
myLastXs[myCurrentCachePosition] = x;
|
|
myLastXOffsets[myCurrentCachePosition] = xOffset;
|
|
myCurrentCachePosition = (myCurrentCachePosition + 1) % myLastStartOffsets.length;
|
|
|
|
return x;
|
|
}
|
|
|
|
private void clearTextWidthCache() {
|
|
for (int i = 0; i < myLastStartOffsets.length; ++i) {
|
|
myLastTargetColumns[i] = -1;
|
|
myLastStartOffsets[i] = -1;
|
|
myLastXs[i] = -1;
|
|
myLastXOffsets[i] = -1;
|
|
}
|
|
}
|
|
|
|
public int visibleLineToY(int line) {
|
|
if (myUseNewRendering) return myView.visualLineToY(line);
|
|
if (line < 0) throw new IndexOutOfBoundsException("Wrong line: " + line);
|
|
return line * getLineHeight();
|
|
}
|
|
|
|
@Override
|
|
public void repaint(int startOffset, int endOffset) {
|
|
repaint(startOffset, endOffset, true);
|
|
}
|
|
|
|
void repaint(int startOffset, int endOffset, boolean invalidateTextLayout) {
|
|
if (myDocument.isInBulkUpdate()) {
|
|
return;
|
|
}
|
|
if (myUseNewRendering) {
|
|
assertIsDispatchThread();
|
|
endOffset = Math.min(endOffset, myDocument.getTextLength());
|
|
|
|
if (invalidateTextLayout) {
|
|
myView.invalidateRange(startOffset, endOffset);
|
|
}
|
|
|
|
if (!isShowing()) {
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
if (!isShowing()) {
|
|
return;
|
|
}
|
|
|
|
endOffset = Math.min(endOffset, myDocument.getTextLength());
|
|
assertIsDispatchThread();
|
|
}
|
|
// 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]</code> range.
|
|
*
|
|
* @param startLine start logical line to repaint (inclusive)
|
|
* @param endLine end logical line to repaint (inclusive)
|
|
*/
|
|
public void repaintLines(int startLine, int endLine) {
|
|
if (!isShowing()) return;
|
|
|
|
Rectangle visibleArea = getScrollingModel().getVisibleArea();
|
|
int yStartLine = logicalLineToY(startLine);
|
|
int endVisLine;
|
|
if (myDocument.getTextLength() <= 0) {
|
|
endVisLine = 0;
|
|
}
|
|
else {
|
|
endVisLine = 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() {
|
|
saveCaretRelativePosition();
|
|
|
|
myCaretModel.onBulkDocumentUpdateStarted();
|
|
mySoftWrapModel.onBulkDocumentUpdateStarted();
|
|
myFoldingModel.onBulkDocumentUpdateStarted();
|
|
}
|
|
|
|
private void bulkUpdateFinished() {
|
|
myFoldingModel.onBulkDocumentUpdateFinished();
|
|
mySoftWrapModel.onBulkDocumentUpdateFinished();
|
|
myCaretModel.onBulkDocumentUpdateFinished();
|
|
|
|
clearTextWidthCache();
|
|
|
|
setMouseSelectionState(MOUSE_SELECTION_STATE_NONE);
|
|
|
|
if (myUseNewRendering) {
|
|
myView.reset();
|
|
}
|
|
else {
|
|
mySizeContainer.reset();
|
|
}
|
|
validateSize();
|
|
|
|
updateGutterSize();
|
|
repaintToScreenBottom(0);
|
|
updateCaretCursor();
|
|
|
|
restoreCaretRelativePosition();
|
|
}
|
|
|
|
private void beforeChangedUpdate(@NotNull DocumentEvent e) {
|
|
myDocumentChangeInProgress = true;
|
|
if (isStickySelection()) {
|
|
setStickySelection(false);
|
|
}
|
|
if (myDocument.isInBulkUpdate()) {
|
|
// Assuming that the job is done at bulk listener callback methods.
|
|
return;
|
|
}
|
|
|
|
saveCaretRelativePosition();
|
|
|
|
// We assume that size container is already notified with the visual line widths during soft wraps processing
|
|
if (!mySoftWrapModel.isSoftWrappingEnabled() && !myUseNewRendering) {
|
|
mySizeContainer.beforeChange(e);
|
|
}
|
|
}
|
|
|
|
private void changedUpdate(DocumentEvent e) {
|
|
myDocumentChangeInProgress = false;
|
|
if (myDocument.isInBulkUpdate()) return;
|
|
|
|
if (myErrorStripeNeedsRepaint) {
|
|
myMarkupModel.repaint(e.getOffset(), e.getOffset() + e.getNewLength());
|
|
myErrorStripeNeedsRepaint = false;
|
|
}
|
|
|
|
clearTextWidthCache();
|
|
setMouseSelectionState(MOUSE_SELECTION_STATE_NONE);
|
|
|
|
// We assume that size container is already notified with the visual line widths during soft wraps processing
|
|
if (!mySoftWrapModel.isSoftWrappingEnabled() && !myUseNewRendering) {
|
|
mySizeContainer.changedUpdate(e);
|
|
}
|
|
validateSize();
|
|
|
|
int startLine = offsetToLogicalLine(e.getOffset());
|
|
int endLine = offsetToLogicalLine(e.getOffset() + e.getNewLength());
|
|
|
|
boolean painted = false;
|
|
if (myDocument.getTextLength() > 0) {
|
|
int startDocLine = myDocument.getLineNumber(e.getOffset());
|
|
int endDocLine = myDocument.getLineNumber(e.getOffset() + e.getNewLength());
|
|
if (e.getOldLength() > e.getNewLength() || startDocLine != endDocLine || StringUtil.indexOf(e.getOldFragment(), '\n') != -1) {
|
|
updateGutterSize();
|
|
}
|
|
|
|
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 (getCaretModel().getOffset() < e.getOffset() || getCaretModel().getOffset() > e.getOffset() + e.getNewLength()) {
|
|
restoreCaretRelativePosition();
|
|
}
|
|
}
|
|
|
|
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 hasTabs() {
|
|
return !(myDocument instanceof DocumentImpl) || ((DocumentImpl)myDocument).mightContainTabs();
|
|
}
|
|
|
|
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;
|
|
SwingUtilities.invokeLater(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
if (!isDisposed()) {
|
|
myGutterComponent.updateSize();
|
|
}
|
|
}
|
|
finally {
|
|
updatingSize = false;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void validateSize() {
|
|
Dimension dim = getPreferredSize();
|
|
|
|
if (!dim.equals(myPreferredSize) && !myDocument.isInBulkUpdate()) {
|
|
dim = mySizeAdjustmentStrategy.adjust(dim, myPreferredSize, this);
|
|
if (dim == null) {
|
|
return;
|
|
}
|
|
myPreferredSize = dim;
|
|
|
|
myGutterComponent.updateSize();
|
|
|
|
myEditorComponent.setSize(dim);
|
|
myEditorComponent.fireResized();
|
|
|
|
myMarkupModel.recalcEditorDimensions();
|
|
myMarkupModel.repaint(-1, -1);
|
|
}
|
|
}
|
|
|
|
void recalculateSizeAndRepaint() {
|
|
if (!myUseNewRendering) mySizeContainer.reset();
|
|
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;
|
|
final Runnable stopDumbRunnable = new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
stopDumb();
|
|
}
|
|
};
|
|
ApplicationManager.getApplication().invokeLater(stopDumbRunnable, ModalityState.current());
|
|
}
|
|
|
|
void resetPaintersWidth() {
|
|
myLinePaintersWidth = 0;
|
|
}
|
|
|
|
public 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(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 (myUpdateCursor) {
|
|
setCursorPosition();
|
|
myUpdateCursor = false;
|
|
}
|
|
|
|
if (isReleased) {
|
|
g.setColor(new JBColor(new Color(128, 255, 128), new Color(128, 255, 128)));
|
|
g.fillRect(clip.x, clip.y, clip.width, clip.height);
|
|
return;
|
|
}
|
|
if (myProject != null && myProject.isDisposed()) return;
|
|
|
|
if (myUseNewRendering) {
|
|
myView.paint(g);
|
|
}
|
|
else {
|
|
VisualPosition clipStartVisualPos = xyToVisualPosition(new Point(0, clip.y));
|
|
LogicalPosition clipStartPosition = visualToLogicalPosition(clipStartVisualPos);
|
|
int clipStartOffset = logicalPositionToOffset(clipStartPosition);
|
|
LogicalPosition clipEndPosition = xyToLogicalPosition(new Point(0, clip.y + clip.height + getLineHeight()));
|
|
int clipEndOffset = logicalPositionToOffset(clipEndPosition);
|
|
paintBackgrounds(g, clip, clipStartPosition, clipStartVisualPos, clipStartOffset, clipEndOffset);
|
|
if (paintPlaceholderText(g, clip)) {
|
|
paintCaretCursor(g);
|
|
return;
|
|
}
|
|
|
|
paintRightMargin(g, clip);
|
|
paintCustomRenderers(g, clipStartOffset, clipEndOffset);
|
|
MarkupModelEx docMarkup = (MarkupModelEx)DocumentMarkupModel.forDocument(myDocument, myProject, true);
|
|
paintLineMarkersSeparators(g, clip, docMarkup, clipStartOffset, clipEndOffset);
|
|
paintLineMarkersSeparators(g, clip, myMarkupModel, clipStartOffset, clipEndOffset);
|
|
paintText(g, clip, clipStartPosition, clipStartOffset, clipEndOffset);
|
|
paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip, clipStartOffset, clipEndOffset, docMarkup);
|
|
BorderEffect borderEffect = new BorderEffect(this, g, clipStartOffset, clipEndOffset);
|
|
borderEffect.paintHighlighters(getHighlighter());
|
|
borderEffect.paintHighlighters(docMarkup);
|
|
borderEffect.paintHighlighters(myMarkupModel);
|
|
|
|
paintCaretCursor(g);
|
|
|
|
paintComposedTextDecoration(g);
|
|
}
|
|
}
|
|
|
|
private static final char IDEOGRAPHIC_SPACE = '\u3000'; // http://www.marathon-studios.com/unicode/U3000/Ideographic_Space
|
|
private static final String WHITESPACE_CHARS = " \t" + IDEOGRAPHIC_SPACE;
|
|
|
|
private void paintCustomRenderers(@NotNull final Graphics2D g, final int clipStartOffset, final int clipEndOffset) {
|
|
myMarkupModel.processRangeHighlightersOverlappingWith(clipStartOffset, clipEndOffset, new Processor<RangeHighlighterEx>() {
|
|
@Override
|
|
public boolean process(@NotNull RangeHighlighterEx highlighter) {
|
|
if (!highlighter.getEditorFilter().avaliableIn(EditorImpl.this)) return true;
|
|
|
|
final CustomHighlighterRenderer customRenderer = highlighter.getCustomRenderer();
|
|
if (customRenderer != null && clipStartOffset < highlighter.getEndOffset() && highlighter.getStartOffset() < clipEndOffset) {
|
|
customRenderer.paint(EditorImpl.this, highlighter, g);
|
|
}
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
@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
|
|
private 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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
private void paintComposedTextDecoration(@NotNull Graphics2D g) {
|
|
TextRange composedTextRange = getComposedTextRange();
|
|
if (composedTextRange != null) {
|
|
VisualPosition visStart = offsetToVisualPosition(Math.min(composedTextRange.getStartOffset(), myDocument.getTextLength()));
|
|
int y = visibleLineToY(visStart.line) + getAscent() + 1;
|
|
Point p1 = visualPositionToXY(visStart);
|
|
Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(composedTextRange.getEndOffset(), myDocument.getTextLength())));
|
|
|
|
Stroke saved = g.getStroke();
|
|
BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2, 0, 2}, 0);
|
|
g.setStroke(dotted);
|
|
UIUtil.drawLine(g, p1.x, y, p2.x, y);
|
|
g.setStroke(saved);
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
public TextRange getComposedTextRange() {
|
|
return myInputMethodRequestsHandler == null || myInputMethodRequestsHandler.composedText == null ?
|
|
null : myInputMethodRequestsHandler.composedTextRange;
|
|
}
|
|
|
|
private void paintRightMargin(@NotNull Graphics g, @NotNull Rectangle clip) {
|
|
Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR);
|
|
if (!mySettings.isRightMarginShown() || rightMargin == null) {
|
|
return;
|
|
}
|
|
int x = mySettings.getRightMargin(myProject) * EditorUtil.getSpaceWidth(Font.PLAIN, this);
|
|
if (x >= clip.x && x < clip.x + clip.width) {
|
|
g.setColor(rightMargin);
|
|
UIUtil.drawLine(g, x, clip.y, x, clip.y + clip.height);
|
|
}
|
|
}
|
|
|
|
private void paintSegmentHighlightersBorderAndAfterEndOfLine(@NotNull final Graphics g,
|
|
@NotNull Rectangle clip,
|
|
int clipStartOffset,
|
|
int clipEndOffset,
|
|
@NotNull MarkupModelEx docMarkup) {
|
|
if (myDocument.getLineCount() == 0) return;
|
|
final int startLine = yPositionToVisibleLine(clip.y);
|
|
final int endLine = yPositionToVisibleLine(clip.y + clip.height) + 1;
|
|
|
|
Processor<RangeHighlighterEx> paintProcessor = new Processor<RangeHighlighterEx>() {
|
|
@Override
|
|
public boolean process(@NotNull RangeHighlighterEx highlighter) {
|
|
if (!highlighter.getEditorFilter().avaliableIn(EditorImpl.this)) return true;
|
|
|
|
paintSegmentHighlighterAfterEndOfLine(g, highlighter, startLine, endLine);
|
|
return true;
|
|
}
|
|
};
|
|
docMarkup.processRangeHighlightersOverlappingWith(clipStartOffset, clipEndOffset, paintProcessor);
|
|
myMarkupModel.processRangeHighlightersOverlappingWith(clipStartOffset, clipEndOffset, paintProcessor);
|
|
}
|
|
|
|
private void paintSegmentHighlighterAfterEndOfLine(@NotNull Graphics g,
|
|
@NotNull RangeHighlighterEx segmentHighlighter,
|
|
int startLine,
|
|
int endLine) {
|
|
if (!segmentHighlighter.isAfterEndOfLine()) {
|
|
return;
|
|
}
|
|
int startOffset = segmentHighlighter.getStartOffset();
|
|
int visibleStartLine = offsetToVisualLine(startOffset);
|
|
|
|
if (getFoldingModel().isOffsetCollapsed(startOffset)) {
|
|
return;
|
|
}
|
|
if (visibleStartLine >= startLine && visibleStartLine <= endLine) {
|
|
int logStartLine = offsetToLogicalLine(startOffset);
|
|
if (logStartLine >= myDocument.getLineCount()) {
|
|
return;
|
|
}
|
|
LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine));
|
|
Point end = logicalPositionToXY(logPosition);
|
|
int charWidth = EditorUtil.getSpaceWidth(Font.PLAIN, this);
|
|
int lineHeight = getLineHeight();
|
|
TextAttributes attributes = segmentHighlighter.getTextAttributes();
|
|
if (attributes != null && getBackgroundColor(attributes) != null) {
|
|
g.setColor(getBackgroundColor(attributes));
|
|
g.fillRect(end.x, end.y, charWidth, lineHeight);
|
|
}
|
|
if (attributes != null && attributes.getEffectColor() != null) {
|
|
int y = visibleLineToY(visibleStartLine) + getAscent() + 1;
|
|
g.setColor(attributes.getEffectColor());
|
|
if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) {
|
|
UIUtil.drawWave((Graphics2D)g, new Rectangle(end.x, y, charWidth - 1, getDescent() - 1));
|
|
}
|
|
else if (attributes.getEffectType() == EffectType.BOLD_DOTTED_LINE) {
|
|
final int dottedAt = SystemInfo.isMac ? y - 1 : y;
|
|
UIUtil.drawBoldDottedLine((Graphics2D)g, end.x, end.x + charWidth - 1, dottedAt,
|
|
getBackgroundColor(attributes), attributes.getEffectColor(), false);
|
|
}
|
|
else if (attributes.getEffectType() == EffectType.STRIKEOUT) {
|
|
int y1 = y - getCharHeight() / 2 - 1;
|
|
UIUtil.drawLine(g, end.x, y1, end.x + charWidth - 1, y1);
|
|
}
|
|
else if (attributes.getEffectType() == EffectType.BOLD_LINE_UNDERSCORE) {
|
|
drawBoldLineUnderScore(g, end.x, y - 1, charWidth - 1);
|
|
}
|
|
else if (attributes.getEffectType() != EffectType.BOXED) {
|
|
UIUtil.drawLine(g, end.x, y, end.x + charWidth - 1, y);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void drawBoldLineUnderScore(Graphics g, int x, int y, int width) {
|
|
int height = JBUI.scale(Registry.intValue("editor.bold.underline.height", 2));
|
|
g.fillRect(x, y, width, height);
|
|
}
|
|
|
|
@Override
|
|
public int getMaxWidthInRange(int startOffset, int endOffset) {
|
|
if (myUseNewRendering) return myView.getMaxWidthInRange(startOffset, endOffset);
|
|
int width = 0;
|
|
int start = offsetToVisualLine(startOffset);
|
|
int end = offsetToVisualLine(endOffset);
|
|
|
|
for (int i = start; i <= end; i++) {
|
|
int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1;
|
|
int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x;
|
|
|
|
if (lineWidth > width) {
|
|
width = lineWidth;
|
|
}
|
|
}
|
|
|
|
return width;
|
|
}
|
|
|
|
private void paintBackgrounds(@NotNull Graphics g,
|
|
@NotNull Rectangle clip,
|
|
@NotNull LogicalPosition clipStartPosition,
|
|
@NotNull VisualPosition clipStartVisualPos,
|
|
int clipStartOffset, int clipEndOffset) {
|
|
Color defaultBackground = getBackgroundColor();
|
|
if (myEditorComponent.isOpaque()) {
|
|
g.setColor(defaultBackground);
|
|
g.fillRect(clip.x, clip.y, clip.width, clip.height);
|
|
}
|
|
Color prevBackColor = null;
|
|
|
|
int lineHeight = getLineHeight();
|
|
|
|
int visibleLine = yPositionToVisibleLine(clip.y);
|
|
|
|
Point position = new Point(0, visibleLine * lineHeight);
|
|
CharSequence prefixText = myPrefixText == null ? null : new CharArrayCharSequence(myPrefixText);
|
|
if (clipStartVisualPos.line == 0 && prefixText != null) {
|
|
Color backColor = myPrefixAttributes.getBackgroundColor();
|
|
position.x = drawBackground(g, backColor, prefixText, 0, prefixText.length(), position,
|
|
myPrefixAttributes.getFontType(),
|
|
defaultBackground, clip);
|
|
prevBackColor = backColor;
|
|
}
|
|
|
|
if (clipStartPosition.line >= myDocument.getLineCount() || clipStartPosition.line < 0) {
|
|
if (position.x > 0) flushBackground(g, clip);
|
|
return;
|
|
}
|
|
|
|
myLastBackgroundPosition = null;
|
|
myLastBackgroundColor = null;
|
|
mySelectionStartPosition = null;
|
|
mySelectionEndPosition = null;
|
|
|
|
int start = clipStartOffset;
|
|
|
|
if (!myPurePaintingMode) {
|
|
getSoftWrapModel().registerSoftWrapsIfNecessary();
|
|
}
|
|
|
|
LineIterator lIterator = createLineIterator();
|
|
lIterator.start(start);
|
|
if (lIterator.atEnd()) {
|
|
return;
|
|
}
|
|
|
|
IterationState iterationState = new IterationState(this, start, clipEndOffset, isPaintSelection());
|
|
TextAttributes attributes = iterationState.getMergedAttributes();
|
|
Color backColor = getBackgroundColor(attributes);
|
|
int fontType = attributes.getFontType();
|
|
int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1);
|
|
|
|
// There is a possible case that we need to draw background from the start of soft wrap-introduced visual line. Given position
|
|
// has valid 'y' coordinate then at it shouldn't be affected by soft wrap that corresponds to the visual line start offset.
|
|
// Hence, we store information about soft wrap to be skipped for further processing and adjust 'x' coordinate value if necessary.
|
|
TIntHashSet softWrapsToSkip = new TIntHashSet();
|
|
SoftWrap softWrap = getSoftWrapModel().getSoftWrap(start);
|
|
if (softWrap != null) {
|
|
softWrapsToSkip.add(softWrap.getStart());
|
|
Color color = null;
|
|
if (backColor != null && !backColor.equals(defaultBackground)) {
|
|
color = backColor;
|
|
}
|
|
|
|
// There is a possible case that target clip points to soft wrap-introduced visual line and that it's an active
|
|
// line (caret cursor is located on it). We want to draw corresponding 'caret line' background for soft wraps-introduced
|
|
// virtual space then.
|
|
if (color == null && position.y == getCaretModel().getVisualPosition().line * getLineHeight()) {
|
|
color = mySettings.isCaretRowShown() ? getColorsScheme().getColor(EditorColors.CARET_ROW_COLOR) : null;
|
|
}
|
|
|
|
if (color != null) {
|
|
drawBackground(g, color, softWrap.getIndentInPixels(), position, defaultBackground, clip);
|
|
prevBackColor = color;
|
|
}
|
|
position.x = softWrap.getIndentInPixels();
|
|
}
|
|
|
|
// There is a possible case that caret is located at soft-wrapped line. We don't need to paint caret row background
|
|
// on a last visual line of that soft-wrapped line then. Below is a holder for the flag that indicates if caret row
|
|
// background is already drawn.
|
|
boolean[] caretRowPainted = new boolean[1];
|
|
|
|
CharSequence text = myDocument.getImmutableCharSequence();
|
|
|
|
while (!iterationState.atEnd() && !lIterator.atEnd()) {
|
|
int hEnd = iterationState.getEndOffset();
|
|
int lEnd = lIterator.getEnd();
|
|
|
|
if (hEnd >= lEnd) {
|
|
FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start);
|
|
if (collapsedFolderAt == null) {
|
|
position.x = drawSoftWrapAwareBackground(g, backColor, prevBackColor, text, start, lEnd - lIterator.getSeparatorLength(),
|
|
position, fontType, defaultBackground, clip, softWrapsToSkip, caretRowPainted);
|
|
prevBackColor = backColor;
|
|
|
|
paintAfterLineEndBackgroundSegments(g, iterationState, position, defaultBackground, lineHeight);
|
|
|
|
if (lIterator.getLineNumber() < lastLineIndex) {
|
|
if (backColor != null && !backColor.equals(defaultBackground)) {
|
|
g.setColor(backColor);
|
|
g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight);
|
|
}
|
|
}
|
|
else {
|
|
if (iterationState.hasPastFileEndBackgroundSegments()) {
|
|
paintAfterLineEndBackgroundSegments(g, iterationState, position, defaultBackground, lineHeight);
|
|
}
|
|
paintAfterFileEndBackground(iterationState,
|
|
g,
|
|
position, clip,
|
|
lineHeight, defaultBackground, caretRowPainted);
|
|
break;
|
|
}
|
|
|
|
position.x = 0;
|
|
if (position.y > clip.y + clip.height) break;
|
|
position.y += lineHeight;
|
|
start = lEnd;
|
|
}
|
|
else if (collapsedFolderAt.getEndOffset() == clipEndOffset) {
|
|
softWrap = mySoftWrapModel.getSoftWrap(collapsedFolderAt.getStartOffset());
|
|
if (softWrap != null) {
|
|
position.x = drawSoftWrapAwareBackground(
|
|
g, backColor, prevBackColor, text, collapsedFolderAt.getStartOffset(), collapsedFolderAt.getStartOffset(), position, fontType,
|
|
defaultBackground, clip, softWrapsToSkip, caretRowPainted
|
|
);
|
|
}
|
|
CharSequence chars = collapsedFolderAt.getPlaceholderText();
|
|
position.x = drawBackground(g, backColor, chars, 0, chars.length(), position, fontType, defaultBackground, clip);
|
|
prevBackColor = backColor;
|
|
}
|
|
|
|
lIterator.advance();
|
|
}
|
|
else {
|
|
FoldRegion collapsedFolderAt = iterationState.getCurrentFold();
|
|
if (collapsedFolderAt != null) {
|
|
softWrap = mySoftWrapModel.getSoftWrap(collapsedFolderAt.getStartOffset());
|
|
if (softWrap != null) {
|
|
position.x = drawSoftWrapAwareBackground(
|
|
g, backColor, prevBackColor, text, collapsedFolderAt.getStartOffset(), collapsedFolderAt.getStartOffset(), position, fontType,
|
|
defaultBackground, clip, softWrapsToSkip, caretRowPainted
|
|
);
|
|
}
|
|
CharSequence chars = collapsedFolderAt.getPlaceholderText();
|
|
position.x = drawBackground(g, backColor, chars, 0, chars.length(), position, fontType, defaultBackground, clip);
|
|
prevBackColor = backColor;
|
|
}
|
|
else if (hEnd > lEnd - lIterator.getSeparatorLength()) {
|
|
position.x = drawSoftWrapAwareBackground(
|
|
g, backColor, prevBackColor, text, start, lEnd - lIterator.getSeparatorLength(), position, fontType,
|
|
defaultBackground, clip, softWrapsToSkip, caretRowPainted
|
|
);
|
|
prevBackColor = backColor;
|
|
}
|
|
else {
|
|
position.x = drawSoftWrapAwareBackground(
|
|
g, backColor, prevBackColor, text, start, hEnd, position, fontType, defaultBackground, clip, softWrapsToSkip, caretRowPainted
|
|
);
|
|
prevBackColor = backColor;
|
|
}
|
|
|
|
iterationState.advance();
|
|
attributes = iterationState.getMergedAttributes();
|
|
backColor = getBackgroundColor(attributes);
|
|
fontType = attributes.getFontType();
|
|
start = iterationState.getStartOffset();
|
|
}
|
|
}
|
|
|
|
flushBackground(g, clip);
|
|
|
|
if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) {
|
|
paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight, defaultBackground, caretRowPainted);
|
|
}
|
|
|
|
// Perform additional activity if soft wrap is added or removed during repainting.
|
|
if (mySoftWrapsChanged) {
|
|
mySoftWrapsChanged = false;
|
|
clearTextWidthCache();
|
|
validateSize();
|
|
|
|
// Repaint editor to the bottom in order to ensure that its content is shown correctly after new soft wrap introduction.
|
|
repaintToScreenBottom(EditorUtil.yPositionToLogicalLine(this, position));
|
|
|
|
// Repaint gutter at all space that is located after active clip in order to ensure that line numbers are correctly redrawn
|
|
// in accordance with the newly introduced soft wrap(s).
|
|
myGutterComponent.repaint(0, clip.y, myGutterComponent.getWidth(), myGutterComponent.getHeight() - clip.y);
|
|
}
|
|
}
|
|
|
|
private void paintAfterLineEndBackgroundSegments(@NotNull Graphics g,
|
|
@NotNull IterationState iterationState,
|
|
@NotNull Point position,
|
|
@NotNull Color defaultBackground,
|
|
int lineHeight) {
|
|
while (iterationState.hasPastLineEndBackgroundSegment()) {
|
|
TextAttributes backgroundAttributes = iterationState.getPastLineEndBackgroundAttributes();
|
|
int width =
|
|
EditorUtil.getSpaceWidth(backgroundAttributes.getFontType(), this) * iterationState.getPastLineEndBackgroundSegmentWidth();
|
|
Color color = getBackgroundColor(backgroundAttributes);
|
|
if (color != null && !color.equals(defaultBackground)) {
|
|
g.setColor(color);
|
|
g.fillRect(position.x, position.y, width, lineHeight);
|
|
}
|
|
position.x += width;
|
|
iterationState.advanceToNextPastLineEndBackgroundSegment();
|
|
}
|
|
}
|
|
|
|
private void paintAfterFileEndBackground(@NotNull IterationState iterationState,
|
|
@NotNull Graphics g,
|
|
@NotNull Point position,
|
|
@NotNull Rectangle clip,
|
|
int lineHeight,
|
|
@NotNull Color defaultBackground,
|
|
@NotNull boolean[] caretRowPainted) {
|
|
Color backColor = iterationState.getPastFileEndBackground();
|
|
if (backColor == null || backColor.equals(defaultBackground)) {
|
|
return;
|
|
}
|
|
if (caretRowPainted[0] && backColor.equals(getColorsScheme().getColor(EditorColors.CARET_ROW_COLOR))) {
|
|
return;
|
|
}
|
|
g.setColor(backColor);
|
|
g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight);
|
|
}
|
|
|
|
private int drawSoftWrapAwareBackground(@NotNull Graphics g,
|
|
@Nullable Color backColor,
|
|
@Nullable Color prevBackColor,
|
|
@NotNull CharSequence text,
|
|
int start,
|
|
int end,
|
|
@NotNull Point position,
|
|
@JdkConstants.FontStyle int fontType,
|
|
@NotNull Color defaultBackground,
|
|
@NotNull Rectangle clip,
|
|
@NotNull TIntHashSet softWrapsToSkip,
|
|
@NotNull boolean[] caretRowPainted) {
|
|
int startToUse = start;
|
|
// Given 'end' offset is exclusive though SoftWrapModel.getSoftWrapsForRange() uses inclusive end offset.
|
|
// Hence, we decrement it if necessary. Please note that we don't do that if start is equal to end. That is the case,
|
|
// for example, for soft-wrapped collapsed fold region - we need to draw soft wrap before it.
|
|
int softWrapRetrievalEndOffset = end;
|
|
if (end > start) {
|
|
softWrapRetrievalEndOffset--;
|
|
}
|
|
List<? extends SoftWrap> softWraps = getSoftWrapModel().getSoftWrapsForRange(start, softWrapRetrievalEndOffset);
|
|
for (SoftWrap softWrap : softWraps) {
|
|
int softWrapStart = softWrap.getStart();
|
|
if (softWrapsToSkip.contains(softWrapStart)) {
|
|
continue;
|
|
}
|
|
if (startToUse < softWrapStart) {
|
|
position.x = drawBackground(g, backColor, text, startToUse, softWrapStart, position, fontType, defaultBackground, clip);
|
|
}
|
|
boolean drawCustomBackgroundAtSoftWrapVirtualSpace =
|
|
!Comparing.equal(backColor, defaultBackground) && (softWrapStart > start || Comparing.equal(prevBackColor, backColor));
|
|
drawSoftWrap(
|
|
g, softWrap, position, fontType, backColor, drawCustomBackgroundAtSoftWrapVirtualSpace, defaultBackground, clip, caretRowPainted
|
|
);
|
|
startToUse = softWrapStart;
|
|
}
|
|
|
|
if (startToUse < end) {
|
|
position.x = drawBackground(g, backColor, text, startToUse, end, position, fontType, defaultBackground, clip);
|
|
}
|
|
return position.x;
|
|
}
|
|
|
|
private void drawSoftWrap(@NotNull Graphics g,
|
|
@NotNull SoftWrap softWrap,
|
|
@NotNull Point position,
|
|
@JdkConstants.FontStyle int fontType,
|
|
@Nullable Color backColor,
|
|
boolean drawCustomBackgroundAtSoftWrapVirtualSpace,
|
|
@NotNull Color defaultBackground,
|
|
@NotNull Rectangle clip,
|
|
@NotNull boolean[] caretRowPainted) {
|
|
// The main idea is to to do the following:
|
|
// *) update given drawing position coordinates in accordance with the current soft wrap;
|
|
// *) draw background at soft wrap-introduced virtual space if necessary;
|
|
|
|
CharSequence softWrapText = softWrap.getText();
|
|
int activeRowY = getCaretModel().getVisualPosition().line * getLineHeight();
|
|
int afterSoftWrapWidth = clip.x + clip.width - position.x;
|
|
if (drawCustomBackgroundAtSoftWrapVirtualSpace && backColor != null) {
|
|
drawBackground(g, backColor, afterSoftWrapWidth, position, defaultBackground, clip);
|
|
}
|
|
else if (position.y == activeRowY) {
|
|
// Draw 'active line' background after soft wrap.
|
|
Color caretRowColor = mySettings.isCaretRowShown() ? getColorsScheme().getColor(EditorColors.CARET_ROW_COLOR) : null;
|
|
drawBackground(g, caretRowColor, afterSoftWrapWidth, position, defaultBackground, clip);
|
|
caretRowPainted[0] = true;
|
|
}
|
|
|
|
paintSelectionOnFirstSoftWrapLineIfNecessary(g, position, clip, defaultBackground, fontType);
|
|
|
|
int i = CharArrayUtil.lastIndexOf(softWrapText, "\n", softWrapText.length()) + 1;
|
|
int width = getTextSegmentWidth(softWrapText, i, softWrapText.length(), 0, fontType, clip)
|
|
+ getSoftWrapModel().getMinDrawingWidthInPixels(SoftWrapDrawingType.AFTER_SOFT_WRAP);
|
|
position.x = 0;
|
|
position.y += getLineHeight();
|
|
|
|
if (drawCustomBackgroundAtSoftWrapVirtualSpace && backColor != null) {
|
|
drawBackground(g, backColor, width, position, defaultBackground, clip);
|
|
}
|
|
else if (position.y == activeRowY) {
|
|
// Draw 'active line' background for the soft wrap-introduced virtual space.
|
|
Color caretRowColor = mySettings.isCaretRowShown() ? getColorsScheme().getColor(EditorColors.CARET_ROW_COLOR) : null;
|
|
drawBackground(g, caretRowColor, width, position, defaultBackground, clip);
|
|
}
|
|
|
|
position.x = 0;
|
|
paintSelectionOnSecondSoftWrapLineIfNecessary(g, position, clip, defaultBackground, fontType, softWrap);
|
|
position.x = width;
|
|
}
|
|
|
|
|
|
private VisualPosition getSelectionStartPositionForPaint() {
|
|
if (mySelectionStartPosition == null) {
|
|
// We cache the value to avoid repeated invocations of Editor.logicalPositionToOffset which is currently slow for long lines
|
|
mySelectionStartPosition = getSelectionModel().getSelectionStartPosition();
|
|
}
|
|
return mySelectionStartPosition;
|
|
}
|
|
|
|
private VisualPosition getSelectionEndPositionForPaint() {
|
|
if (mySelectionEndPosition == null) {
|
|
// We cache the value to avoid repeated invocations of Editor.logicalPositionToOffset which is currently slow for long lines
|
|
mySelectionEndPosition = getSelectionModel().getSelectionEndPosition();
|
|
}
|
|
return mySelectionEndPosition;
|
|
}
|
|
|
|
/**
|
|
* End user is allowed to perform selection by visual coordinates (e.g. by dragging mouse with left button hold). There is a possible
|
|
* case that such a move intersects with soft wrap introduced virtual space. We want to draw corresponding selection background
|
|
* there then.
|
|
* <p/>
|
|
* This method encapsulates functionality of drawing selection background on the first soft wrap line (e.g. on a visual line where
|
|
* it is applied).
|
|
*
|
|
* @param g graphics to draw on
|
|
* @param position current position (assumed to be position of soft wrap appliance)
|
|
* @param clip target drawing area boundaries
|
|
* @param defaultBackground default background
|
|
* @param fontType current font type
|
|
*/
|
|
private void paintSelectionOnFirstSoftWrapLineIfNecessary(@NotNull Graphics g,
|
|
@NotNull Point position,
|
|
@NotNull Rectangle clip,
|
|
@NotNull Color defaultBackground,
|
|
@JdkConstants.FontStyle int fontType) {
|
|
// There is a possible case that the user performed selection at soft wrap virtual space. We need to paint corresponding background
|
|
// there then.
|
|
VisualPosition selectionStartPosition = getSelectionStartPositionForPaint();
|
|
VisualPosition selectionEndPosition = getSelectionEndPositionForPaint();
|
|
if (selectionStartPosition.equals(selectionEndPosition)) {
|
|
return;
|
|
}
|
|
|
|
int currentVisualLine = position.y / getLineHeight();
|
|
int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, currentVisualLine);
|
|
|
|
// Check if the first soft wrap line is within the visual selection.
|
|
if (currentVisualLine < selectionStartPosition.line || currentVisualLine > selectionEndPosition.line
|
|
|| currentVisualLine == selectionEndPosition.line && selectionEndPosition.column <= lastColumn) {
|
|
return;
|
|
}
|
|
|
|
// Adjust 'x' if selection starts at soft wrap virtual space.
|
|
final int columnsToSkip = selectionStartPosition.column - lastColumn;
|
|
if (columnsToSkip > 0) {
|
|
position.x += getSoftWrapModel().getMinDrawingWidthInPixels(SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED);
|
|
position.x += (columnsToSkip - 1) * EditorUtil.getSpaceWidth(Font.PLAIN, this);
|
|
}
|
|
|
|
// Calculate selection width.
|
|
final int width;
|
|
if (selectionEndPosition.line > currentVisualLine) {
|
|
width = clip.x + clip.width - position.x;
|
|
}
|
|
else if (selectionStartPosition.line < currentVisualLine || selectionStartPosition.column <= lastColumn) {
|
|
width = getSoftWrapModel().getMinDrawingWidthInPixels(SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED)
|
|
+ (selectionEndPosition.column - lastColumn - 1) * EditorUtil.getSpaceWidth(fontType, this);
|
|
}
|
|
else {
|
|
width = (selectionEndPosition.column - selectionStartPosition.column) * EditorUtil.getSpaceWidth(fontType, this);
|
|
}
|
|
|
|
drawBackground(g, getColorsScheme().getColor(EditorColors.SELECTION_BACKGROUND_COLOR), width, position, defaultBackground, clip);
|
|
}
|
|
|
|
/**
|
|
* End user is allowed to perform selection by visual coordinates (e.g. by dragging mouse with left button hold). There is a possible
|
|
* case that such a move intersects with soft wrap introduced virtual space. We want to draw corresponding selection background
|
|
* there then.
|
|
* <p/>
|
|
* This method encapsulates functionality of drawing selection background on the second soft wrap line (e.g. on a visual line after
|
|
* the one where it is applied).
|
|
*
|
|
* @param g graphics to draw on
|
|
* @param position current position (assumed to be position of soft wrap appliance)
|
|
* @param clip target drawing area boundaries
|
|
* @param defaultBackground default background
|
|
* @param fontType current font type
|
|
* @param softWrap target soft wrap which second line virtual space may contain selection
|
|
*/
|
|
private void paintSelectionOnSecondSoftWrapLineIfNecessary(@NotNull Graphics g,
|
|
@NotNull Point position,
|
|
@NotNull Rectangle clip,
|
|
@NotNull Color defaultBackground,
|
|
@JdkConstants.FontStyle int fontType,
|
|
@NotNull SoftWrap softWrap) {
|
|
// There is a possible case that the user performed selection at soft wrap virtual space. We need to paint corresponding background
|
|
// there then.
|
|
VisualPosition selectionStartPosition = getSelectionStartPositionForPaint();
|
|
VisualPosition selectionEndPosition = getSelectionEndPositionForPaint();
|
|
if (selectionStartPosition.equals(selectionEndPosition)) {
|
|
return;
|
|
}
|
|
|
|
int currentVisualLine = position.y / getLineHeight();
|
|
|
|
// Check if the second soft wrap line is within the visual selection.
|
|
if (currentVisualLine < selectionStartPosition.line || currentVisualLine > selectionEndPosition.line
|
|
|| currentVisualLine == selectionStartPosition.line && selectionStartPosition.column >= softWrap.getIndentInColumns()) {
|
|
return;
|
|
}
|
|
|
|
// Adjust 'x' if selection starts at soft wrap virtual space.
|
|
if (selectionStartPosition.line == currentVisualLine && selectionStartPosition.column > 0) {
|
|
position.x += selectionStartPosition.column * EditorUtil.getSpaceWidth(fontType, this);
|
|
}
|
|
|
|
// Calculate selection width.
|
|
final int width;
|
|
if (selectionEndPosition.line > currentVisualLine || selectionEndPosition.column >= softWrap.getIndentInColumns()) {
|
|
width = softWrap.getIndentInPixels() - position.x;
|
|
}
|
|
else {
|
|
width = selectionEndPosition.column * EditorUtil.getSpaceWidth(fontType, this) - position.x;
|
|
}
|
|
|
|
drawBackground(g, getColorsScheme().getColor(EditorColors.SELECTION_BACKGROUND_COLOR), width, position, defaultBackground, clip);
|
|
}
|
|
|
|
private int drawBackground(@NotNull Graphics g,
|
|
Color backColor,
|
|
@NotNull CharSequence text,
|
|
int start,
|
|
int end,
|
|
@NotNull Point position,
|
|
@JdkConstants.FontStyle int fontType,
|
|
@NotNull Color defaultBackground,
|
|
@NotNull Rectangle clip) {
|
|
int width = getTextSegmentWidth(text, start, end, position.x, fontType, clip);
|
|
return drawBackground(g, backColor, width, position, defaultBackground, clip);
|
|
}
|
|
|
|
private int drawBackground(@NotNull Graphics g,
|
|
@Nullable Color backColor,
|
|
int width,
|
|
@NotNull Point position,
|
|
@NotNull Color defaultBackground,
|
|
@NotNull Rectangle clip) {
|
|
if (backColor != null && !backColor.equals(defaultBackground) && clip.intersects(position.x, position.y, width, getLineHeight())) {
|
|
if (backColor.equals(myLastBackgroundColor) && myLastBackgroundPosition.y == position.y &&
|
|
myLastBackgroundPosition.x + myLastBackgroundWidth == position.x) {
|
|
myLastBackgroundWidth += width;
|
|
}
|
|
else {
|
|
flushBackground(g, clip);
|
|
myLastBackgroundColor = backColor;
|
|
myLastBackgroundPosition = new Point(position);
|
|
myLastBackgroundWidth = width;
|
|
}
|
|
}
|
|
|
|
return position.x + width;
|
|
}
|
|
|
|
private void flushBackground(@NotNull Graphics g, @NotNull final Rectangle clip) {
|
|
if (myLastBackgroundColor != null) {
|
|
final Point position = myLastBackgroundPosition;
|
|
final int w = myLastBackgroundWidth;
|
|
final int height = getLineHeight();
|
|
if (clip.intersects(position.x, position.y, w, height)) {
|
|
g.setColor(myLastBackgroundColor);
|
|
g.fillRect(position.x, position.y, w, height);
|
|
}
|
|
myLastBackgroundColor = null;
|
|
}
|
|
}
|
|
|
|
@NotNull
|
|
private LineIterator createLineIterator() {
|
|
return myDocument.createLineIterator();
|
|
}
|
|
|
|
private void paintText(@NotNull Graphics g,
|
|
@NotNull Rectangle clip,
|
|
@NotNull LogicalPosition clipStartPosition,
|
|
int clipStartOffset,
|
|
int clipEndOffset) {
|
|
myCurrentFontType = null;
|
|
myLastCache = null;
|
|
|
|
int lineHeight = getLineHeight();
|
|
|
|
int visibleLine = clip.y / lineHeight;
|
|
|
|
int startLine = clipStartPosition.line;
|
|
int start = clipStartOffset;
|
|
|
|
Point position = new Point(0, visibleLine * lineHeight);
|
|
if (startLine == 0 && myPrefixText != null) {
|
|
position.x = drawStringWithSoftWraps(g, new CharArrayCharSequence(myPrefixText), 0, myPrefixText.length, position, clip,
|
|
myPrefixAttributes.getEffectColor(), myPrefixAttributes.getEffectType(),
|
|
myPrefixAttributes.getFontType(), myPrefixAttributes.getForegroundColor(), -1,
|
|
PAINT_NO_WHITESPACE);
|
|
}
|
|
if (startLine >= myDocument.getLineCount() || startLine < 0) {
|
|
if (position.x > 0) flushCachedChars(g);
|
|
return;
|
|
}
|
|
|
|
LineIterator lIterator = createLineIterator();
|
|
lIterator.start(start);
|
|
if (lIterator.atEnd()) {
|
|
return;
|
|
}
|
|
|
|
IterationState iterationState = new IterationState(this, start, clipEndOffset, isPaintSelection());
|
|
TextAttributes attributes = iterationState.getMergedAttributes();
|
|
Color currentColor = attributes.getForegroundColor();
|
|
if (currentColor == null) {
|
|
currentColor = getForegroundColor();
|
|
}
|
|
Color effectColor = attributes.getEffectColor();
|
|
EffectType effectType = attributes.getEffectType();
|
|
int fontType = attributes.getFontType();
|
|
g.setColor(currentColor);
|
|
|
|
CharSequence chars = myDocument.getImmutableCharSequence();
|
|
LineWhitespacePaintingStrategy context = new LineWhitespacePaintingStrategy();
|
|
context.update(chars, lIterator);
|
|
|
|
while (!iterationState.atEnd() && !lIterator.atEnd()) {
|
|
int hEnd = iterationState.getEndOffset();
|
|
int lEnd = lIterator.getEnd();
|
|
if (hEnd >= lEnd) {
|
|
FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start);
|
|
if (collapsedFolderAt == null) {
|
|
drawStringWithSoftWraps(g, chars, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor,
|
|
effectType, fontType, currentColor, clipStartOffset, context);
|
|
final VirtualFile file = getVirtualFile();
|
|
if (myProject != null && file != null && !isOneLineMode()) {
|
|
int offset = position.x;
|
|
String additionalText = "";
|
|
for (EditorLinePainter painter : EditorLinePainter.EP_NAME.getExtensions()) {
|
|
Collection<LineExtensionInfo> extensions = painter.getLineExtensions(myProject, file, lIterator.getLineNumber());
|
|
if (extensions != null && !extensions.isEmpty()) {
|
|
for (LineExtensionInfo info : extensions) {
|
|
final String text = info.getText();
|
|
additionalText += text;
|
|
position.x = drawString(g, text, 0, text.length(), position, clip,
|
|
info.getEffectColor() == null ? effectColor : info.getEffectColor(),
|
|
info.getEffectType() == null ? effectType : info.getEffectType(),
|
|
info.getFontType(),
|
|
info.getColor() == null ? currentColor : info.getColor(),
|
|
context);
|
|
}
|
|
}
|
|
}
|
|
for (char ch : additionalText.toCharArray()) {
|
|
offset += EditorUtil.charWidth(ch, Font.ITALIC, this);
|
|
}
|
|
myLinePaintersWidth = Math.max(myLinePaintersWidth, offset);
|
|
}
|
|
|
|
position.x = 0;
|
|
if (position.y > clip.y + clip.height) {
|
|
break;
|
|
}
|
|
position.y += lineHeight;
|
|
start = lEnd;
|
|
}
|
|
|
|
// myBorderEffect.eolReached(g, this);
|
|
lIterator.advance();
|
|
if (!lIterator.atEnd()) {
|
|
context.update(chars, lIterator);
|
|
}
|
|
}
|
|
else {
|
|
FoldRegion collapsedFolderAt = iterationState.getCurrentFold();
|
|
if (collapsedFolderAt != null) {
|
|
SoftWrap softWrap = mySoftWrapModel.getSoftWrap(collapsedFolderAt.getStartOffset());
|
|
if (softWrap != null) {
|
|
position.x = drawStringWithSoftWraps(
|
|
g, chars, collapsedFolderAt.getStartOffset(), collapsedFolderAt.getStartOffset(), position, clip, effectColor, effectType,
|
|
fontType, currentColor, clipStartOffset, context
|
|
);
|
|
}
|
|
int foldingXStart = position.x;
|
|
position.x = drawString(
|
|
g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor,
|
|
PAINT_NO_WHITESPACE);
|
|
//drawStringWithSoftWraps(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType,
|
|
// fontType, currentColor, logicalPosition);
|
|
BorderEffect.paintFoldedEffect(g, foldingXStart, position.y, position.x, getLineHeight(), effectColor, effectType);
|
|
}
|
|
else {
|
|
position.x = drawStringWithSoftWraps(g, chars, start, Math.min(hEnd, lEnd - lIterator.getSeparatorLength()), position, clip,
|
|
effectColor, effectType, fontType, currentColor, clipStartOffset, context);
|
|
}
|
|
|
|
iterationState.advance();
|
|
attributes = iterationState.getMergedAttributes();
|
|
|
|
currentColor = attributes.getForegroundColor();
|
|
if (currentColor == null) {
|
|
currentColor = getForegroundColor();
|
|
}
|
|
|
|
effectColor = attributes.getEffectColor();
|
|
effectType = attributes.getEffectType();
|
|
fontType = attributes.getFontType();
|
|
|
|
start = iterationState.getStartOffset();
|
|
}
|
|
}
|
|
|
|
FoldRegion collapsedFolderAt = iterationState.getCurrentFold();
|
|
if (collapsedFolderAt != null) {
|
|
int foldingXStart = position.x;
|
|
int foldingXEnd = drawStringWithSoftWraps(
|
|
g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor, clipStartOffset,
|
|
PAINT_NO_WHITESPACE);
|
|
BorderEffect.paintFoldedEffect(g, foldingXStart, position.y, foldingXEnd, getLineHeight(), effectColor, effectType);
|
|
// myBorderEffect.collapsedFolderReached(g, this);
|
|
}
|
|
|
|
final SoftWrap softWrap = mySoftWrapModel.getSoftWrap(clipEndOffset);
|
|
if (softWrap != null) {
|
|
mySoftWrapModel.paint(g, SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED, position.x, position.y, getLineHeight());
|
|
}
|
|
|
|
flushCachedChars(g);
|
|
}
|
|
|
|
private boolean paintPlaceholderText(@NotNull Graphics g, @NotNull Rectangle clip) {
|
|
CharSequence hintText = myPlaceholderText;
|
|
if (myDocument.getTextLength() > 0 || hintText == null || hintText.length() == 0) {
|
|
return false;
|
|
}
|
|
|
|
if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() == myEditorComponent && !myShowPlaceholderWhenFocused) {
|
|
// There is a possible case that placeholder text was painted and the editor gets focus now. We want to over-paint previously
|
|
// used placeholder text then.
|
|
myLastBackgroundColor = getBackgroundColor();
|
|
myLastBackgroundPosition = new Point(0, 0);
|
|
myLastBackgroundWidth = myLastPaintedPlaceholderWidth;
|
|
flushBackground(g, clip);
|
|
return false;
|
|
}
|
|
else {
|
|
hintText = SwingUtilities.layoutCompoundLabel(g.getFontMetrics(), hintText.toString(), null, 0, 0, 0, 0,
|
|
myEditorComponent.getBounds(), new Rectangle(), new Rectangle(), 0);
|
|
myLastPaintedPlaceholderWidth = drawString(
|
|
g, hintText, 0, hintText.length(), new Point(0, 0), clip, null, null, Font.PLAIN,
|
|
myFoldingModel.getPlaceholderAttributes().getForegroundColor(), PAINT_NO_WHITESPACE
|
|
);
|
|
flushCachedChars(g);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public boolean isPaintSelection() {
|
|
return myPaintSelection || !isOneLineMode() || IJSwingUtilities.hasFocus(getContentComponent());
|
|
}
|
|
|
|
public void setPaintSelection(boolean paintSelection) {
|
|
myPaintSelection = paintSelection;
|
|
}
|
|
|
|
@Override
|
|
@NotNull
|
|
@NonNls
|
|
public String dumpState() {
|
|
return "prefix: '" + (myPrefixText == null ? "none" : new String(myPrefixText))
|
|
+ "', allow caret inside tab: " + mySettings.isCaretInsideTabs()
|
|
+ ", allow caret after line end: " + mySettings.isVirtualSpace()
|
|
+ ", soft wraps: " + (mySoftWrapModel.isSoftWrappingEnabled() ? "on" : "off")
|
|
+ ", soft wraps data: " + getSoftWrapModel().dumpState()
|
|
+ "\n\nfolding data: " + getFoldingModel().dumpState()
|
|
+ (myDocument instanceof DocumentImpl ? "\n\ndocument info: " + ((DocumentImpl)myDocument).dumpState() : "")
|
|
+ "\nfont preferences: " + myScheme.getFontPreferences();
|
|
}
|
|
|
|
private class CachedFontContent {
|
|
final CharSequence[] data = new CharSequence[CACHED_CHARS_BUFFER_SIZE];
|
|
final int[] starts = new int[CACHED_CHARS_BUFFER_SIZE];
|
|
final int[] ends = new int[CACHED_CHARS_BUFFER_SIZE];
|
|
final int[] x = new int[CACHED_CHARS_BUFFER_SIZE];
|
|
final int[] y = new int[CACHED_CHARS_BUFFER_SIZE];
|
|
final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE];
|
|
final boolean[] whitespaceShown = new boolean[CACHED_CHARS_BUFFER_SIZE];
|
|
|
|
int myCount = 0;
|
|
@NotNull final FontInfo myFontType;
|
|
final boolean myHasBreakSymbols;
|
|
final int spaceWidth;
|
|
|
|
@Nullable private CharSequence myLastData;
|
|
|
|
private CachedFontContent(@NotNull FontInfo fontInfo) {
|
|
myFontType = fontInfo;
|
|
spaceWidth = fontInfo.charWidth(' ');
|
|
myHasBreakSymbols = fontInfo.hasGlyphsToBreakDrawingIteration();
|
|
}
|
|
|
|
private void flushContent(@NotNull Graphics g) {
|
|
if (myCount != 0) {
|
|
if (myCurrentFontType != myFontType) {
|
|
myCurrentFontType = myFontType;
|
|
g.setFont(myFontType.getFont());
|
|
}
|
|
Color currentColor = null;
|
|
for (int i = 0; i < myCount; i++) {
|
|
if (!Comparing.equal(color[i], currentColor)) {
|
|
currentColor = color[i] != null ? color[i] : JBColor.black;
|
|
|
|
g.setColor(currentColor);
|
|
}
|
|
|
|
drawChars(g, data[i], starts[i], ends[i], x[i], y[i], whitespaceShown[i]);
|
|
color[i] = null;
|
|
data[i] = null;
|
|
}
|
|
|
|
myCount = 0;
|
|
myLastData = null;
|
|
}
|
|
}
|
|
|
|
private void addContent(@NotNull Graphics g,
|
|
CharSequence _data,
|
|
int _start,
|
|
int _end,
|
|
int _x,
|
|
int _y,
|
|
@Nullable Color _color,
|
|
boolean drawWhitespace) {
|
|
final int count = myCount;
|
|
if (count > 0) {
|
|
final int lastCount = count - 1;
|
|
final Color lastColor = color[lastCount];
|
|
if (_data == myLastData &&
|
|
_start == ends[lastCount] &&
|
|
(_color == null || lastColor == null || _color.equals(lastColor))
|
|
&&
|
|
_y == y[lastCount]
|
|
/* there is a possible case that vertical position is adjusted because of soft wrap */
|
|
&&
|
|
(!myHasBreakSymbols || !myFontType.getSymbolsToBreakDrawingIteration().contains(_data.charAt(ends[lastCount] - 1)))
|
|
&&
|
|
(!myDisableRtl ||
|
|
_start < 1 ||
|
|
_start >= _data.length() ||
|
|
!isRtlCharacter(_data.charAt(_start)) && !isRtlCharacter(_data.charAt(_start - 1)))
|
|
&&
|
|
drawWhitespace == whitespaceShown[lastCount]) {
|
|
ends[lastCount] = _end;
|
|
if (lastColor == null) color[lastCount] = _color;
|
|
return;
|
|
}
|
|
}
|
|
|
|
myLastData = _data;
|
|
data[count] = _data;
|
|
x[count] = _x;
|
|
y[count] = _y;
|
|
starts[count] = _start;
|
|
ends[count] = _end;
|
|
color[count] = _color;
|
|
whitespaceShown[count] = drawWhitespace;
|
|
|
|
myCount++;
|
|
if (count >= CACHED_CHARS_BUFFER_SIZE - 1) {
|
|
flushContent(g);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static boolean isRtlCharacter(char c) {
|
|
byte directionality = Character.getDirectionality(c);
|
|
return directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT
|
|
|| directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC
|
|
|| directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING
|
|
|| directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE;
|
|
}
|
|
|
|
private void flushCachedChars(@NotNull Graphics g) {
|
|
for (CachedFontContent cache : myFontCache) {
|
|
cache.flushContent(g);
|
|
}
|
|
myLastCache = null;
|
|
}
|
|
|
|
private void paintCaretCursor(@NotNull Graphics g) {
|
|
// There is a possible case that visual caret position is changed because of newly added or removed soft wraps.
|
|
// We check if that's the case and ask caret model to recalculate visual position if necessary.
|
|
|
|
myCaretCursor.paint(g);
|
|
}
|
|
|
|
@Nullable
|
|
public CaretRectangle[] getCaretLocations(boolean onlyIfShown) {
|
|
return myCaretCursor.getCaretLocations(onlyIfShown);
|
|
}
|
|
|
|
private void paintLineMarkersSeparators(@NotNull final Graphics g,
|
|
@NotNull final Rectangle clip,
|
|
@NotNull MarkupModelEx markupModel,
|
|
int clipStartOffset,
|
|
int clipEndOffset) {
|
|
markupModel.processRangeHighlightersOverlappingWith(clipStartOffset, clipEndOffset, new Processor<RangeHighlighterEx>() {
|
|
@Override
|
|
public boolean process(@NotNull RangeHighlighterEx lineMarker) {
|
|
if (!lineMarker.getEditorFilter().avaliableIn(EditorImpl.this)) return true;
|
|
|
|
paintLineMarkerSeparator(lineMarker, clip, g);
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
private void paintLineMarkerSeparator(@NotNull RangeHighlighter marker, @NotNull Rectangle clip, @NotNull Graphics g) {
|
|
Color separatorColor = marker.getLineSeparatorColor();
|
|
LineSeparatorRenderer lineSeparatorRenderer = marker.getLineSeparatorRenderer();
|
|
if (separatorColor == null && lineSeparatorRenderer == null) {
|
|
return;
|
|
}
|
|
int line = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument()
|
|
.getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset());
|
|
if (line < 0 || line >= myDocument.getLineCount()) {
|
|
return;
|
|
}
|
|
|
|
// There is a possible case that particular logical line occupies more than one visual line (because of soft wraps processing),
|
|
// hence, we need to consider that during calculating 'y' position for the last visual line used for the target logical
|
|
// line representation.
|
|
int y;
|
|
SeparatorPlacement placement = marker.getLineSeparatorPlacement();
|
|
if (placement == SeparatorPlacement.TOP) {
|
|
y = visibleLineToY(logicalToVisualLine(line));
|
|
}
|
|
else if (line + 1 >= myDocument.getLineCount()) {
|
|
y = visibleLineToY(offsetToVisualLine(myDocument.getTextLength()) + 1);
|
|
}
|
|
else {
|
|
y = logicalLineToY(line + 1);
|
|
}
|
|
|
|
y -= 1;
|
|
if (y + getLineHeight() < clip.y || y > clip.y + clip.height) return;
|
|
|
|
int endShift = clip.x + clip.width;
|
|
g.setColor(separatorColor);
|
|
|
|
if (mySettings.isRightMarginShown() && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) {
|
|
endShift = Math.min(endShift, mySettings.getRightMargin(myProject) * EditorUtil.getSpaceWidth(Font.PLAIN, this));
|
|
}
|
|
|
|
if (lineSeparatorRenderer != null) {
|
|
lineSeparatorRenderer.drawLine(g, 0, endShift, y);
|
|
}
|
|
else {
|
|
UIUtil.drawLine(g, 0, y, endShift, y);
|
|
}
|
|
}
|
|
|
|
private int drawStringWithSoftWraps(@NotNull Graphics g,
|
|
@NotNull final String text,
|
|
@NotNull Point position,
|
|
@NotNull Rectangle clip,
|
|
Color effectColor,
|
|
EffectType effectType,
|
|
@JdkConstants.FontStyle int fontType,
|
|
Color fontColor,
|
|
int startDrawingOffset,
|
|
WhitespacePaintingStrategy context) {
|
|
return drawStringWithSoftWraps(g, text, 0, text.length(), position, clip, effectColor, effectType,
|
|
fontType, fontColor, startDrawingOffset, context);
|
|
}
|
|
|
|
private int drawStringWithSoftWraps(@NotNull Graphics g,
|
|
final CharSequence text,
|
|
int start,
|
|
final int end,
|
|
@NotNull Point position,
|
|
@NotNull Rectangle clip,
|
|
Color effectColor,
|
|
EffectType effectType,
|
|
@JdkConstants.FontStyle int fontType,
|
|
Color fontColor,
|
|
int startDrawingOffset,
|
|
WhitespacePaintingStrategy context) {
|
|
if (start >= end && getSoftWrapModel().getSoftWrap(start) == null) {
|
|
return position.x;
|
|
}
|
|
|
|
// Given 'end' offset is exclusive though SoftWrapModel.getSoftWrapsForRange() uses inclusive end offset.
|
|
// Hence, we decrement it if necessary. Please note that we don't do that if start is equal to end. That is the case,
|
|
// for example, for soft-wrapped collapsed fold region - we need to draw soft wrap before it.
|
|
int softWrapRetrievalEndOffset = end;
|
|
if (start < end) {
|
|
softWrapRetrievalEndOffset--;
|
|
}
|
|
|
|
outer:
|
|
for (SoftWrap softWrap : getSoftWrapModel().getSoftWrapsForRange(start, softWrapRetrievalEndOffset)) {
|
|
char[] softWrapChars = softWrap.getChars();
|
|
CharArrayCharSequence softWrapSeq = new CharArrayCharSequence(softWrapChars);
|
|
|
|
if (softWrap.getStart() == startDrawingOffset) {
|
|
// If we are here that means that we are located on soft wrap-introduced visual line just after soft wrap. Hence, we need
|
|
// to draw soft wrap indent if any and 'after soft wrap' sign.
|
|
int i = CharArrayUtil.lastIndexOf(softWrapChars, '\n', 0, softWrapChars.length);
|
|
if (i < softWrapChars.length - 1) {
|
|
position.x = 0; // Soft wrap starts new visual line
|
|
position.x = drawString(
|
|
g, softWrapSeq, i + 1, softWrapChars.length, position, clip, null, null, fontType, fontColor, context
|
|
);
|
|
}
|
|
position.x += mySoftWrapModel.paint(g, SoftWrapDrawingType.AFTER_SOFT_WRAP, position.x, position.y, getLineHeight());
|
|
continue;
|
|
}
|
|
|
|
// Draw token text before the wrap.
|
|
if (softWrap.getStart() > start) {
|
|
position.x = drawString(
|
|
g, text, start, softWrap.getStart(), position, clip, null, null, fontType, fontColor, context
|
|
);
|
|
}
|
|
|
|
start = softWrap.getStart();
|
|
|
|
// We don't draw every soft wrap symbol one-by-one but whole visual line. Current variable holds index that points
|
|
// to the first soft wrap symbol that is not drawn yet.
|
|
int softWrapSegmentStartIndex = 0;
|
|
for (int i = 0; i < softWrapChars.length; i++) {
|
|
// Delay soft wraps symbols drawing until EOL is found.
|
|
if (softWrapChars[i] != '\n') {
|
|
continue;
|
|
}
|
|
|
|
// Draw soft wrap symbols on current visual line if any.
|
|
if (i - softWrapSegmentStartIndex > 0) {
|
|
drawString(
|
|
g, softWrapSeq, softWrapSegmentStartIndex, i, position, clip, null, null, fontType, fontColor, context
|
|
);
|
|
}
|
|
mySoftWrapModel.paint(g, SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED, position.x, position.y, getLineHeight());
|
|
|
|
// Reset 'x' coordinate because of new line start.
|
|
position.x = 0;
|
|
|
|
// Stop the processing if we drew the whole clip.
|
|
if (position.y > clip.y + clip.height) {
|
|
break outer;
|
|
}
|
|
position.y += getLineHeight();
|
|
softWrapSegmentStartIndex = i + 1;
|
|
}
|
|
|
|
// Draw remaining soft wrap symbols from its last line if any.
|
|
if (softWrapSegmentStartIndex < softWrapChars.length) {
|
|
position.x += drawString(
|
|
g, softWrapSeq, softWrapSegmentStartIndex, softWrapChars.length, position, clip, null, null, fontType, fontColor, context
|
|
);
|
|
}
|
|
position.x += mySoftWrapModel.paint(g, SoftWrapDrawingType.AFTER_SOFT_WRAP, position.x, position.y, getLineHeight());
|
|
}
|
|
return position.x = drawString(g, text, start, end, position, clip, effectColor, effectType, fontType, fontColor, context);
|
|
}
|
|
|
|
private int drawString(@NotNull Graphics g,
|
|
final CharSequence text,
|
|
int start,
|
|
int end,
|
|
@NotNull Point position,
|
|
@NotNull Rectangle clip,
|
|
@Nullable Color effectColor,
|
|
@Nullable EffectType effectType,
|
|
@JdkConstants.FontStyle int fontType,
|
|
Color fontColor,
|
|
WhitespacePaintingStrategy context) {
|
|
if (start >= end) return position.x;
|
|
|
|
boolean isInClip = getLineHeight() + position.y >= clip.y && position.y <= clip.y + clip.height;
|
|
|
|
if (!isInClip) return position.x;
|
|
|
|
int y = getAscent() + position.y;
|
|
int x = position.x;
|
|
return drawTabbedString(g, text, start, end, x, y, effectColor, effectType, fontType, fontColor, clip, context);
|
|
}
|
|
|
|
public int getAscent() {
|
|
if (myUseNewRendering) return myView.getAscent();
|
|
return getLineHeight() - getDescent();
|
|
}
|
|
|
|
private int drawString(@NotNull Graphics g,
|
|
@NotNull String text,
|
|
@NotNull Point position,
|
|
@NotNull Rectangle clip,
|
|
Color effectColor,
|
|
EffectType effectType,
|
|
@JdkConstants.FontStyle int fontType,
|
|
Color fontColor,
|
|
WhitespacePaintingStrategy context) {
|
|
boolean isInClip = getLineHeight() + position.y >= clip.y && position.y <= clip.y + clip.height;
|
|
|
|
if (!isInClip) return position.x;
|
|
|
|
int y = getAscent() + position.y;
|
|
int x = position.x;
|
|
|
|
return drawTabbedString(g, text, 0, text.length(), x, y, effectColor, effectType, fontType, fontColor, clip, context);
|
|
}
|
|
|
|
private int drawTabbedString(@NotNull Graphics g,
|
|
CharSequence text,
|
|
int start,
|
|
int end,
|
|
int x,
|
|
int y,
|
|
@Nullable Color effectColor,
|
|
EffectType effectType,
|
|
@JdkConstants.FontStyle int fontType,
|
|
Color fontColor,
|
|
@NotNull final Rectangle clip,
|
|
WhitespacePaintingStrategy context) {
|
|
int xStart = x;
|
|
|
|
for (int i = start; i < end; i++) {
|
|
if (text.charAt(i) != '\t') continue;
|
|
|
|
x = drawTablessString(text, start, i, g, x, y, fontType, fontColor, clip, context);
|
|
|
|
int x1 = EditorUtil.nextTabStop(x, this);
|
|
drawTabPlacer(g, y, x, x1, i, context);
|
|
x = x1;
|
|
start = i + 1;
|
|
}
|
|
|
|
x = drawTablessString(text, start, end, g, x, y, fontType, fontColor, clip, context);
|
|
|
|
if (effectColor != null) {
|
|
final Color savedColor = g.getColor();
|
|
|
|
// myBorderEffect.flushIfCantProlong(g, this, effectType, effectColor);
|
|
int xEnd = x;
|
|
if (xStart < clip.x && xEnd < clip.x || xStart > clip.x + clip.width && xEnd > clip.x + clip.width) {
|
|
return x;
|
|
}
|
|
|
|
if (xEnd > clip.x + clip.width) {
|
|
xEnd = clip.x + clip.width;
|
|
}
|
|
if (xStart < clip.x) {
|
|
xStart = clip.x;
|
|
}
|
|
|
|
if (effectType == EffectType.LINE_UNDERSCORE) {
|
|
g.setColor(effectColor);
|
|
UIUtil.drawLine(g, xStart, y + 1, xEnd, y + 1);
|
|
g.setColor(savedColor);
|
|
}
|
|
else if (effectType == EffectType.BOLD_LINE_UNDERSCORE) {
|
|
g.setColor(effectColor);
|
|
drawBoldLineUnderScore(g, xStart, y, xEnd - xStart);
|
|
g.setColor(savedColor);
|
|
}
|
|
else if (effectType == EffectType.STRIKEOUT) {
|
|
g.setColor(effectColor);
|
|
int y1 = y - getCharHeight() / 2;
|
|
UIUtil.drawLine(g, xStart, y1, xEnd, y1);
|
|
g.setColor(savedColor);
|
|
}
|
|
else if (effectType == EffectType.WAVE_UNDERSCORE) {
|
|
g.setColor(effectColor);
|
|
UIUtil.drawWave((Graphics2D)g, new Rectangle(xStart, y + 1, xEnd - xStart, getDescent() - 1));
|
|
g.setColor(savedColor);
|
|
}
|
|
else if (effectType == EffectType.BOLD_DOTTED_LINE) {
|
|
final Color bgColor = getBackgroundColor();
|
|
final int dottedAt = SystemInfo.isMac ? y : y + 1;
|
|
UIUtil.drawBoldDottedLine((Graphics2D)g, xStart, xEnd, dottedAt, bgColor, effectColor, false);
|
|
}
|
|
}
|
|
|
|
return x;
|
|
}
|
|
|
|
private int drawTablessString(final CharSequence text,
|
|
int start,
|
|
final int end,
|
|
@NotNull final Graphics g,
|
|
int x,
|
|
final int y,
|
|
@JdkConstants.FontStyle final int fontType,
|
|
final Color fontColor,
|
|
@NotNull final Rectangle clip,
|
|
WhitespacePaintingStrategy context) {
|
|
int endX = x;
|
|
if (start < end) {
|
|
FontInfo font = null;
|
|
boolean drawWhitespace = false;
|
|
for (int j = start; j < end; j++) {
|
|
if (x > clip.x + clip.width) {
|
|
return endX;
|
|
}
|
|
final char c = text.charAt(j);
|
|
FontInfo newFont = EditorUtil.fontForChar(c, fontType, this);
|
|
boolean newDrawWhitespace = context.showWhitespaceAtOffset(j);
|
|
boolean isRtlChar = myDisableRtl && isRtlCharacter(c);
|
|
if (j > start &&
|
|
(endX < clip.x || endX > clip.x + clip.width || newFont != font || newDrawWhitespace != drawWhitespace || isRtlChar)) {
|
|
if (isOverlappingRange(clip, x, endX)) {
|
|
drawCharsCached(g, text, start, j, x, y, fontType, fontColor, drawWhitespace);
|
|
}
|
|
start = j;
|
|
x = endX;
|
|
}
|
|
font = newFont;
|
|
drawWhitespace = newDrawWhitespace;
|
|
endX += font.charWidth(c);
|
|
|
|
if (font.hasGlyphsToBreakDrawingIteration() && font.getSymbolsToBreakDrawingIteration().contains(c) || isRtlChar) {
|
|
drawCharsCached(g, text, start, j + 1, x, y, fontType, fontColor, drawWhitespace);
|
|
start = j + 1;
|
|
x = endX;
|
|
}
|
|
}
|
|
|
|
if (isOverlappingRange(clip, x, endX)) {
|
|
drawCharsCached(g, text, start, end, x, y, fontType, fontColor, drawWhitespace);
|
|
}
|
|
}
|
|
|
|
return endX;
|
|
}
|
|
|
|
private static boolean isOverlappingRange(Rectangle clip, int xStart, int xEnd) {
|
|
return !(xStart < clip.x && xEnd < clip.x || xStart > clip.x + clip.width && xEnd > clip.x + clip.width);
|
|
}
|
|
|
|
private void drawTabPlacer(Graphics g, int y, int start, int stop, int offset, WhitespacePaintingStrategy context) {
|
|
if (context.showWhitespaceAtOffset(offset)) {
|
|
myTabPainter.paint(g, y, start, stop);
|
|
}
|
|
}
|
|
|
|
private void drawCharsCached(@NotNull Graphics g,
|
|
CharSequence data,
|
|
int start,
|
|
int end,
|
|
int x,
|
|
int y,
|
|
@JdkConstants.FontStyle int fontType,
|
|
Color color,
|
|
boolean drawWhitespace) {
|
|
FontInfo fnt = EditorUtil.fontForChar(data.charAt(start), fontType, this);
|
|
if (myLastCache != null && spacesOnly(data, start, end) && fnt.charWidth(' ') == myLastCache.spaceWidth) {
|
|
// we don't care about font if we only need to paint spaces and space width matches
|
|
myLastCache.addContent(g, data, start, end, x, y, null, drawWhitespace);
|
|
}
|
|
else {
|
|
drawCharsCached(g, data, start, end, x, y, fnt, color, drawWhitespace);
|
|
}
|
|
}
|
|
|
|
private void drawCharsCached(@NotNull Graphics g,
|
|
@NotNull CharSequence data,
|
|
int start,
|
|
int end,
|
|
int x,
|
|
int y,
|
|
@NotNull FontInfo fnt,
|
|
Color color,
|
|
boolean drawWhitespace) {
|
|
CachedFontContent cache = null;
|
|
for (CachedFontContent fontCache : myFontCache) {
|
|
if (fontCache.myFontType == fnt) {
|
|
cache = fontCache;
|
|
break;
|
|
}
|
|
}
|
|
if (cache == null) {
|
|
cache = new CachedFontContent(fnt);
|
|
myFontCache.add(cache);
|
|
}
|
|
|
|
myLastCache = cache;
|
|
cache.addContent(g, data, start, end, x, y, color, drawWhitespace);
|
|
}
|
|
|
|
private static boolean spacesOnly(CharSequence chars, int start, int end) {
|
|
for (int i = start; i < end; i++) {
|
|
if (chars.charAt(i) != ' ') return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void drawChars(@NotNull Graphics g, CharSequence data, int start, int end, int x, int y, boolean drawWhitespace) {
|
|
g.drawString(data.subSequence(start, end).toString(), x, y);
|
|
|
|
if (drawWhitespace) {
|
|
Color oldColor = g.getColor();
|
|
g.setColor(myScheme.getColor(EditorColors.WHITESPACES_COLOR));
|
|
final FontMetrics metrics = g.getFontMetrics();
|
|
|
|
for (int i = start; i < end; i++) {
|
|
final char c = data.charAt(i);
|
|
final int charWidth = isOracleRetina ? GraphicsUtil.charWidth(c, g.getFont()) : metrics.charWidth(c);
|
|
|
|
if (c == ' ') {
|
|
g.fillRect(x + (charWidth >> 1), y, 1, 1);
|
|
}
|
|
else if (c == IDEOGRAPHIC_SPACE) {
|
|
final int charHeight = getCharHeight();
|
|
g.drawRect(x + 2, y - charHeight, charWidth - 4, charHeight);
|
|
}
|
|
|
|
x += charWidth;
|
|
}
|
|
g.setColor(oldColor);
|
|
}
|
|
}
|
|
|
|
private int getTextSegmentWidth(@NotNull CharSequence text,
|
|
int start,
|
|
int end,
|
|
int xStart,
|
|
@JdkConstants.FontStyle int fontType,
|
|
@NotNull Rectangle clip) {
|
|
int x = xStart;
|
|
|
|
for (int i = start; i < end && xStart < clip.x + clip.width; i++) {
|
|
char c = text.charAt(i);
|
|
if (c == '\t') {
|
|
x = EditorUtil.nextTabStop(x, this);
|
|
}
|
|
else {
|
|
x += EditorUtil.charWidth(c, fontType, this);
|
|
}
|
|
if (x > clip.x + clip.width) {
|
|
break;
|
|
}
|
|
}
|
|
return x - xStart;
|
|
}
|
|
|
|
@Override
|
|
public int getLineHeight() {
|
|
if (myUseNewRendering) return myView.getLineHeight();
|
|
assertReadAccess();
|
|
int lineHeight = myLineHeight;
|
|
if (lineHeight < 0) {
|
|
FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN));
|
|
int fontMetricsHeight = fontMetrics.getHeight();
|
|
lineHeight = (int)(fontMetricsHeight * (isOneLineMode() ? 1 : myScheme.getLineSpacing()));
|
|
if (lineHeight <= 0) {
|
|
lineHeight = fontMetricsHeight;
|
|
if (lineHeight <= 0) {
|
|
lineHeight = 12;
|
|
}
|
|
}
|
|
assert lineHeight > 0 : lineHeight;
|
|
myLineHeight = lineHeight;
|
|
}
|
|
return lineHeight;
|
|
}
|
|
|
|
public int getDescent() {
|
|
if (myUseNewRendering) return myView.getDescent();
|
|
if (myDescent != -1) {
|
|
return myDescent;
|
|
}
|
|
FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN));
|
|
myDescent = fontMetrics.getDescent();
|
|
return myDescent;
|
|
}
|
|
|
|
@NotNull
|
|
public FontMetrics getFontMetrics(@JdkConstants.FontStyle int fontType) {
|
|
if (myPlainFontMetrics == null) {
|
|
assertIsDispatchThread();
|
|
myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN));
|
|
myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD));
|
|
myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC));
|
|
myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC));
|
|
}
|
|
|
|
if (fontType == Font.PLAIN) return myPlainFontMetrics;
|
|
if (fontType == Font.BOLD) return myBoldFontMetrics;
|
|
if (fontType == Font.ITALIC) return myItalicFontMetrics;
|
|
if (fontType == (Font.BOLD | Font.ITALIC)) return myBoldItalicFontMetrics;
|
|
|
|
LOG.error("Unknown font type: " + fontType);
|
|
|
|
return myPlainFontMetrics;
|
|
}
|
|
|
|
public int getCharHeight() {
|
|
if (myUseNewRendering) return myView.getCharHeight();
|
|
if (myCharHeight == -1) {
|
|
assertIsDispatchThread();
|
|
FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN));
|
|
myCharHeight = fontMetrics.charWidth('a');
|
|
}
|
|
return myCharHeight;
|
|
}
|
|
|
|
public int getPreferredHeight() {
|
|
if (ourIsUnitTestMode && getUserData(DO_DOCUMENT_UPDATE_TEST) == null) {
|
|
return 1;
|
|
}
|
|
|
|
if (isOneLineMode()) return getLineHeight();
|
|
|
|
// Preferred height of less than a single line height doesn't make sense:
|
|
// at least a single line with a blinking caret on it is to be displayed
|
|
int size = Math.max(getVisibleLineCount(), 1) * getLineHeight();
|
|
|
|
if (mySettings.isAdditionalPageAtBottom()) {
|
|
int lineHeight = getLineHeight();
|
|
int visibleAreaHeight = getScrollingModel().getVisibleArea().height;
|
|
// There is a possible case that user with 'show additional page at bottom' scrolls to that virtual page; switched to another
|
|
// editor (another tab); and then returns to the previously used editor (the one scrolled to virtual page). We want to preserve
|
|
// correct view size then because viewport position is set to the end of the original text otherwise.
|
|
if (visibleAreaHeight > 0 || myVirtualPageHeight <= 0) {
|
|
myVirtualPageHeight = Math.max(visibleAreaHeight - 2 * lineHeight, lineHeight);
|
|
}
|
|
|
|
return size + Math.max(myVirtualPageHeight, 0);
|
|
}
|
|
|
|
return size + mySettings.getAdditionalLinesCount() * getLineHeight();
|
|
}
|
|
|
|
public Dimension getPreferredSize() {
|
|
if (myUseNewRendering) return myView.getPreferredSize();
|
|
if (ourIsUnitTestMode && getUserData(DO_DOCUMENT_UPDATE_TEST) == null) {
|
|
return new Dimension(1, 1);
|
|
}
|
|
|
|
final Dimension draft = getSizeWithoutCaret();
|
|
final int additionalSpace = shouldRespectAdditionalColumns()
|
|
? mySettings.getAdditionalColumnsCount() * EditorUtil.getSpaceWidth(Font.PLAIN, this)
|
|
: 0;
|
|
|
|
if (!myDocument.isInBulkUpdate()) {
|
|
for (Caret caret : myCaretModel.getAllCarets()) {
|
|
if (caret.isUpToDate()) {
|
|
int caretX = visualPositionToXY(caret.getVisualPosition()).x;
|
|
draft.width = Math.max(caretX, draft.width);
|
|
}
|
|
}
|
|
}
|
|
draft.width += additionalSpace;
|
|
return draft;
|
|
}
|
|
|
|
private boolean shouldRespectAdditionalColumns() {
|
|
return !mySoftWrapModel.isSoftWrappingEnabled()
|
|
|| mySoftWrapModel.isRespectAdditionalColumns()
|
|
|| mySizeContainer.getContentSize().getWidth() > myScrollingModel.getVisibleArea().getWidth();
|
|
}
|
|
|
|
private Dimension getSizeWithoutCaret() {
|
|
Dimension size = mySizeContainer.getContentSize();
|
|
return new Dimension(size.width, getPreferredHeight());
|
|
}
|
|
|
|
@NotNull
|
|
@Override
|
|
public Dimension getContentSize() {
|
|
if (myUseNewRendering) return myView.getPreferredSize();
|
|
Dimension size = mySizeContainer.getContentSize();
|
|
return new Dimension(size.width, size.height + mySettings.getAdditionalLinesCount() * getLineHeight());
|
|
}
|
|
|
|
@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 logicalPositionToOffset(pos, true);
|
|
}
|
|
|
|
@Override
|
|
public int logicalPositionToOffset(@NotNull LogicalPosition pos, boolean softWrapAware) {
|
|
if (myUseNewRendering) return myView.logicalPositionToOffset(pos);
|
|
if (softWrapAware) {
|
|
return mySoftWrapModel.logicalPositionToOffset(pos);
|
|
}
|
|
assertReadAccess();
|
|
if (myDocument.getLineCount() == 0) return 0;
|
|
|
|
if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line);
|
|
if (pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column);
|
|
|
|
if (pos.line >= myDocument.getLineCount()) {
|
|
return myDocument.getTextLength();
|
|
}
|
|
|
|
int start = myDocument.getLineStartOffset(pos.line);
|
|
if (pos.column == 0) return start;
|
|
int end = myDocument.getLineEndOffset(pos.line);
|
|
|
|
int x = getDocument().getLineNumber(start) == 0 ? getPrefixTextWidthInPixels() : 0;
|
|
|
|
int result = EditorUtil.calcSoftWrapUnawareOffset(this, myDocument.getImmutableCharSequence(), start, end, pos.column,
|
|
EditorUtil.getTabSize(this), x, new int[]{0}, null);
|
|
if (result >= 0) {
|
|
return result;
|
|
}
|
|
|
|
return end;
|
|
}
|
|
|
|
@Override
|
|
public void setLastColumnNumber(int val) {
|
|
assertIsDispatchThread();
|
|
myLastColumnNumber = val;
|
|
}
|
|
|
|
@Override
|
|
public int getLastColumnNumber() {
|
|
assertReadAccess();
|
|
return myLastColumnNumber;
|
|
}
|
|
|
|
/**
|
|
* @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.getFoldedLinesCountBefore(getDocument().getTextLength() + 1);
|
|
}
|
|
|
|
@Override
|
|
@NotNull
|
|
public VisualPosition logicalToVisualPosition(@NotNull LogicalPosition logicalPos) {
|
|
return logicalToVisualPosition(logicalPos, true);
|
|
}
|
|
|
|
@Override
|
|
@NotNull
|
|
public VisualPosition logicalToVisualPosition(@NotNull LogicalPosition logicalPos, boolean softWrapAware) {
|
|
if (myUseNewRendering) return myView.logicalToVisualPosition(logicalPos);
|
|
return doLogicalToVisualPosition(logicalPos, softWrapAware, 0);
|
|
}
|
|
|
|
@NotNull
|
|
private VisualPosition doLogicalToVisualPosition(@NotNull LogicalPosition logicalPos, boolean softWrapAware,
|
|
// TODO den remove as soon as the problem is fixed.
|
|
int stackDepth) {
|
|
assertReadAccess();
|
|
if (!myFoldingModel.isFoldingEnabled() && !mySoftWrapModel.isSoftWrappingEnabled()) {
|
|
return new VisualPosition(logicalPos.line, logicalPos.column);
|
|
}
|
|
|
|
int offset = logicalPositionToOffset(logicalPos);
|
|
|
|
FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset);
|
|
if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) {
|
|
if (offset < getDocument().getTextLength()) {
|
|
offset = outermostCollapsed.getStartOffset();
|
|
LogicalPosition foldStart = offsetToLogicalPosition(offset);
|
|
// TODO den remove as soon as the problem is fixed.
|
|
if (stackDepth > 15) {
|
|
LOG.error("Detected potential StackOverflowError at logical->visual position mapping. Given logical position: '" +
|
|
logicalPos + "'. State: " + dumpState());
|
|
stackDepth = -1;
|
|
}
|
|
return doLogicalToVisualPosition(foldStart, true, stackDepth + 1);
|
|
}
|
|
else {
|
|
offset = outermostCollapsed.getEndOffset() + 3; // WTF?
|
|
}
|
|
}
|
|
|
|
int line = logicalPos.line;
|
|
int column = logicalPos.column;
|
|
|
|
int foldedLinesCountBefore = myFoldingModel.getFoldedLinesCountBefore(offset);
|
|
line -= foldedLinesCountBefore;
|
|
if (line < 0) {
|
|
LogMessageEx.error(
|
|
LOG, "Invalid LogicalPosition -> VisualPosition processing", String.format(
|
|
"Given logical position: %s; matched line: %d; fold lines before: %d, state: %s",
|
|
logicalPos, line, foldedLinesCountBefore, dumpState()
|
|
));
|
|
}
|
|
|
|
FoldRegion[] topLevel = myFoldingModel.fetchTopLevel();
|
|
LogicalPosition anchorFoldingPosition = logicalPos;
|
|
for (int idx = myFoldingModel.getLastCollapsedRegionBefore(offset); idx >= 0 && topLevel != null; idx--) {
|
|
FoldRegion region = topLevel[idx];
|
|
if (region.isValid()) {
|
|
if (region.getDocument().getLineNumber(region.getEndOffset()) == anchorFoldingPosition.line && region.getEndOffset() <= offset) {
|
|
LogicalPosition foldStart = offsetToLogicalPosition(region.getStartOffset());
|
|
LogicalPosition foldEnd = offsetToLogicalPosition(region.getEndOffset());
|
|
column += foldStart.column + region.getPlaceholderText().length() - foldEnd.column;
|
|
offset = region.getStartOffset();
|
|
anchorFoldingPosition = foldStart;
|
|
}
|
|
else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
VisualPosition softWrapUnawarePosition = new VisualPosition(line, Math.max(0, column));
|
|
if (softWrapAware) {
|
|
return mySoftWrapModel.adjustVisualPosition(logicalPos, softWrapUnawarePosition);
|
|
}
|
|
return softWrapUnawarePosition;
|
|
}
|
|
|
|
@Nullable
|
|
private FoldRegion getLastCollapsedBeforePosition(@NotNull VisualPosition visualPos) {
|
|
FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel();
|
|
|
|
if (topLevelCollapsed == null) return null;
|
|
|
|
int start = 0;
|
|
int end = topLevelCollapsed.length - 1;
|
|
int i = 0;
|
|
|
|
while (start <= end) {
|
|
i = (start + end) / 2;
|
|
FoldRegion region = topLevelCollapsed[i];
|
|
if (!region.isValid()) {
|
|
// Folding model is inconsistent (update in progress).
|
|
return null;
|
|
}
|
|
int regionVisualLine = offsetToVisualLine(region.getEndOffset() - 1);
|
|
if (regionVisualLine < visualPos.line) {
|
|
start = i + 1;
|
|
}
|
|
else {
|
|
if (regionVisualLine > visualPos.line) {
|
|
end = i - 1;
|
|
}
|
|
else {
|
|
VisualPosition visFoldEnd = offsetToVisualPosition(region.getEndOffset() - 1);
|
|
if (visFoldEnd.column < visualPos.column) {
|
|
start = i + 1;
|
|
}
|
|
else {
|
|
if (visFoldEnd.column > visualPos.column) {
|
|
end = i - 1;
|
|
}
|
|
else {
|
|
i--;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
while (i >= 0 && i < topLevelCollapsed.length) {
|
|
if (topLevelCollapsed[i].isValid()) break;
|
|
i--;
|
|
}
|
|
|
|
if (i >= 0 && i < topLevelCollapsed.length) {
|
|
FoldRegion region = topLevelCollapsed[i];
|
|
VisualPosition visFoldEnd = offsetToVisualPosition(region.getEndOffset() - 1);
|
|
if (visFoldEnd.line > visualPos.line || visFoldEnd.line == visualPos.line && visFoldEnd.column > visualPos.column) {
|
|
i--;
|
|
if (i >= 0) {
|
|
return topLevelCollapsed[i];
|
|
}
|
|
return null;
|
|
}
|
|
return region;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
@NotNull
|
|
public LogicalPosition visualToLogicalPosition(@NotNull VisualPosition visiblePos) {
|
|
return visualToLogicalPosition(visiblePos, true);
|
|
}
|
|
|
|
@Override
|
|
@NotNull
|
|
public LogicalPosition visualToLogicalPosition(@NotNull VisualPosition visiblePos, boolean softWrapAware) {
|
|
if (myUseNewRendering) return myView.visualToLogicalPosition(visiblePos);
|
|
assertReadAccess();
|
|
if (softWrapAware) {
|
|
return mySoftWrapModel.visualToLogicalPosition(visiblePos);
|
|
}
|
|
if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column);
|
|
|
|
int line = visiblePos.line;
|
|
int column = visiblePos.column;
|
|
|
|
FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos);
|
|
|
|
if (lastCollapsedBefore != null) {
|
|
int logFoldEndLine = offsetToLogicalLine(lastCollapsedBefore.getEndOffset());
|
|
int visFoldEndLine = logicalToVisualLine(logFoldEndLine);
|
|
|
|
line = logFoldEndLine + visiblePos.line - visFoldEndLine;
|
|
if (visFoldEndLine == visiblePos.line) {
|
|
LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset(), false);
|
|
VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd, false);
|
|
if (visiblePos.column >= visFoldEnd.column) {
|
|
column = logFoldEnd.column + visiblePos.column - visFoldEnd.column;
|
|
}
|
|
else {
|
|
return offsetToLogicalPosition(lastCollapsedBefore.getStartOffset(), false);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (column < 0) column = 0;
|
|
|
|
return new LogicalPosition(line, column);
|
|
}
|
|
|
|
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 calcColumnNumber(offset, lineIndex, true, myDocument.getImmutableCharSequence());
|
|
}
|
|
|
|
public int calcColumnNumber(int offset, int lineIndex, boolean softWrapAware, @NotNull CharSequence documentCharSequence) {
|
|
if (myUseNewRendering) return myView.offsetToLogicalPosition(offset).column;
|
|
if (myDocument.getTextLength() == 0) return 0;
|
|
|
|
int lineStartOffset = myDocument.getLineStartOffset(lineIndex);
|
|
if (lineStartOffset == offset) return 0;
|
|
int lineEndOffset = myDocument.getLineEndOffset(lineIndex);
|
|
if (lineEndOffset < offset) offset = lineEndOffset; // handling the case when offset is inside non-normalized line terminator
|
|
int column = EditorUtil.calcColumnNumber(this, documentCharSequence, lineStartOffset, offset);
|
|
|
|
if (softWrapAware) {
|
|
int line = offsetToLogicalLine(offset);
|
|
return mySoftWrapModel.adjustLogicalPosition(new LogicalPosition(line, column), offset).column;
|
|
}
|
|
else {
|
|
return column;
|
|
}
|
|
}
|
|
|
|
private LogicalPosition getLogicalPositionForScreenPos(int x, int y, boolean trimToLineWidth) {
|
|
if (x < 0) {
|
|
x = 0;
|
|
}
|
|
|
|
LogicalPosition pos = xyToLogicalPosition(new Point(x, y));
|
|
|
|
int column = pos.column;
|
|
int line = pos.line;
|
|
int softWrapLinesBeforeTargetLogicalLine = pos.softWrapLinesBeforeCurrentLogicalLine;
|
|
int softWrapLinesOnTargetLogicalLine = pos.softWrapLinesOnCurrentLogicalLine;
|
|
int softWrapColumns = pos.softWrapColumnDiff;
|
|
boolean leansForward = pos.leansForward;
|
|
boolean leansRight = pos.visualPositionLeansRight;
|
|
|
|
final int totalLines = myDocument.getLineCount();
|
|
if (totalLines <= 0) {
|
|
return new LogicalPosition(0, 0);
|
|
}
|
|
|
|
if (line >= totalLines && totalLines > 0) {
|
|
int visibleLineCount = getVisibleLineCount();
|
|
int newY = visibleLineCount > 0 ? visibleLineToY(visibleLineCount - 1) : 0;
|
|
if (newY > 0 && newY == y) {
|
|
newY = visibleLineToY(getVisibleLogicalLinesCount());
|
|
}
|
|
if (newY >= y) {
|
|
LogMessageEx.error(LOG, "cycled moveCaretToScreenPos() detected", String.format("x=%d, y=%d\nstate=%s", x, y, dumpState()));
|
|
throw new IllegalStateException("cycled moveCaretToScreenPos() detected");
|
|
}
|
|
return getLogicalPositionForScreenPos(x, newY, trimToLineWidth);
|
|
}
|
|
|
|
if (!mySettings.isVirtualSpace() && trimToLineWidth) {
|
|
int lineEndOffset = myDocument.getLineEndOffset(line);
|
|
int lineEndColumn = calcColumnNumber(lineEndOffset, line);
|
|
if (column > lineEndColumn) {
|
|
column = lineEndColumn;
|
|
leansForward = true;
|
|
leansRight = true;
|
|
if (softWrapColumns != 0) {
|
|
softWrapColumns -= column - lineEndColumn;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!mySettings.isCaretInsideTabs()) {
|
|
int offset = logicalPositionToOffset(new LogicalPosition(line, column));
|
|
CharSequence text = myDocument.getImmutableCharSequence();
|
|
if (offset >= 0 && offset < myDocument.getTextLength()) {
|
|
if (text.charAt(offset) == '\t') {
|
|
column = calcColumnNumber(offset, line);
|
|
}
|
|
}
|
|
}
|
|
return pos.visualPositionAware ?
|
|
new LogicalPosition(
|
|
line, column, softWrapLinesBeforeTargetLogicalLine, softWrapLinesOnTargetLogicalLine, softWrapColumns,
|
|
pos.foldedLines, pos.foldingColumnDiff, leansForward, leansRight
|
|
) :
|
|
new LogicalPosition(line, column, leansForward);
|
|
}
|
|
|
|
private boolean checkIgnore(@NotNull MouseEvent e, boolean isFinalCheck) {
|
|
if (!myIgnoreMouseEventsConsecutiveToInitial) {
|
|
myInitialMouseEvent = null;
|
|
return false;
|
|
}
|
|
|
|
if (myInitialMouseEvent != null &&
|
|
(e.getComponent() != myInitialMouseEvent.getComponent() || !e.getPoint().equals(myInitialMouseEvent.getPoint()))) {
|
|
myIgnoreMouseEventsConsecutiveToInitial = false;
|
|
myInitialMouseEvent = null;
|
|
return false;
|
|
}
|
|
|
|
if (isFinalCheck) {
|
|
myIgnoreMouseEventsConsecutiveToInitial = false;
|
|
myInitialMouseEvent = null;
|
|
}
|
|
|
|
e.consume();
|
|
|
|
return true;
|
|
}
|
|
|
|
private void processMouseReleased(@NotNull MouseEvent e) {
|
|
if (checkIgnore(e, true)) 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(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
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 new DataContext() {
|
|
@Override
|
|
public Object getData(String dataId) {
|
|
if (CommonDataKeys.PROJECT.is(dataId)) {
|
|
return myProject;
|
|
}
|
|
return original.getData(dataId);
|
|
}
|
|
};
|
|
}
|
|
|
|
|
|
@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;
|
|
}
|
|
}
|
|
myEditorComponent.setCursor(UIUtil.getTextCursor(getBackgroundColor()));
|
|
}
|
|
}
|
|
|
|
private void runMouseDraggedCommand(@NotNull final MouseEvent e) {
|
|
if (myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) {
|
|
return;
|
|
}
|
|
myCommandProcessor.executeCommand(myProject, new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
processMouseDragged(e);
|
|
}
|
|
}, "", MOUSE_DRAGGED_GROUP, UndoConfirmationPolicy.DEFAULT, getDocument());
|
|
}
|
|
|
|
private void processMouseDragged(@NotNull MouseEvent e) {
|
|
if (JBSwingUtilities.isRightMouseButton(e)) {
|
|
return;
|
|
}
|
|
|
|
if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) {
|
|
// 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;
|
|
LogicalPosition newLogicalCaret = getLogicalPositionForScreenPos(x, y, !multiCaretSelection);
|
|
if (multiCaretSelection) {
|
|
myMultiSelectionInProgress = true;
|
|
myRectangularSelectionInProgress = columnSelectionDrag || addRectangularSelectionEvent;
|
|
myTargetMultiSelectionPosition = xyToVisualPosition(new Point(Math.max(x, 0), Math.max(y, 0)));
|
|
}
|
|
else {
|
|
getCaretModel().moveToLogicalPosition(newLogicalCaret);
|
|
}
|
|
|
|
int newCaretOffset = getCaretModel().getOffset();
|
|
VisualPosition 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()) {
|
|
if (myDraggedRange == null) {
|
|
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 clearDraggedRange() {
|
|
if (myDraggedRange != null) {
|
|
myDraggedRange.dispose();
|
|
myDraggedRange = null;
|
|
}
|
|
}
|
|
|
|
private void createSelectionTill(@NotNull LogicalPosition targetPosition) {
|
|
List<CaretState> caretStates = new ArrayList<CaretState>(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);
|
|
}
|
|
else {
|
|
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 = null;
|
|
@NotNull private final MyRepaintRunnable myRepaintRunnable;
|
|
private ScheduledFuture<?> mySchedulerHandle;
|
|
|
|
private RepaintCursorCommand() {
|
|
myRepaintRunnable = new MyRepaintRunnable();
|
|
}
|
|
|
|
private class MyRepaintRunnable implements Runnable {
|
|
@Override
|
|
public void run() {
|
|
if (myEditor != null) {
|
|
myEditor.myCaretCursor.repaint();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void start() {
|
|
if (mySchedulerHandle != null) {
|
|
mySchedulerHandle.cancel(false);
|
|
}
|
|
mySchedulerHandle = JobScheduler.getScheduler().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) {
|
|
SwingUtilities.invokeLater(myRepaintRunnable);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void updateCaretCursor() {
|
|
myUpdateCursor = true;
|
|
}
|
|
|
|
private void setCursorPosition() {
|
|
final List<CaretRectangle> caretPoints = new ArrayList<CaretRectangle>();
|
|
for (Caret caret : getCaretModel().getAllCarets()) {
|
|
boolean isRtl = caret.isAtRtlLocation();
|
|
VisualPosition caretPosition = caret.getVisualPosition();
|
|
Point pos1 = visualPositionToXY(caretPosition);
|
|
Point pos2 = visualPositionToXY(new VisualPosition(caretPosition.line, Math.max(0, caretPosition.column + (isRtl ? -1 : 1))));
|
|
caretPoints.add(new CaretRectangle(pos1, Math.abs(pos2.x - pos1.x), 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;
|
|
}
|
|
}
|
|
|
|
private class CaretCursor {
|
|
private CaretRectangle[] myLocations;
|
|
private boolean myEnabled;
|
|
|
|
@SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"})
|
|
private boolean myIsShown = false;
|
|
private long myStartTime = 0;
|
|
|
|
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;
|
|
if (!myUseNewRendering) {
|
|
repaint();
|
|
}
|
|
}
|
|
|
|
private void repaint() {
|
|
if (myUseNewRendering) {
|
|
myView.repaintCarets();
|
|
}
|
|
else {
|
|
for (CaretRectangle location : myLocations) {
|
|
myEditorComponent.repaintEditorComponent(location.myPoint.x, location.myPoint.y, location.myWidth, getLineHeight());
|
|
}
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
private CaretRectangle[] getCaretLocations(boolean onlyIfShown) {
|
|
if (onlyIfShown && (!isEnabled() || !myIsShown || isRendererMode() || !IJSwingUtilities.hasFocus(getContentComponent()))) return null;
|
|
return myLocations;
|
|
}
|
|
|
|
private void paint(@NotNull Graphics g) {
|
|
CaretRectangle[] locations = getCaretLocations(true);
|
|
if (locations == null) return;
|
|
|
|
for (CaretRectangle location : myLocations) {
|
|
paintAt(g, location.myPoint.x, location.myPoint.y, location.myWidth, location.myCaret);
|
|
}
|
|
}
|
|
|
|
private void paintAt(@NotNull Graphics g, int x, int y, int width, Caret caret) {
|
|
int lineHeight = getLineHeight();
|
|
|
|
Rectangle viewRectangle = getScrollingModel().getVisibleArea();
|
|
if (x - viewRectangle.x < 0) {
|
|
return;
|
|
}
|
|
|
|
|
|
g.setColor(myScheme.getColor(EditorColors.CARET_COLOR));
|
|
|
|
Graphics2D originalG = IdeBackgroundUtil.getOriginalGraphics(g);
|
|
if (!paintBlockCaret()) {
|
|
if (UIUtil.isRetina()) {
|
|
originalG.fillRect(x, y, mySettings.getLineCursorWidth(), lineHeight);
|
|
}
|
|
else {
|
|
for (int i = 0; i < mySettings.getLineCursorWidth(); i++) {
|
|
UIUtil.drawLine(g, x + i, y, x + i, y + lineHeight - 1);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
Color caretColor = myScheme.getColor(EditorColors.CARET_COLOR);
|
|
if (caretColor == null) caretColor = new JBColor(Gray._0, Gray._255);
|
|
g.setColor(caretColor);
|
|
originalG.fillRect(x, y, width, lineHeight - 1);
|
|
final LogicalPosition startPosition = caret == null ? getCaretModel().getLogicalPosition() : caret.getLogicalPosition();
|
|
final int offset = logicalPositionToOffset(startPosition);
|
|
CharSequence chars = myDocument.getImmutableCharSequence();
|
|
if (chars.length() > offset && myDocument.getTextLength() > offset) {
|
|
FoldRegion folding = myFoldingModel.getCollapsedRegionAtOffset(offset);
|
|
final char ch;
|
|
if (folding == null || folding.isExpanded()) {
|
|
ch = chars.charAt(offset);
|
|
}
|
|
else {
|
|
VisualPosition visual = caret == null ? getCaretModel().getVisualPosition() : caret.getVisualPosition();
|
|
VisualPosition foldingPosition = offsetToVisualPosition(folding.getStartOffset());
|
|
if (visual.line == foldingPosition.line) {
|
|
ch = folding.getPlaceholderText().charAt(visual.column - foldingPosition.column);
|
|
}
|
|
else {
|
|
ch = chars.charAt(offset);
|
|
}
|
|
}
|
|
//don't worry it's cheap. Cache is not required
|
|
IterationState state = new IterationState(EditorImpl.this, offset, offset + 1, true);
|
|
TextAttributes attributes = state.getMergedAttributes();
|
|
FontInfo info = EditorUtil.fontForChar(ch, attributes.getFontType(), EditorImpl.this);
|
|
if (info != null) {
|
|
g.setFont(info.getFont());
|
|
}
|
|
//todo[kb]
|
|
//in case of italic style we paint out of the cursor block. Painting the symbol to a dedicated buffered image
|
|
//solves the problem, but still looks weird because it leaves colored pixels at right.
|
|
g.setColor(ColorUtil.isDark(caretColor) ? CURSOR_FOREGROUND_LIGHT : CURSOR_FOREGROUND_DARK);
|
|
g.drawChars(new char[]{ch}, 0, 1, x, y + getAscent());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
boolean paintBlockCaret() {
|
|
return myIsInsertMode == mySettings.isBlockCursor();
|
|
}
|
|
|
|
private class ScrollingTimer {
|
|
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 = 0;
|
|
private int yPassedCycles = 0;
|
|
|
|
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, new ActionListener() {
|
|
@Override
|
|
public void actionPerformed(@NotNull ActionEvent e) {
|
|
myCommandProcessor.executeCommand(myProject, new DocumentRunnable(myDocument, myProject) {
|
|
@Override
|
|
public void run() {
|
|
// We experienced situation when particular editor was disposed but the timer was still on.
|
|
if (isDisposed()) {
|
|
myTimer.stop();
|
|
return;
|
|
}
|
|
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 {
|
|
@NonNls private static final String APPLE_LAF_AQUA_SCROLL_BAR_UI_CLASS = "apple.laf.AquaScrollBarUI";
|
|
private ScrollBarUI myPersistentUI;
|
|
|
|
private MyScrollBar(@JdkConstants.AdjustableOrientation int orientation) {
|
|
super(orientation);
|
|
}
|
|
|
|
void setPersistentUI(ScrollBarUI ui) {
|
|
myPersistentUI = ui;
|
|
setUI(ui);
|
|
}
|
|
|
|
@Override
|
|
public boolean canBePreprocessed(MouseEvent e) {
|
|
return JBScrollPane.canBePreprocessed(e, this);
|
|
}
|
|
|
|
@Override
|
|
public void setUI(ScrollBarUI ui) {
|
|
if (myPersistentUI == null) myPersistentUI = ui;
|
|
super.setUI(myPersistentUI);
|
|
setOpaque(false);
|
|
}
|
|
|
|
/**
|
|
* This is helper method. It returns height of the top (decrease) scroll bar
|
|
* button. Please note, that it's possible to return real height only if scroll bar
|
|
* is instance of BasicScrollBarUI. Otherwise it returns fake (but good enough :) )
|
|
* value.
|
|
*/
|
|
int getDecScrollButtonHeight() {
|
|
ScrollBarUI barUI = getUI();
|
|
Insets insets = getInsets();
|
|
int top = Math.max(0, insets.top);
|
|
if (barUI instanceof ButtonlessScrollBarUI) {
|
|
return top + ((ButtonlessScrollBarUI)barUI).getDecrementButtonHeight();
|
|
}
|
|
if (barUI instanceof BasicScrollBarUI) {
|
|
try {
|
|
JButton decrButtonValue = (JButton)decrButtonField.get(barUI);
|
|
LOG.assertTrue(decrButtonValue != null);
|
|
return top + decrButtonValue.getHeight();
|
|
}
|
|
catch (Exception exc) {
|
|
throw new IllegalStateException(exc);
|
|
}
|
|
}
|
|
return top + 15;
|
|
}
|
|
|
|
/**
|
|
* This is helper method. It returns height of the bottom (increase) scroll bar
|
|
* button. Please note, that it's possible to return real height only if scroll bar
|
|
* is instance of BasicScrollBarUI. Otherwise it returns fake (but good enough :) )
|
|
* value.
|
|
*/
|
|
int getIncScrollButtonHeight() {
|
|
ScrollBarUI barUI = getUI();
|
|
Insets insets = getInsets();
|
|
if (barUI instanceof ButtonlessScrollBarUI) {
|
|
return insets.top + ((ButtonlessScrollBarUI)barUI).getIncrementButtonHeight();
|
|
}
|
|
if (barUI instanceof BasicScrollBarUI) {
|
|
try {
|
|
JButton incrButtonValue = (JButton)incrButtonField.get(barUI);
|
|
LOG.assertTrue(incrButtonValue != null);
|
|
return insets.bottom + incrButtonValue.getHeight();
|
|
}
|
|
catch (Exception exc) {
|
|
throw new IllegalStateException(exc);
|
|
}
|
|
}
|
|
if (APPLE_LAF_AQUA_SCROLL_BAR_UI_CLASS.equals(barUI.getClass().getName())) {
|
|
return insets.bottom + 30;
|
|
}
|
|
return insets.bottom + 15;
|
|
}
|
|
|
|
@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);
|
|
}
|
|
|
|
public void registerRepaintCallback(@Nullable ButtonlessScrollBarUI.ScrollbarRepaintCallback callback) {
|
|
if (myPersistentUI instanceof ButtonlessScrollBarUI) {
|
|
((ButtonlessScrollBarUI)myPersistentUI).registerRepaintCallback(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 {
|
|
@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) {
|
|
if (!isCutEnabled(dataContext)) return false;
|
|
return 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() {
|
|
ThreadingAssertions.assertEventDispatchThread();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
@NotNull
|
|
MyScrollBar getHorizontalScrollBar() {
|
|
return (MyScrollBar)myScrollPane.getHorizontalScrollBar();
|
|
}
|
|
|
|
private int getMouseSelectionState() {
|
|
return myMouseSelectionState;
|
|
}
|
|
|
|
private void setMouseSelectionState(int mouseSelectionState) {
|
|
if (getMouseSelectionState() == mouseSelectionState) return;
|
|
|
|
myMouseSelectionState = mouseSelectionState;
|
|
myMouseSelectionChangeTimestamp = System.currentTimeMillis();
|
|
|
|
myMouseSelectionStateAlarm.cancelAllRequests();
|
|
if (myMouseSelectionState != MOUSE_SELECTION_STATE_NONE) {
|
|
if (myMouseSelectionStateResetRunnable == null) {
|
|
myMouseSelectionStateResetRunnable = new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
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) {
|
|
getInputMethodRequests();
|
|
myInputMethodRequestsHandler.replaceInputMethodText(e);
|
|
}
|
|
|
|
void inputMethodCaretPositionChanged(@NotNull InputMethodEvent e) {
|
|
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();
|
|
}
|
|
|
|
public EditorDropHandler getDropHandler() {
|
|
return myDropHandler;
|
|
}
|
|
|
|
public void setDropHandler(@NotNull EditorDropHandler dropHandler) {
|
|
myDropHandler = dropHandler;
|
|
}
|
|
|
|
|
|
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(new Computable<Rectangle>() {
|
|
@Override
|
|
public Rectangle compute() {
|
|
return myDelegate.getTextLocation(offset);
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public TextHitInfo getLocationOffset(final int x, final int y) {
|
|
return execute(new Computable<TextHitInfo>() {
|
|
@Override
|
|
public TextHitInfo compute() {
|
|
return myDelegate.getLocationOffset(x, y);
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public int getInsertPositionOffset() {
|
|
return execute(new Computable<Integer>() {
|
|
@Override
|
|
public Integer compute() {
|
|
return myDelegate.getInsertPositionOffset();
|
|
}
|
|
});
|
|
}
|
|
|
|
@NotNull
|
|
@Override
|
|
public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex,
|
|
final AttributedCharacterIterator.Attribute[] attributes) {
|
|
return execute(new Computable<AttributedCharacterIterator>() {
|
|
@Override
|
|
public AttributedCharacterIterator compute() {
|
|
return myDelegate.getCommittedText(beginIndex, endIndex, attributes);
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public int getCommittedTextLength() {
|
|
return execute(new Computable<Integer>() {
|
|
@Override
|
|
public Integer compute() {
|
|
return myDelegate.getCommittedTextLength();
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
@Nullable
|
|
public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) {
|
|
return execute(new Computable<AttributedCharacterIterator>() {
|
|
@Override
|
|
public AttributedCharacterIterator compute() {
|
|
return myDelegate.getSelectedText(attributes);
|
|
}
|
|
});
|
|
}
|
|
|
|
private static <T> T execute(final Computable<T> computable) {
|
|
if (ApplicationManager.getApplication().isDispatchThread()) {
|
|
return computable.compute();
|
|
}
|
|
else {
|
|
final Ref<T> ref = Ref.create();
|
|
try {
|
|
GuiUtils.invokeAndWait(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
ref.set(computable.compute());
|
|
}
|
|
});
|
|
}
|
|
catch (InterruptedException e) {
|
|
LOG.error(e);
|
|
}
|
|
catch (InvocationTargetException e) {
|
|
LOG.error(e);
|
|
}
|
|
return ref.get();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
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(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
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.org/browse/JDK-8074882 is fixed, this workaround should be replaced with a proper solution.
|
|
myNeedToSelectPreviousChar = false;
|
|
getCaretModel().runForEachCaret(new CaretAction() {
|
|
@Override
|
|
public void perform(Caret 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(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
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(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false);
|
|
}
|
|
});
|
|
|
|
composedTextRange = ProperTextRange.from(getCaretModel().getOffset(), composedText.length());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private class MyMouseAdapter extends MouseAdapter {
|
|
private boolean mySelectionTweaked;
|
|
|
|
@Override
|
|
public void mousePressed(@NotNull MouseEvent e) {
|
|
requestFocus();
|
|
runMousePressedCommand(e);
|
|
}
|
|
|
|
@Override
|
|
public void mouseReleased(@NotNull MouseEvent e) {
|
|
myMousePressArea = 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.<CaretState>emptyList();
|
|
myCurrentDragIsSubstantial = false;
|
|
clearDraggedRange();
|
|
|
|
|
|
mySelectionTweaked = false;
|
|
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) {
|
|
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 = new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (processMousePressed(e) && myProject != null && !myProject.isDefault()) {
|
|
IdeDocumentHistory.getInstance(myProject).includeCurrentCommandAsNavigation();
|
|
}
|
|
}
|
|
};
|
|
myCommandProcessor
|
|
.executeCommand(myProject, runnable, "", DocCommandGroupId.noneGroupId(getDocument()), UndoConfirmationPolicy.DEFAULT,
|
|
getDocument());
|
|
}
|
|
else {
|
|
processMousePressed(e);
|
|
}
|
|
}
|
|
|
|
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 (!mySelectionTweaked) {
|
|
tweakSelectionIfNecessary(e);
|
|
}
|
|
if (e.isConsumed()) {
|
|
return;
|
|
}
|
|
|
|
myScrollingTimer.stop();
|
|
EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e));
|
|
for (EditorMouseListener listener : myMouseListeners) {
|
|
listener.mouseReleased(event);
|
|
if (event.isConsumed()) {
|
|
e.consume();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (myCommandProcessor != null) {
|
|
Runnable runnable = new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
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 = new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
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)) {
|
|
mySelectionTweaked = true;
|
|
}
|
|
else {
|
|
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).
|
|
if (e.getSource() != myGutterComponent
|
|
|| (eventArea != EditorMouseEventArea.LINE_MARKERS_AREA && eventArea != EditorMouseEventArea.ANNOTATIONS_AREA)) {
|
|
LogicalPosition pos = getLogicalPositionForScreenPos(x, y, true);
|
|
if (toggleCaret) {
|
|
VisualPosition visualPosition = logicalToVisualPosition(pos);
|
|
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().moveToLogicalPosition(pos);
|
|
}
|
|
}
|
|
else if (myCaretModel.supportsMultipleCarets() && e.getSource() != myGutterComponent && isCreateRectangularSelectionEvent(e)) {
|
|
mySelectionModel.setBlockSelection(myCaretModel.getLogicalPosition(), pos);
|
|
}
|
|
else {
|
|
getCaretModel().removeSecondaryCarets();
|
|
getCaretModel().moveToLogicalPosition(pos);
|
|
}
|
|
}
|
|
|
|
if (e.isPopupTrigger()) return false;
|
|
|
|
requestFocus();
|
|
|
|
int caretOffset = getCaretModel().getOffset();
|
|
|
|
int newStart = mySelectionModel.getSelectionStart();
|
|
int newEnd = mySelectionModel.getSelectionEnd();
|
|
|
|
boolean isNavigation = oldStart == oldEnd && newStart == newEnd && oldStart != newStart;
|
|
|
|
myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y));
|
|
myMousePressedInsideSelection = mySelectionModel.hasSelection() && caretOffset >= mySelectionModel.getSelectionStart() &&
|
|
caretOffset <= mySelectionModel.getSelectionEnd();
|
|
|
|
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 (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.isPopupTrigger()
|
|
&& (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 KeymapUtil.isMouseActionEvent(e, IdeActions.ACTION_EDITOR_ADD_OR_REMOVE_CARET) || isAddRectangularSelectionEvent(e);
|
|
}
|
|
|
|
private static boolean isAddRectangularSelectionEvent(@NotNull MouseEvent e) {
|
|
return KeymapUtil.isMouseActionEvent(e, IdeActions.ACTION_EDITOR_ADD_RECTANGULAR_SELECTION_ON_MOUSE_DRAG);
|
|
}
|
|
|
|
private static boolean isCreateRectangularSelectionEvent(@NotNull MouseEvent e) {
|
|
return KeymapUtil.isMouseActionEvent(e, IdeActions.ACTION_EDITOR_CREATE_RECTANGULAR_SELECTION);
|
|
}
|
|
|
|
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</code> if action that produces given event will trigger editor selection change; <code>false</code> 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</code> button. The selection should be adjusted if necessary.
|
|
*
|
|
* @param e event for mouse click on gutter area
|
|
* @return <code>true</code> if editor's selection is changed because of the click; <code>false</code> 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 = yPositionToVisibleLine(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;
|
|
}
|
|
|
|
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) {
|
|
validateMousePointer(e);
|
|
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 FontPreferences myFontPreferences = new FontPreferences();
|
|
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 = null;
|
|
private int myMaxFontSize = OptionsConstants.MAX_EDITOR_FONT_SIZE;
|
|
private int myFontSize = -1;
|
|
private String myFaceName = null;
|
|
|
|
private MyColorSchemeDelegate(@Nullable EditorColorsScheme globalScheme) {
|
|
super(globalScheme == null ? EditorColorsManager.getInstance().getGlobalScheme() : globalScheme);
|
|
myCustomGlobalScheme = globalScheme;
|
|
updateGlobalScheme();
|
|
}
|
|
|
|
private void reinitFonts() {
|
|
String editorFontName = getEditorFontName();
|
|
int editorFontSize = getEditorFontSize();
|
|
|
|
myFontPreferences.clear();
|
|
myFontPreferences.register(editorFontName, editorFontSize);
|
|
|
|
EditorColorsScheme delegate = getDelegate();
|
|
List<String> secondaryFonts =
|
|
delegate != null ? delegate.getFontPreferences().getRealFontFamilies() : ContainerUtil.<String>emptyList();
|
|
boolean first = true; //skip delegate's primary font
|
|
for (String font : secondaryFonts) {
|
|
if (!first) {
|
|
myFontPreferences.register(font, editorFontSize);
|
|
}
|
|
first = false;
|
|
}
|
|
|
|
myFontsMap = new EnumMap<EditorFontType, Font>(EditorFontType.class);
|
|
|
|
Font plainFont = new Font(editorFontName, Font.PLAIN, editorFontSize);
|
|
Font boldFont = new Font(editorFontName, Font.BOLD, editorFontSize);
|
|
Font italicFont = new Font(editorFontName, Font.ITALIC, editorFontSize);
|
|
Font boldItalicFont = new Font(editorFontName, Font.BOLD | Font.ITALIC, editorFontSize);
|
|
|
|
myFontsMap.put(EditorFontType.PLAIN, plainFont);
|
|
myFontsMap.put(EditorFontType.BOLD, boldFont);
|
|
myFontsMap.put(EditorFontType.ITALIC, italicFont);
|
|
myFontsMap.put(EditorFontType.BOLD_ITALIC, boldItalicFont);
|
|
}
|
|
|
|
protected 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(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();
|
|
}
|
|
|
|
@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 Object clone() {
|
|
return null;
|
|
}
|
|
|
|
public 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(OptionsConstants.MAX_EDITOR_FONT_SIZE, globalFontSize);
|
|
reinitFonts();
|
|
clearSettingsCache();
|
|
}
|
|
|
|
@Override
|
|
public void setConsoleFontSize(int fontSize) {
|
|
getDelegate().setConsoleFontSize(fontSize);
|
|
reinitSettings();
|
|
}
|
|
}
|
|
|
|
private static class MyTransferHandler extends TransferHandler {
|
|
private static EditorImpl getEditor(@NotNull JComponent comp) {
|
|
EditorComponentImpl editorComponent = (EditorComponentImpl)comp;
|
|
return editorComponent.getEditor();
|
|
}
|
|
|
|
@Override
|
|
public boolean importData(@NotNull final JComponent comp, @NotNull final Transferable t) {
|
|
final EditorImpl editor = getEditor(comp);
|
|
|
|
final EditorDropHandler dropHandler = editor.getDropHandler();
|
|
if (dropHandler != null && dropHandler.canHandleDrop(t.getTransferDataFlavors())) {
|
|
dropHandler.handleDrop(t, editor.getProject(), null);
|
|
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, new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
ApplicationManager.getApplication().runWriteAction(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
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);
|
|
((EditorTextInsertHandler)pasteHandler).execute(editor, editor.getDataContext(), new Producer<Transferable>() {
|
|
@Override
|
|
public Transferable produce() {
|
|
return 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;
|
|
}
|
|
|
|
@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;
|
|
}
|
|
if (editor.isViewer()) return false;
|
|
|
|
int offset = editor.getCaretModel().getOffset();
|
|
if (editor.getDocument().getRangeGuard(offset, offset) != null) return false;
|
|
|
|
for (DataFlavor transferFlavor : transferFlavors) {
|
|
if (transferFlavor.equals(DataFlavor.stringFlavor)) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@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)) return;
|
|
|
|
final EditorImpl editor = getEditor(source);
|
|
if (action == MOVE && !editor.isViewer() && editor.myDraggedRange != null) {
|
|
if (!FileDocumentManager.getInstance().requestWriting(editor.getDocument(), editor.getProject())) {
|
|
return;
|
|
}
|
|
CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
ApplicationManager.getApplication().runWriteAction(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
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());
|
|
}
|
|
|
|
editor.clearDraggedRange();
|
|
}
|
|
}
|
|
|
|
class EditorDocumentAdapter implements PrioritizedDocumentListener {
|
|
@Override
|
|
public void beforeDocumentChange(@NotNull DocumentEvent e) {
|
|
beforeChangedUpdate(e);
|
|
}
|
|
|
|
@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();
|
|
}
|
|
}
|
|
|
|
@SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"})
|
|
private class EditorSizeContainer {
|
|
/**
|
|
* Holds logical line widths in pixels.
|
|
*/
|
|
private TIntArrayList myLineWidths;
|
|
private int maxCalculatedLine = -1;
|
|
|
|
/**
|
|
* Holds value that indicates if line widths recalculation should be performed.
|
|
*/
|
|
private volatile boolean myIsDirty;
|
|
|
|
/**
|
|
* Holds number of the last logical line affected by the last document change.
|
|
*/
|
|
private int myOldEndLine;
|
|
|
|
private Dimension mySize;
|
|
private int myMaxWidth = -1;
|
|
|
|
public synchronized void reset() {
|
|
int lineCount = getDocument().getLineCount();
|
|
myLineWidths = new TIntArrayList(lineCount + 300);
|
|
insertNewLines(lineCount, 0);
|
|
maxCalculatedLine = -1;
|
|
myIsDirty = true;
|
|
}
|
|
|
|
private void insertNewLines(int lineCount, int index) {
|
|
int[] values = new int[lineCount];
|
|
Arrays.fill(values, -1);
|
|
myLineWidths.insert(index, values);
|
|
if (index <= maxCalculatedLine) {
|
|
maxCalculatedLine += lineCount;
|
|
}
|
|
}
|
|
|
|
@SuppressWarnings({"NonPrivateFieldAccessedInSynchronizedContext"})
|
|
public synchronized void beforeChange(@NotNull DocumentEvent e) {
|
|
if (myDocument.isInBulkUpdate()) {
|
|
myMaxWidth = mySize == null ? -1 : mySize.width;
|
|
}
|
|
|
|
myOldEndLine = offsetToLogicalLine(e.getOffset() + e.getOldLength());
|
|
}
|
|
|
|
/**
|
|
* Notifies current size container about document content change.
|
|
* <p/>
|
|
* Every change is assumed to be identified by three characteristics - start, ole end and new end lines.
|
|
* <b>Example:</b>
|
|
* <pre>
|
|
* <ol>
|
|
* <li>
|
|
* Consider that we have the following document initially:
|
|
* <pre>
|
|
* line 1
|
|
* line 2
|
|
* line 3
|
|
* </pre>
|
|
* </li>
|
|
* <li>
|
|
* Let's assume that the user selected the last two lines and typed 'new line' (that effectively removed selected text).
|
|
* Current document state:
|
|
* <pre>
|
|
* line 1
|
|
* new line
|
|
* </pre>
|
|
* </li>
|
|
* <li>
|
|
* Current method is expected to be called with the following parameters:
|
|
* <ul>
|
|
* <li><b>startLine</b> is 1'</li>
|
|
* <li><b>oldEndLine</b> is 2'</li>
|
|
* <li><b>newEndLine</b> is 1'</li>
|
|
* </ul>
|
|
* </li>
|
|
* </ol>
|
|
* </pre>
|
|
*
|
|
* @param startLine logical line that contains changed fragment start offset
|
|
* @param newEndLine logical line that contains changed fragment end
|
|
* @param oldEndLine logical line that contained changed fragment end
|
|
*/
|
|
public synchronized void update(int startLine, int newEndLine, int oldEndLine) {
|
|
final int lineWidthSize = myLineWidths.size();
|
|
if (lineWidthSize == 0 || myDocument.getTextLength() <= 0) {
|
|
reset();
|
|
}
|
|
else {
|
|
final int min = Math.min(oldEndLine, newEndLine);
|
|
final boolean toAddNewLines = min >= lineWidthSize;
|
|
|
|
if (toAddNewLines) {
|
|
insertNewLines(min - lineWidthSize + 1, lineWidthSize);
|
|
}
|
|
|
|
for (int i = min; i > startLine - 1; i--) {
|
|
myLineWidths.set(i, -1);
|
|
if (maxCalculatedLine == i) maxCalculatedLine--;
|
|
}
|
|
if (newEndLine > oldEndLine) {
|
|
insertNewLines(newEndLine - oldEndLine, oldEndLine + 1);
|
|
}
|
|
else if (oldEndLine > newEndLine && !toAddNewLines && newEndLine + 1 < lineWidthSize) {
|
|
int length = Math.min(oldEndLine, lineWidthSize) - newEndLine - 1;
|
|
int index = newEndLine + 1;
|
|
myLineWidths.remove(index, length);
|
|
if (index <= maxCalculatedLine) {
|
|
maxCalculatedLine -= length;
|
|
}
|
|
}
|
|
myIsDirty = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notifies current container about visual width change of the target logical line.
|
|
* <p/>
|
|
* Please note that there is a possible case that particular logical line is represented in more than one visual lines,
|
|
* hence, this method may be called multiple times with the same logical line argument but different with values. Current
|
|
* container is expected to store max of the given values then.
|
|
*
|
|
* @param logicalLine logical line which visual width is changed
|
|
* @param widthInPixels visual width of the given logical line
|
|
*/
|
|
public synchronized void updateLineWidthIfNecessary(int logicalLine, int widthInPixels) {
|
|
if (logicalLine < myLineWidths.size()) {
|
|
int currentWidth = myLineWidths.get(logicalLine);
|
|
if (widthInPixels > currentWidth) {
|
|
myLineWidths.set(logicalLine, widthInPixels);
|
|
}
|
|
if (widthInPixels > myMaxWidth) {
|
|
myMaxWidth = widthInPixels;
|
|
}
|
|
maxCalculatedLine = Math.max(maxCalculatedLine, logicalLine);
|
|
}
|
|
}
|
|
|
|
public synchronized void changedUpdate(@NotNull DocumentEvent e) {
|
|
int startLine = e.getOldLength() == 0 ? myOldEndLine : myDocument.getLineNumber(e.getOffset());
|
|
int newEndLine = e.getNewLength() == 0 ? startLine : myDocument.getLineNumber(e.getOffset() + e.getNewLength());
|
|
int oldEndLine = myOldEndLine;
|
|
|
|
update(startLine, newEndLine, oldEndLine);
|
|
}
|
|
|
|
@SuppressWarnings({"NonPrivateFieldAccessedInSynchronizedContext", "AssignmentToForLoopParameter"})
|
|
private void validateSizes() {
|
|
if (!myIsDirty && !(myLinePaintersWidth > myMaxWidth)) return;
|
|
|
|
synchronized (this) {
|
|
if (!myIsDirty) return;
|
|
int lineCount = Math.min(myLineWidths.size(), myDocument.getLineCount());
|
|
|
|
if (myMaxWidth != -1 && myDocument.isInBulkUpdate()) {
|
|
mySize = new Dimension(myMaxWidth, getLineHeight() * lineCount);
|
|
myIsDirty = false;
|
|
return;
|
|
}
|
|
|
|
final CharSequence text = myDocument.getImmutableCharSequence();
|
|
int documentLength = myDocument.getTextLength();
|
|
int x = 0;
|
|
boolean lastLineLengthCalculated = false;
|
|
|
|
List<? extends SoftWrap> softWraps = getSoftWrapModel().getRegisteredSoftWraps();
|
|
int softWrapsIndex = -1;
|
|
|
|
CharWidthCache charWidthCache = new CharWidthCache(EditorImpl.this);
|
|
|
|
for (int line = 0; line < lineCount; line++) {
|
|
if (myLineWidths.getQuick(line) != -1) continue;
|
|
if (line == lineCount - 1) {
|
|
lastLineLengthCalculated = true;
|
|
}
|
|
|
|
x = 0;
|
|
int offset = myDocument.getLineStartOffset(line);
|
|
|
|
if (offset >= myDocument.getTextLength()) {
|
|
myLineWidths.set(line, 0);
|
|
maxCalculatedLine = Math.max(maxCalculatedLine, line);
|
|
break;
|
|
}
|
|
|
|
if (softWrapsIndex < 0) {
|
|
softWrapsIndex = getSoftWrapModel().getSoftWrapIndex(offset);
|
|
if (softWrapsIndex < 0) {
|
|
softWrapsIndex = -softWrapsIndex - 1;
|
|
}
|
|
}
|
|
|
|
int endLine;
|
|
if (maxCalculatedLine < line + 1) {
|
|
endLine = lineCount;
|
|
}
|
|
else {
|
|
for (endLine = line + 1; endLine < maxCalculatedLine; endLine++) {
|
|
if (myLineWidths.getQuick(endLine) != -1) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
int endOffset = endLine >= lineCount ? documentLength : myDocument.getLineEndOffset(endLine);
|
|
for (
|
|
FoldRegion region = myFoldingModel.getCollapsedRegionAtOffset(endOffset);
|
|
region != null && endOffset < myDocument.getTextLength();
|
|
region = myFoldingModel.getCollapsedRegionAtOffset(endOffset)) {
|
|
final int lineNumber = myDocument.getLineNumber(region.getEndOffset());
|
|
endOffset = myDocument.getLineEndOffset(lineNumber);
|
|
}
|
|
if (endOffset > myDocument.getTextLength()) {
|
|
break;
|
|
}
|
|
|
|
IterationState state = new IterationState(EditorImpl.this, offset, endOffset, false);
|
|
int fontType = state.getMergedAttributes().getFontType();
|
|
|
|
int maxPreviousSoftWrappedWidth = -1;
|
|
|
|
while (offset < documentLength && line < lineCount) {
|
|
char c = text.charAt(offset);
|
|
if (offset >= state.getEndOffset()) {
|
|
state.advance();
|
|
fontType = state.getMergedAttributes().getFontType();
|
|
}
|
|
|
|
while (softWrapsIndex < softWraps.size() && line < lineCount) {
|
|
SoftWrap softWrap = softWraps.get(softWrapsIndex);
|
|
if (softWrap.getStart() > offset) {
|
|
break;
|
|
}
|
|
softWrapsIndex++;
|
|
if (softWrap.getStart() == offset) {
|
|
maxPreviousSoftWrappedWidth = Math.max(maxPreviousSoftWrappedWidth, x);
|
|
x = softWrap.getIndentInPixels();
|
|
}
|
|
}
|
|
|
|
FoldRegion collapsed = state.getCurrentFold();
|
|
if (collapsed != null) {
|
|
String placeholder = collapsed.getPlaceholderText();
|
|
for (int i = 0; i < placeholder.length(); i++) {
|
|
x += charWidthCache.charWidth(placeholder.charAt(i), fontType);
|
|
}
|
|
offset = collapsed.getEndOffset();
|
|
line = myDocument.getLineNumber(offset);
|
|
}
|
|
else if (c == '\t') {
|
|
x = EditorUtil.nextTabStop(x, EditorImpl.this);
|
|
offset++;
|
|
}
|
|
else if (c == '\n') {
|
|
int width = Math.max(x, maxPreviousSoftWrappedWidth);
|
|
myLineWidths.set(line, width);
|
|
maxCalculatedLine = Math.max(maxCalculatedLine, line);
|
|
if (line + 1 >= lineCount || myLineWidths.getQuick(line + 1) != -1) break;
|
|
offset++;
|
|
x = 0;
|
|
//noinspection AssignmentToForLoopParameter
|
|
line++;
|
|
if (line == lineCount - 1) {
|
|
lastLineLengthCalculated = true;
|
|
}
|
|
}
|
|
else {
|
|
x += charWidthCache.charWidth(c, fontType);
|
|
offset++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (lineCount > 0 && lastLineLengthCalculated) {
|
|
myLineWidths.set(lineCount - 1,
|
|
x); // Last line can be non-zero length and won't be caught by in-loop procedure since latter only react on \n's
|
|
maxCalculatedLine = Math.max(maxCalculatedLine, lineCount - 1);
|
|
}
|
|
|
|
// There is a following possible situation:
|
|
// 1. Big document is opened at editor;
|
|
// 2. Soft wraps are calculated for the current visible area;
|
|
// 2. The user scrolled down;
|
|
// 3. The user significantly reduced visible area width (say, reduced it twice);
|
|
// 4. Soft wraps are calculated for the current visible area;
|
|
// We need to consider only the widths for the logical lines that are completely shown at the current visible area then.
|
|
// I.e. we shouldn't use widths of the lines that are not shown for max width calculation because previous widths are calculated
|
|
// for another visible area width.
|
|
int startToUse = 0;
|
|
int endToUse = Math.min(lineCount, myLineWidths.size());
|
|
if (endToUse > 0 && getSoftWrapModel().isSoftWrappingEnabled()) {
|
|
Rectangle visibleArea = getScrollingModel().getVisibleArea();
|
|
startToUse = EditorUtil.yPositionToLogicalLine(EditorImpl.this, visibleArea.getLocation());
|
|
endToUse = Math.min(endToUse, EditorUtil.yPositionToLogicalLine(EditorImpl.this, visibleArea.y + visibleArea.height));
|
|
if (endToUse <= startToUse) {
|
|
// There is a possible case that there is the only soft-wrapped line, i.e. end == start. We still want to update the
|
|
// size container's width then.
|
|
endToUse = Math.min(myLineWidths.size(), startToUse + 1);
|
|
}
|
|
}
|
|
int maxWidth = 0;
|
|
for (int i = startToUse; i < endToUse; i++) {
|
|
maxWidth = Math.max(maxWidth, myLineWidths.getQuick(i));
|
|
}
|
|
|
|
mySize = new Dimension(maxWidth, getLineHeight() * Math.max(getVisibleLineCount(), 1));
|
|
|
|
myIsDirty = false;
|
|
}
|
|
}
|
|
|
|
@NotNull
|
|
private Dimension getContentSize() {
|
|
validateSizes();
|
|
return new Dimension(Math.max(mySize.width, myLinePaintersWidth), mySize.height);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
@NotNull
|
|
public EditorGutter getGutter() {
|
|
return getGutterComponentEx();
|
|
}
|
|
|
|
@Override
|
|
public int calcColumnNumber(@NotNull CharSequence text, int start, int offset, int tabSize) {
|
|
if (myUseNewRendering) return myView.offsetToLogicalPosition(offset).column;
|
|
IterationState state = new IterationState(this, start, offset, false);
|
|
int fontType = state.getMergedAttributes().getFontType();
|
|
int column = 0;
|
|
int x = 0;
|
|
int plainSpaceSize = EditorUtil.getSpaceWidth(Font.PLAIN, this);
|
|
for (int i = start; i < offset; i++) {
|
|
if (i >= state.getEndOffset()) {
|
|
state.advance();
|
|
fontType = state.getMergedAttributes().getFontType();
|
|
}
|
|
|
|
SoftWrap softWrap = getSoftWrapModel().getSoftWrap(i);
|
|
if (softWrap != null) {
|
|
x = softWrap.getIndentInPixels();
|
|
}
|
|
|
|
char c = text.charAt(i);
|
|
if (c == '\t') {
|
|
int prevX = x;
|
|
x = EditorUtil.nextTabStop(x, this);
|
|
column += EditorUtil.columnsNumber(c, x, prevX, plainSpaceSize);
|
|
}
|
|
else {
|
|
x += EditorUtil.charWidth(c, fontType, this);
|
|
column++;
|
|
}
|
|
}
|
|
|
|
return column;
|
|
}
|
|
|
|
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 class MyScrollPane extends JBScrollPane {
|
|
private MyScrollPane() {
|
|
super(0);
|
|
setupCorners();
|
|
}
|
|
|
|
@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);
|
|
}
|
|
super.layout();
|
|
}
|
|
|
|
@Override
|
|
protected void processMouseWheelEvent(@NotNull MouseWheelEvent e) {
|
|
if (mySettings.isWheelFontChangeEnabled() && !MouseGestureManager.getInstance().hasTrackpad()) {
|
|
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
|
|
public JScrollBar createHorizontalScrollBar() {
|
|
return new MyScrollBar(Adjustable.HORIZONTAL);
|
|
}
|
|
|
|
@Override
|
|
protected void setupCorners() {
|
|
super.setupCorners();
|
|
setBorder(new TablessBorder());
|
|
}
|
|
|
|
protected boolean isOverlaidScrollbar(@Nullable JScrollBar scrollbar) {
|
|
ScrollBarUI vsbUI = scrollbar == null ? null : scrollbar.getUI();
|
|
return vsbUI instanceof ButtonlessScrollBarUI && !((ButtonlessScrollBarUI)vsbUI).alwaysShowTrack();
|
|
}
|
|
}
|
|
|
|
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().SHOW_MAIN_TOOLBAR || UISettings.getInstance().SHOW_NAVIGATION_BAR ||
|
|
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);
|
|
}
|
|
|
|
public 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 = 0;
|
|
|
|
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) {
|
|
drawCharsCached(g, new CharArrayCharSequence(data), start, end, x, y, fontInfo, color, false);
|
|
}
|
|
}
|
|
|
|
public interface WhitespacePaintingStrategy {
|
|
boolean showWhitespaceAtOffset(int offset);
|
|
}
|
|
|
|
private static final WhitespacePaintingStrategy PAINT_NO_WHITESPACE = new WhitespacePaintingStrategy() {
|
|
@Override
|
|
public boolean showWhitespaceAtOffset(int offset) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Strategy, controlled by current editor settings. Usable only for the current line.
|
|
public class LineWhitespacePaintingStrategy implements WhitespacePaintingStrategy {
|
|
private final boolean myWhitespaceShown = mySettings.isWhitespacesShown();
|
|
private final boolean myLeadingWhitespaceShown = mySettings.isLeadingWhitespaceShown();
|
|
private final boolean myInnerWhitespaceShown = mySettings.isInnerWhitespaceShown();
|
|
private final boolean myTrailingWhitespaceShown = mySettings.isTrailingWhitespaceShown();
|
|
|
|
// Offsets on current line where leading whitespace ends and trailing whitespace starts correspondingly.
|
|
private int currentLeadingEdge;
|
|
private int currentTrailingEdge;
|
|
|
|
// Updates the state, to be used for the line, iterator is currently at.
|
|
public void update(CharSequence chars, LineIterator iterator) {
|
|
int lineStart = iterator.getStart();
|
|
int lineEnd = iterator.getEnd() - iterator.getSeparatorLength();
|
|
update(chars, lineStart, lineEnd);
|
|
}
|
|
|
|
public void update(CharSequence chars, int lineStart, int lineEnd) {
|
|
if (myWhitespaceShown
|
|
&& (myLeadingWhitespaceShown || myInnerWhitespaceShown || myTrailingWhitespaceShown)
|
|
&& !(myLeadingWhitespaceShown && myInnerWhitespaceShown && myTrailingWhitespaceShown)) {
|
|
currentTrailingEdge = CharArrayUtil.shiftBackward(chars, lineStart, lineEnd - 1, WHITESPACE_CHARS) + 1;
|
|
currentLeadingEdge = CharArrayUtil.shiftForward(chars, lineStart, currentTrailingEdge, WHITESPACE_CHARS);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean showWhitespaceAtOffset(int offset) {
|
|
return myWhitespaceShown
|
|
&& (offset < currentLeadingEdge ? myLeadingWhitespaceShown :
|
|
offset >= currentTrailingEdge ? myTrailingWhitespaceShown :
|
|
myInnerWhitespaceShown);
|
|
}
|
|
}
|
|
}
|