| /* |
| * Copyright 2000-2014 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.Patches; |
| 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.*; |
| import com.intellij.ide.dnd.DnDManager; |
| import com.intellij.ide.ui.UISettings; |
| import com.intellij.openapi.Disposable; |
| import com.intellij.openapi.actionSystem.*; |
| import com.intellij.openapi.actionSystem.ex.ActionManagerEx; |
| import com.intellij.openapi.actionSystem.impl.MouseGestureManager; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.application.ModalityState; |
| import com.intellij.openapi.application.impl.LaterInvocator; |
| import com.intellij.openapi.command.CommandProcessor; |
| import com.intellij.openapi.command.UndoConfirmationPolicy; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.*; |
| import com.intellij.openapi.editor.actionSystem.*; |
| import com.intellij.openapi.editor.actions.EditorActionUtil; |
| import com.intellij.openapi.editor.colors.*; |
| import com.intellij.openapi.editor.colors.impl.DelegateColorScheme; |
| import com.intellij.openapi.editor.event.*; |
| import com.intellij.openapi.editor.ex.*; |
| 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.markup.*; |
| import com.intellij.openapi.fileEditor.FileDocumentManager; |
| import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory; |
| import com.intellij.openapi.fileEditor.impl.EditorsSplitters; |
| import com.intellij.openapi.keymap.Keymap; |
| import com.intellij.openapi.keymap.KeymapManager; |
| import com.intellij.openapi.progress.ProgressManager; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.ui.Queryable; |
| import com.intellij.openapi.util.*; |
| import com.intellij.openapi.util.registry.Registry; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.openapi.wm.IdeFocusManager; |
| import com.intellij.openapi.wm.IdeGlassPane; |
| import com.intellij.openapi.wm.ToolWindowAnchor; |
| import com.intellij.openapi.wm.ex.ToolWindowManagerEx; |
| import com.intellij.psi.PsiDocumentManager; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.ui.*; |
| import com.intellij.ui.components.JBLayeredPane; |
| import com.intellij.ui.components.JBScrollBar; |
| import com.intellij.ui.components.JBScrollPane; |
| import com.intellij.ui.components.OrphanGuardian; |
| import com.intellij.util.*; |
| 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.*; |
| 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.Timer; |
| import javax.swing.border.Border; |
| import javax.swing.border.EmptyBorder; |
| import javax.swing.plaf.ScrollBarUI; |
| import javax.swing.plaf.basic.BasicScrollBarUI; |
| import java.awt.*; |
| import java.awt.datatransfer.DataFlavor; |
| import java.awt.datatransfer.StringSelection; |
| import java.awt.datatransfer.Transferable; |
| import java.awt.dnd.DropTarget; |
| import java.awt.dnd.DropTargetAdapter; |
| import java.awt.dnd.DropTargetDragEvent; |
| import java.awt.dnd.DropTargetDropEvent; |
| import java.awt.event.*; |
| import java.awt.font.TextHitInfo; |
| import java.awt.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.*; |
| import java.util.List; |
| import java.util.concurrent.ScheduledFuture; |
| import java.util.concurrent.TimeUnit; |
| |
| public final class EditorImpl 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"); |
| 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 JBColor CURSOR_FOREGROUND = new JBColor(Gray._255, 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()); |
| |
| 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 final 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; |
| 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 Alarm myAppleRepaintAlarm; |
| |
| private final Alarm myMouseSelectionStateAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD); |
| private Runnable myMouseSelectionStateResetRunnable; |
| |
| private boolean myEmbeddedIntoDialogWrapper; |
| @Nullable private CachedFontContent myLastCache; |
| private int myDragOnGutterSelectionStartLine = -1; |
| |
| /** |
| * Positive value is assumed to indicate that space width for all interested font styles (bold, italic etc) is equal. |
| * That value is stored at the current field. |
| */ |
| private int myCommonSpaceWidth; |
| |
| /** |
| * There is a possible case that specific font is used for particular text drawing operation (e.g. for 'before' and 'after' |
| * soft wraps drawings). Hence, even if mySpacesHaveSameWidth is <code>true</code>, space size for that specific |
| * font may be different. So, we define additional flag that should indicate that {@link #myLastCache} should be reset. |
| */ |
| private boolean myForceRefreshFont; |
| private boolean mySoftWrapsChanged; |
| |
| 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 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 LogicalPosition myLastMousePressedLocation; |
| private VisualPosition myTargetMultiSelectionPosition; |
| private boolean myMultiSelectionInProgress; |
| 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; |
| |
| static { |
| ourCaretBlinkingCommand = new RepaintCursorCommand(); |
| ourCaretBlinkingCommand.start(); |
| } |
| |
| 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); |
| |
| mySelectionModel = new SelectionModelImpl(this); |
| myMarkupModel = new EditorMarkupModelImpl(this); |
| myFoldingModel = new FoldingModelImpl(this); |
| myCaretModel = new CaretModelImpl(this); |
| mySoftWrapModel = new SoftWrapModelImpl(this); |
| 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 |
| 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); |
| 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 (renderersChanged || highlighter.getErrorStripeMarkColor() != null) { |
| ((EditorMarkupModelImpl)getMarkupModel()).repaint(start, end); |
| } |
| |
| if (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 |
| } |
| updateCaretVisualPosition(e); |
| myPrimaryCaret = myCaretModel.getPrimaryCaret(); |
| } |
| |
| @Override |
| public void caretRemoved(CaretEvent e) { |
| updateCaretVisualPosition(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 softWrapAdded(@NotNull SoftWrap softWrap) { |
| mySoftWrapsChanged = true; |
| } |
| |
| @Override |
| public void softWrapsRemoved() { |
| mySoftWrapsChanged = true; |
| } |
| }); |
| |
| 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(); |
| myPanel = new JPanel(); |
| |
| myPanel.putClientProperty(OrphanGuardian.CLIENT_PROPERTY_KEY, new OrphanGuardian() { |
| |
| @Override |
| public void iterateOrphans(Consumer<JComponent> consumer) { |
| JComponent component = getPermanentHeaderComponent(); |
| if (component != null && !component.isValid()) { |
| consumer.consume(component); |
| } |
| } |
| }); |
| |
| myHeaderPanel = new MyHeaderPanel(); |
| myGutterComponent = new EditorGutterComponentImpl(this); |
| initComponent(); |
| myScrollingModel = new ScrollingModelImpl(this); |
| if (UISettings.getInstance().PRESENTATION_MODE) { |
| setFontSize(UISettings.getInstance().PRESENTATION_MODE_FONT_SIZE); |
| } |
| |
| myGutterComponent.updateSize(); |
| Dimension preferredSize = getPreferredSize(); |
| myEditorComponent.setSize(preferredSize); |
| |
| if (Patches.APPLE_BUG_ID_3716835) { |
| myScrollingModel.addVisibleAreaListener(new VisibleAreaListener() { |
| @Override |
| public void visibleAreaChanged(VisibleAreaEvent e) { |
| if (myAppleRepaintAlarm == null) { |
| myAppleRepaintAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD); |
| } |
| myAppleRepaintAlarm.cancelAllRequests(); |
| myAppleRepaintAlarm.addRequest(new Runnable() { |
| @Override |
| public void run() { |
| repaint(0, myDocument.getTextLength()); |
| } |
| }, 50, ModalityState.stateForComponent(myEditorComponent)); |
| } |
| }); |
| } |
| |
| 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 static void updateCaretVisualPosition(CaretEvent e) { |
| CaretImpl caretImpl = ((CaretImpl)e.getCaret()); |
| if (caretImpl != null) { |
| caretImpl.updateVisualPosition(); // repainting caret region |
| } |
| } |
| |
| @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 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(); |
| } |
| |
| @Override |
| public boolean isPurePaintingMode() { |
| return myPurePaintingMode; |
| } |
| |
| @Override |
| public void setPurePaintingMode(boolean enabled) { |
| myPurePaintingMode = enabled; |
| } |
| |
| @Override |
| public void registerScrollBarRepaintCallback(@Nullable RepaintCallback callback) { |
| myVerticalScrollBar.registerRepaintCallback(callback); |
| } |
| |
| @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) { |
| getSoftWrapModel().setPlace(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() { |
| mySizeContainer.reset(); |
| } |
| |
| @Override |
| public void reinitSettings() { |
| assertIsDispatchThread(); |
| myCharHeight = -1; |
| myLineHeight = -1; |
| myDescent = -1; |
| myPlainFontMetrics = null; |
| |
| clearTextWidthCache(); |
| |
| boolean softWrapsUsedBefore = mySoftWrapModel.isSoftWrappingEnabled(); |
| |
| mySettings.reinitSettings(); |
| mySoftWrapModel.reinitSettings(); |
| myCaretModel.reinitSettings(); |
| mySelectionModel.reinitSettings(); |
| ourCaretBlinkingCommand.setBlinkCaret(mySettings.isBlinkCaret()); |
| ourCaretBlinkingCommand.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); |
| mySizeContainer.reset(); |
| myFoldingModel.rebuild(); |
| |
| if (softWrapsUsedBefore ^ mySoftWrapModel.isSoftWrappingEnabled()) { |
| mySizeContainer.reset(); |
| validateSize(); |
| } |
| |
| for (EditorColorsScheme scheme = myScheme; scheme instanceof DelegateColorScheme; scheme = ((DelegateColorScheme)scheme).getDelegate()) { |
| if (scheme instanceof MyColorSchemeDelegate) { |
| ((MyColorSchemeDelegate)scheme).updateGlobalScheme(); |
| break; |
| } |
| } |
| myHighlighter.setColorScheme(myScheme); |
| myFoldingModel.refreshSettings(); |
| |
| myGutterComponent.reinitSettings(); |
| myGutterComponent.revalidate(); |
| |
| myEditorComponent.repaint(); |
| |
| initTabPainter(); |
| updateCaretCursor(); |
| |
| if (myInitialMouseEvent != null) { |
| myIgnoreMouseEventsConsecutiveToInitial = true; |
| } |
| |
| if (myCaretModel.supportsMultipleCarets()) { |
| myCaretModel.updateVisualPosition(); |
| } |
| else { |
| // There is a possible case that 'use soft wrap' setting value is changed and we need to repaint all affected lines then. |
| repaintToScreenBottom(getCaretModel().getLogicalPosition().line); |
| int y = getCaretModel().getVisualLineStart() * getLineHeight(); |
| myGutterComponent.repaint(0, y, myGutterComponent.getWidth(), myGutterComponent.getHeight() - y); |
| } |
| // 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 initTabPainter() { |
| myTabPainter = new ArrowPainter( |
| ColorProvider.byColorsScheme(myScheme, EditorColors.WHITESPACES_COLOR), |
| new Computable.PredefinedValueComputable(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; |
| |
| myFoldingModel.dispose(); |
| |
| mySoftWrapModel.release(); |
| |
| myMarkupModel.dispose(); |
| |
| myLineHeight = -1; |
| myCharHeight = -1; |
| myDescent = -1; |
| myPlainFontMetrics = null; |
| myScrollingModel.dispose(); |
| myGutterComponent.dispose(); |
| myMousePressedEvent = null; |
| myMouseMovedEvent = null; |
| Disposer.dispose(myCaretModel); |
| Disposer.dispose(mySoftWrapModel); |
| 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 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); |
| } |
| } |
| } |
| }; |
| |
| layeredPane.add(myScrollPane, JLayeredPane.DEFAULT_LAYER); |
| myPanel.add(layeredPane); |
| |
| new ContextMenuImpl(layeredPane, myScrollPane, this); |
| } |
| else { |
| myPanel.add(myScrollPane); |
| } |
| |
| myEditorComponent.addKeyListener(new KeyAdapter() { |
| @Override |
| public void keyTyped(@NotNull KeyEvent event) { |
| if (Patches.APPLE_BUG_ID_3337563) |
| return; // Everything is going through InputMethods under MacOS X in JDK releases earlier than 1.4.2_03-117.1 |
| if (event.isConsumed()) { |
| return; |
| } |
| if (processKeyTyped(event)) { |
| event.consume(); |
| } |
| } |
| }); |
| |
| 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(FocusEvent e) { |
| myCaretCursor.activate(); |
| for (Caret caret : myCaretModel.getAllCarets()) { |
| int caretLine = caret.getLogicalPosition().line; |
| repaintLines(caretLine, caretLine); |
| } |
| fireFocusGained(); |
| } |
| |
| @Override |
| public void focusLost(FocusEvent e) { |
| clearCaretThread(); |
| for (Caret caret : myCaretModel.getAllCarets()) { |
| int caretLine = caret.getLogicalPosition().line; |
| repaintLines(caretLine, caretLine); |
| } |
| fireFocusLost(); |
| } |
| }); |
| |
| new UiNotifyConnector(myEditorComponent, new Activatable.Adapter(){ |
| @Override |
| public void showNotify() { |
| myGutterComponent.updateSize(); |
| } |
| }); |
| |
| try { |
| final DropTarget dropTarget = myEditorComponent.getDropTarget(); |
| if (dropTarget != null) { // might be null in headless environment |
| dropTarget.addDropTargetListener(new DropTargetAdapter() { |
| @Override |
| public void drop(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(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(); |
| if (manager != null) { |
| 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 EditorGutterComponentEx getGutterComponentEx() { |
| return myGutterComponent; |
| } |
| |
| @Override |
| public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) { |
| myPropertyChangeSupport.addPropertyChangeListener(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); |
| if (myCaretModel.supportsMultipleCarets()) { |
| myCaretCursor.repaint(); |
| } |
| else { |
| //Repaint the caret line by moving caret to the same place |
| LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); |
| getCaretModel().moveToLogicalPosition(caretPosition); |
| } |
| } |
| |
| @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; |
| } |
| |
| private int yPositionToVisibleLine(int y) { |
| assert y >= 0 : y; |
| return y / getLineHeight(); |
| } |
| |
| @Override |
| @NotNull |
| public VisualPosition xyToVisualPosition(@NotNull Point p) { |
| int line = yPositionToVisibleLine(p.y); |
| int px = p.x; |
| if (line == 0 && myPrefixText != null) { |
| px -= myPrefixWidthInPixels; |
| } |
| |
| 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(p.x, 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(p.x, 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 outer; |
| } |
| 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 outer; |
| } |
| 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); |
| } |
| |
| @Override |
| @NotNull |
| public VisualPosition offsetToVisualPosition(int offset) { |
| 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 (softWrapAware) { |
| return mySoftWrapModel.offsetToLogicalPosition(offset); |
| } |
| int line = offsetToLogicalLine(offset, false); |
| 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) { |
| 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) { |
| 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; |
| } |
| |
| // 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 (line < 0) throw new IndexOutOfBoundsException("Wrong line: " + line); |
| return line * getLineHeight(); |
| } |
| |
| @Override |
| public void repaint(final int startOffset, int endOffset) { |
| if (!isShowing() || myDocument.isInBulkUpdate()) { |
| 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() + WAVE_HEIGHT; |
| |
| myEditorComponent.repaintEditorComponent(visibleArea.x, yStartLine, visibleArea.x + visibleArea.width, height); |
| myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), height); |
| } |
| |
| private void bulkUpdateStarted() { |
| } |
| |
| private void bulkUpdateFinished() { |
| |
| clearTextWidthCache(); |
| |
| mySelectionModel.removeBlockSelection(); |
| setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); |
| |
| mySizeContainer.reset(); |
| validateSize(); |
| |
| updateGutterSize(); |
| repaintToScreenBottom(0); |
| updateCaretCursor(); |
| } |
| |
| private void beforeChangedUpdate(@NotNull DocumentEvent e) { |
| if (isStickySelection()) { |
| setStickySelection(false); |
| } |
| if (myDocument.isInBulkUpdate()) { |
| // Assuming that the job is done at bulk listener callback methods. |
| return; |
| } |
| |
| Rectangle visibleArea = getScrollingModel().getVisibleArea(); |
| Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); |
| myCaretUpdateVShift = pos.y - visibleArea.y; |
| |
| // We assume that size container is already notified with the visual line widths during soft wraps processing |
| if (!mySoftWrapModel.isSoftWrappingEnabled()) { |
| mySizeContainer.beforeChange(e); |
| } |
| } |
| |
| private void changedUpdate(DocumentEvent e) { |
| if (myDocument.isInBulkUpdate()) return; |
| |
| clearTextWidthCache(); |
| mySelectionModel.removeBlockSelection(); |
| 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()) { |
| 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); |
| } |
| |
| Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); |
| int scrollOffset = caretLocation.y - myCaretUpdateVShift; |
| getScrollingModel().scrollVertically(scrollOffset); |
| } |
| |
| 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 void updateGutterSize() { |
| LaterInvocator.invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| myGutterComponent.updateSize(); |
| } |
| }); |
| } |
| |
| void validateSize() { |
| Dimension dim = getPreferredSize(); |
| |
| if (!dim.equals(myPreferredSize) && !myDocument.isInBulkUpdate()) { |
| dim = mySizeAdjustmentStrategy.adjust(dim, myPreferredSize, this); |
| if (dim == null) { |
| return; |
| } |
| myPreferredSize = dim; |
| |
| myGutterComponent.setLineNumberAreaWidth(new TIntFunction() { |
| @Override |
| public int execute(int lineNumber) { |
| return getFontMetrics(Font.PLAIN).stringWidth(Integer.toString(lineNumber + 2)) + 6; |
| } |
| }); |
| myGutterComponent.updateSize(); |
| |
| myEditorComponent.setSize(dim); |
| myEditorComponent.fireResized(); |
| |
| myMarkupModel.recalcEditorDimensions(); |
| myMarkupModel.repaint(-1, -1); |
| } |
| } |
| |
| void recalculateSizeAndRepaint() { |
| 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()); |
| } |
| |
| public void stopDumb() { |
| putUserData(BUFFER, null); |
| } |
| |
| /** |
| * {@link #stopDumbLater} or {@link #stopDumb} must be performed in finally |
| */ |
| public void startDumb() { |
| if (ApplicationManager.getApplication().isUnitTestMode()) return; |
| final JComponent component = getContentComponent(); |
| final Rectangle rect = ((JViewport)component.getParent()).getViewRect(); |
| BufferedImage image = UIUtil.createImage(rect.width, rect.height, BufferedImage.TYPE_INT_RGB); |
| final Graphics2D graphics = image.createGraphics(); |
| UISettings.setupAntialiasing(graphics); |
| graphics.translate(-rect.x, -rect.y); |
| graphics.setClip(rect.x, rect.y, rect.width, rect.height); |
| paint(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; |
| |
| 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)) return; |
| |
| paintRectangularSelection(g); |
| 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 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; |
| } |
| |
| private 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) { |
| if (myInputMethodRequestsHandler != null |
| && myInputMethodRequestsHandler.composedText != null |
| && myInputMethodRequestsHandler.composedTextRange != null) { |
| VisualPosition visStart = |
| offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextRange.getStartOffset(), myDocument.getTextLength())); |
| int y = visibleLineToY(visStart.line) + getAscent() + 1; |
| Point p1 = visualPositionToXY(visStart); |
| Point p2 = logicalPositionToXY( |
| offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.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); |
| } |
| } |
| |
| 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) { |
| drawWave(g, end.x, end.x + charWidth - 1, y); |
| } |
| 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) { |
| UIUtil.drawLine(g, end.x, y - 1, end.x + charWidth - 1, y - 1); |
| UIUtil.drawLine(g, end.x, y, end.x + charWidth - 1, y); |
| } |
| else if (attributes.getEffectType() != EffectType.BOXED) { |
| UIUtil.drawLine(g, end.x, y, end.x + charWidth - 1, y); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public int getMaxWidthInRange(int startOffset, int 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(); |
| g.setColor(defaultBackground); |
| g.fillRect(clip.x, clip.y, clip.width, clip.height); |
| |
| 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) { |
| position.x = drawBackground(g, myPrefixAttributes.getBackgroundColor(), prefixText, 0, prefixText.length(), position, |
| myPrefixAttributes.getFontType(), |
| defaultBackground, clip); |
| } |
| |
| if (clipStartPosition.line >= myDocument.getLineCount() || clipStartPosition.line < 0) { |
| if (position.x > 0) flushBackground(g, clip); |
| return; |
| } |
| |
| myLastBackgroundPosition = null; |
| myLastBackgroundColor = 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 = getColorsScheme().getColor(EditorColors.CARET_ROW_COLOR); |
| } |
| |
| if (color != null) { |
| drawBackground(g, color, softWrap.getIndentInPixels(), position, defaultBackground, clip); |
| } |
| 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, text, start, lEnd - lIterator.getSeparatorLength(), position, fontType, |
| defaultBackground, clip, softWrapsToSkip, caretRowPainted); |
| |
| 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, 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); |
| } |
| |
| lIterator.advance(); |
| } |
| else { |
| FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); |
| if (collapsedFolderAt != null) { |
| softWrap = mySoftWrapModel.getSoftWrap(collapsedFolderAt.getStartOffset()); |
| if (softWrap != null) { |
| position.x = drawSoftWrapAwareBackground( |
| g, backColor, 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); |
| } |
| else if (hEnd > lEnd - lIterator.getSeparatorLength()) { |
| position.x = drawSoftWrapAwareBackground( |
| g, backColor, text, start, lEnd - lIterator.getSeparatorLength(), position, fontType, |
| defaultBackground, clip, softWrapsToSkip, caretRowPainted |
| ); |
| } |
| else { |
| position.x = drawSoftWrapAwareBackground( |
| g, backColor, text, start, hEnd, position, fontType, defaultBackground, clip, softWrapsToSkip, caretRowPainted |
| ); |
| } |
| |
| 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 paintRectangularSelection(@NotNull Graphics g) { |
| final SelectionModel model = getSelectionModel(); |
| if (!model.hasBlockSelection()) return; |
| final LogicalPosition blockStart = model.getBlockStart(); |
| final LogicalPosition blockEnd = model.getBlockEnd(); |
| assert blockStart != null; |
| assert blockEnd != null; |
| |
| final Point start = logicalPositionToXY(blockStart); |
| final Point end = logicalPositionToXY(blockEnd); |
| g.setColor(myScheme.getColor(EditorColors.SELECTION_BACKGROUND_COLOR)); |
| final int y; |
| final int height; |
| if (start.y <= end.y) { |
| y = start.y; |
| height = end.y - y + getLineHeight(); |
| } |
| else { |
| y = end.y; |
| height = start.y - end.y + getLineHeight(); |
| } |
| final int x = Math.min(start.x, end.x); |
| final int width = Math.max(2, Math.abs(end.x - start.x)); |
| g.fillRect(x, y, width, height); |
| } |
| |
| 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, |
| Color backColor, |
| @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(myLastBackgroundColor, 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 = getColorsScheme().getColor(EditorColors.CARET_ROW_COLOR); |
| 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 = getColorsScheme().getColor(EditorColors.CARET_ROW_COLOR); |
| drawBackground(g, caretRowColor, width, position, defaultBackground, clip); |
| } |
| |
| position.x = 0; |
| paintSelectionOnSecondSoftWrapLineIfNecessary(g, position, clip, defaultBackground, fontType, softWrap); |
| position.x = width; |
| } |
| |
| /** |
| * 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 = getSelectionModel().getSelectionStartPosition(); |
| VisualPosition selectionEndPosition = getSelectionModel().getSelectionEndPosition(); |
| 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 = getSelectionModel().getSelectionStartPosition(); |
| VisualPosition selectionEndPosition = getSelectionModel().getSelectionEndPosition(); |
| 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; |
| final int plainSpaceWidth = EditorUtil.getSpaceWidth(Font.PLAIN, this); |
| final int boldSpaceWidth = EditorUtil.getSpaceWidth(Font.BOLD, this); |
| final int italicSpaceWidth = EditorUtil.getSpaceWidth(Font.ITALIC, this); |
| final int boldItalicSpaceWidth = EditorUtil.getSpaceWidth(Font.BOLD | Font.ITALIC, this); |
| |
| boolean spacesHaveSameWidth = |
| plainSpaceWidth == boldSpaceWidth && plainSpaceWidth == italicSpaceWidth && plainSpaceWidth == boldItalicSpaceWidth; |
| myCommonSpaceWidth = spacesHaveSameWidth ? boldSpaceWidth : -1; |
| |
| int lineHeight = getLineHeight(); |
| |
| int visibleLine = clip.y / lineHeight; |
| |
| // The main idea is that there is a possible case that we need to perform painting starting from soft-wrapped logical line. |
| // We may want to skip necessary number of visual lines then. Hence, we remember logical position that corresponds to the starting |
| // visual line in order to use it for further processing. As soon as necessary number of visual lines is skipped, logical |
| // position is expected to be set to null as an indication that no soft wrap-introduced visual lines should be skipped on |
| // current painting iteration. |
| Ref<LogicalPosition> logicalPosition = new Ref<LogicalPosition>(clipStartPosition); |
| 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(), logicalPosition); |
| } |
| 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(); |
| |
| while (!iterationState.atEnd() && !lIterator.atEnd()) { |
| int hEnd = iterationState.getEndOffset(); |
| int lEnd = lIterator.getEnd(); |
| if (hEnd >= lEnd) { |
| FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); |
| if (collapsedFolderAt == null) { |
| int i = drawStringWithSoftWraps(g, chars, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, |
| effectType, fontType, currentColor, logicalPosition); |
| final VirtualFile file = getVirtualFile(); |
| if (myProject != null && file != null && !isOneLineMode()) { |
| 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) { |
| drawStringWithSoftWraps(g, info.getText(), 0, info.getText().length(), position, clip, |
| info.getEffectColor() == null ? effectColor : info.getEffectColor(), |
| info.getEffectType() == null ? effectType : info.getEffectType(), |
| info.getFontType(), |
| info.getColor() == null ? currentColor : info.getColor(), |
| logicalPosition); |
| } |
| } |
| } |
| } |
| |
| position.x = 0; |
| if (position.y > clip.y + clip.height) { |
| break; |
| } |
| position.y += lineHeight; |
| start = lEnd; |
| } |
| |
| // myBorderEffect.eolReached(g, this); |
| lIterator.advance(); |
| } |
| 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, logicalPosition |
| ); |
| } |
| int foldingXStart = position.x; |
| position.x = drawString( |
| g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor |
| ); |
| //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, logicalPosition); |
| } |
| |
| 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, logicalPosition); |
| 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) { |
| // 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 { |
| myLastPaintedPlaceholderWidth = drawString( |
| g, hintText, 0, hintText.length(), new Point(0, 0), clip, null, null, Font.PLAIN, |
| myFoldingModel.getPlaceholderAttributes().getForegroundColor() |
| ); |
| 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]; |
| |
| 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]; |
| g.setColor(currentColor != null ? currentColor : JBColor.black); |
| } |
| |
| drawChars(g, data[i], starts[i], ends[i], x[i], y[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) { |
| 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)))) { |
| 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; |
| |
| myCount++; |
| if (count >= CACHED_CHARS_BUFFER_SIZE - 1) { |
| flushContent(g); |
| } |
| } |
| } |
| |
| 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); |
| } |
| |
| 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(); |
| if (separatorColor == 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); |
| } |
| |
| if (y < 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)); |
| } |
| |
| final LineSeparatorRenderer lineSeparatorRenderer = marker.getLineSeparatorRenderer(); |
| if (lineSeparatorRenderer != null) { |
| lineSeparatorRenderer.drawLine(g, 0, endShift, y - 1); |
| } |
| else { |
| UIUtil.drawLine(g, 0, y - 1, endShift, y - 1); |
| } |
| } |
| |
| 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, |
| @NotNull Ref<LogicalPosition> startDrawingLogicalPosition) { |
| return drawStringWithSoftWraps(g, text, 0, text.length(), position, clip, effectColor, effectType, |
| fontType, fontColor, startDrawingLogicalPosition); |
| } |
| |
| private int drawStringWithSoftWraps(@NotNull Graphics g, |
| final CharSequence text, |
| final int start, |
| final int end, |
| @NotNull Point position, |
| @NotNull Rectangle clip, |
| Color effectColor, |
| EffectType effectType, |
| @JdkConstants.FontStyle int fontType, |
| Color fontColor, |
| @NotNull Ref<LogicalPosition> startDrawingLogicalPosition) { |
| int startToUse = start; |
| |
| // 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 need to skip necessary number of visual lines then. |
| int softWrapLinesToSkip = 0; |
| if (startDrawingLogicalPosition.get() != null) { |
| softWrapLinesToSkip = startDrawingLogicalPosition.get().softWrapLinesOnCurrentLogicalLine; |
| } |
| SoftWrap lastSkippedSoftWrap = null; |
| if (softWrapLinesToSkip > 0) { |
| List<? extends SoftWrap> softWraps = getSoftWrapModel().getSoftWrapsForLine(startDrawingLogicalPosition.get().line); |
| for (SoftWrap softWrap : softWraps) { |
| softWrapLinesToSkip--; // Assuming that soft wrap has a single line feed all the time |
| if (softWrapLinesToSkip <= 0) { |
| lastSkippedSoftWrap = softWrap; |
| startToUse = softWrap.getStart(); |
| break; |
| } |
| } |
| } |
| startToUse = Math.max(startToUse, start); |
| |
| if (startToUse >= end && getSoftWrapModel().getSoftWrap(startToUse) == null) { |
| return position.x; |
| } |
| startDrawingLogicalPosition.set(null); |
| |
| // 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 (startToUse < end) { |
| softWrapRetrievalEndOffset--; |
| } |
| |
| outer: |
| for (SoftWrap softWrap : getSoftWrapModel().getSoftWrapsForRange(startToUse, softWrapRetrievalEndOffset)) { |
| char[] softWrapChars = softWrap.getChars(); |
| CharArrayCharSequence softWrapSeq = new CharArrayCharSequence(softWrapChars); |
| |
| if (softWrap.equals(lastSkippedSoftWrap)) { |
| // 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 |
| ); |
| } |
| position.x += mySoftWrapModel.paint(g, SoftWrapDrawingType.AFTER_SOFT_WRAP, position.x, position.y, getLineHeight()); |
| myForceRefreshFont = true; |
| continue; |
| } |
| |
| // Draw token text before the wrap. |
| if (softWrap.getStart() > startToUse) { |
| position.x = drawString( |
| g, text, startToUse, softWrap.getStart(), position, clip, null, null, fontType, fontColor |
| ); |
| } |
| |
| startToUse = 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 |
| ); |
| } |
| mySoftWrapModel.paint(g, SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED, position.x, position.y, getLineHeight()); |
| myForceRefreshFont = true; |
| |
| // 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 |
| ); |
| } |
| position.x += mySoftWrapModel.paint(g, SoftWrapDrawingType.AFTER_SOFT_WRAP, position.x, position.y, getLineHeight()); |
| myForceRefreshFont = true; |
| } |
| return position.x = drawString(g, text, startToUse, end, position, clip, effectColor, effectType, fontType, fontColor); |
| } |
| |
| 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) { |
| 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); |
| } |
| |
| public int 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) { |
| 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); |
| } |
| |
| 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) { |
| 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); |
| |
| int x1 = EditorUtil.nextTabStop(x, this); |
| drawTabPlacer(g, y, x, x1); |
| x = x1; |
| start = i + 1; |
| } |
| |
| x = drawTablessString(text, start, end, g, x, y, fontType, fontColor, clip); |
| |
| 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); |
| UIUtil.drawLine(g, xStart, y, xEnd, y); |
| UIUtil.drawLine(g, xStart, y + 1, xEnd, y + 1); |
| 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); |
| drawWave(g, xStart, xEnd, y + 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) { |
| int endX = x; |
| if (start < end) { |
| FontInfo font = EditorUtil.fontForChar(text.charAt(start), fontType, this); |
| for (int j = start; j < end; j++) { |
| final char c = text.charAt(j); |
| FontInfo newFont = EditorUtil.fontForChar(c, fontType, this); |
| if (font != newFont || endX > clip.x + clip.width) { |
| if (!(x < clip.x && endX < clip.x || x > clip.x + clip.width && endX > clip.x + clip.width)) { |
| drawCharsCached(g, text, start, j, x, y, fontType, fontColor); |
| } |
| start = j; |
| x = endX; |
| font = newFont; |
| } |
| if (x < clip.x && endX < clip.x) { |
| start = j; |
| x = endX; |
| font = newFont; |
| } |
| else if (x > clip.x + clip.width) { |
| return endX; |
| } |
| |
| // We experienced the following situation: |
| // * the editor was configured to use monospaced font; |
| // * the document contained either english or russian symbols; |
| // * different fonts were used to display english and russian symbols; |
| // * the fonts mentioned above have different space width; |
| // So, the problem was when white space followed russian word - the white space width was calculated using the english font |
| // but drawn using the russian font, so, there was a visual inconsistency at the editor. |
| final int charWidth = font.charWidth(c); |
| if (c == ' ' |
| && myCommonSpaceWidth > 0 |
| && myLastCache != null |
| && (charWidth != myCommonSpaceWidth || charWidth != myLastCache.spaceWidth)) { |
| myForceRefreshFont = true; |
| } |
| endX += charWidth; |
| |
| if (newFont.hasGlyphsToBreakDrawingIteration() && newFont.getSymbolsToBreakDrawingIteration().contains(c)) { |
| drawCharsCached(g, text, start, j + 1, x, y, fontType, fontColor); |
| x = endX; |
| start = j + 1; |
| } |
| } |
| |
| if (!(x < clip.x && endX < clip.x || x > clip.x + clip.width && endX > clip.x + clip.width)) { |
| drawCharsCached(g, text, start, end, x, y, fontType, fontColor); |
| } |
| } |
| |
| return endX; |
| } |
| |
| private void drawTabPlacer(Graphics g, int y, int start, int stop) { |
| if (mySettings.isWhitespacesShown()) { |
| 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) { |
| if (!myForceRefreshFont && myCommonSpaceWidth > 0 && myLastCache != null && spacesOnly(data, start, end)) { |
| myLastCache.addContent(g, data, start, end, x, y, null); |
| } |
| else { |
| myForceRefreshFont = false; |
| FontInfo fnt = EditorUtil.fontForChar(data.charAt(start), fontType, this); |
| drawCharsCached(g, data, start, end, x, y, fnt, color); |
| } |
| } |
| |
| private void drawCharsCached(@NotNull Graphics g, |
| @NotNull CharSequence data, |
| int start, |
| int end, |
| int x, |
| int y, |
| @NotNull FontInfo fnt, |
| Color color) { |
| 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); |
| } |
| |
| 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 static final char IDEOGRAPHIC_SPACE = '\u3000'; // http://www.marathon-studios.com/unicode/U3000/Ideographic_Space |
| |
| private void drawChars(@NotNull Graphics g, CharSequence data, int start, int end, int x, int y) { |
| g.drawString(data.subSequence(start, end).toString(), x, y); |
| |
| if (mySettings.isWhitespacesShown()) { |
| 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 static final int WAVE_HEIGHT = 2; |
| private static final int WAVE_SEGMENT_LENGTH = 4; |
| |
| private static void drawWave(Graphics g, int xStart, int xEnd, int y) { |
| int startSegment = xStart / WAVE_SEGMENT_LENGTH; |
| int endSegment = xEnd / WAVE_SEGMENT_LENGTH; |
| for (int i = startSegment; i < endSegment; i++) { |
| drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); |
| } |
| |
| int x = WAVE_SEGMENT_LENGTH * endSegment; |
| UIUtil.drawLine(g, x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); |
| } |
| |
| private static void drawWaveSegment(Graphics g, int x, int y) { |
| UIUtil.drawLine(g, x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); |
| UIUtil.drawLine(g, x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); |
| } |
| |
| 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() { |
| 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 (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; |
| } |
| |
| private int 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; |
| } |
| |
| return getHeightWithoutCaret(); |
| } |
| |
| public Dimension getPreferredSize() { |
| if (ourIsUnitTestMode && getUserData(DO_DOCUMENT_UPDATE_TEST) == null) { |
| return new Dimension(1, 1); |
| } |
| |
| final Dimension draft = getSizeWithoutCaret(); |
| final int additionalSpace = mySoftWrapModel.isRespectAdditionalColumns() |
| ? 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 Dimension getSizeWithoutCaret() { |
| Dimension size = mySizeContainer.getContentSize(); |
| if (isOneLineMode()) return new Dimension(size.width, getLineHeight()); |
| if (mySettings.isAdditionalPageAtBottom()) { |
| int lineHeight = getLineHeight(); |
| int visibleAreaHeight = getScrollingModel().getVisibleArea().height; |
| int virtualPageHeight; |
| // 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) { |
| virtualPageHeight = myVirtualPageHeight; |
| } |
| else { |
| myVirtualPageHeight = virtualPageHeight = Math.max(visibleAreaHeight - 2 * lineHeight, lineHeight); |
| } |
| |
| if (myVirtualPageHeight > 0) { |
| return new Dimension(size.width, size.height + virtualPageHeight); |
| } |
| return size; |
| } |
| |
| return getContentSize(); |
| } |
| |
| private int getHeightWithoutCaret() { |
| if (isOneLineMode()) return getLineHeight(); |
| int size = mySizeContainer.getContentHeight(); |
| if (mySettings.isAdditionalPageAtBottom()) { |
| int lineHeight = getLineHeight(); |
| return size + Math.max(getScrollingModel().getVisibleArea().height - 2 * lineHeight, lineHeight); |
| } |
| |
| return size + mySettings.getAdditionalLinesCount() * getLineHeight(); |
| } |
| |
| @NotNull |
| @Override |
| public Dimension getContentSize() { |
| 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, null); |
| } |
| |
| |
| public int logicalPositionToOffset(@NotNull LogicalPosition pos, @Nullable StringBuilder debugBuffer) { |
| 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); |
| |
| CharSequence text = myDocument.getImmutableCharSequence(); |
| |
| return EditorUtil.calcOffset(this, text, start, end, pos.column, EditorUtil.getTabSize(this), debugBuffer); |
| } |
| |
| @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) { |
| 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.getLastTopLevelIndexBefore(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) { |
| 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(), false); |
| 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) { |
| return offsetToLogicalLine(offset, true); |
| } |
| |
| private int offsetToLogicalLine(int offset, boolean softWrapAware) { |
| 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()); |
| |
| if (softWrapAware && getSoftWrapModel().isSoftWrappingEnabled()) { |
| int column = calcColumnNumber(offset, lineIndex, false, myDocument.getImmutableCharSequence()); |
| return mySoftWrapModel.adjustLogicalPosition(new LogicalPosition(lineIndex, column), offset).line; |
| } |
| else { |
| 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 (myDocument.getTextLength() == 0) return 0; |
| |
| int lineStartOffset = myDocument.getLineStartOffset(lineIndex); |
| if (lineStartOffset == offset) return 0; |
| int column = EditorUtil.calcColumnNumber(this, documentCharSequence, lineStartOffset, offset); |
| |
| if (softWrapAware) { |
| int line = offsetToLogicalLine(offset, false); |
| 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; |
| |
| if (line < 0) { |
| line = 0; |
| column = 0; |
| softWrapLinesBeforeTargetLogicalLine = 0; |
| softWrapLinesOnTargetLogicalLine = 0; |
| softWrapColumns = 0; |
| } |
| |
| 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() && !mySelectionModel.hasBlockSelection() && trimToLineWidth) { |
| int lineEndOffset = myDocument.getLineEndOffset(line); |
| int lineEndColumn = calcColumnNumber(lineEndOffset, line); |
| if (column > lineEndColumn) { |
| column = lineEndColumn; |
| 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 new LogicalPosition( |
| line, column, softWrapLinesBeforeTargetLogicalLine, softWrapLinesOnTargetLogicalLine, softWrapColumns, |
| pos.foldedLines, pos.foldingColumnDiff |
| ); |
| } |
| |
| 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()) { |
| 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) { |
| FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); |
| myGutterComponent.setActiveFoldRegion(foldingAtCursor); |
| if (foldingAtCursor != null) { |
| myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); |
| } |
| else { |
| myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); |
| } |
| } |
| 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; |
| } |
| |
| 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(); |
| LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); |
| boolean multiCaretSelection = myCaretModel.supportsMultipleCarets() && (isColumnMode() || e.isAltDown()); |
| LogicalPosition newLogicalCaret = getLogicalPositionForScreenPos(x, y, !multiCaretSelection); |
| if (multiCaretSelection) { |
| myMultiSelectionInProgress = true; |
| myTargetMultiSelectionPosition = xyToVisualPosition(new Point(Math.max(x, 0), Math.max(y, 0))); |
| getScrollingModel().scrollTo(newLogicalCaret, ScrollType.RELATIVE); |
| } |
| else { |
| getCaretModel().moveToLogicalPosition(newLogicalCaret); |
| getScrollingModel().scrollToCaret(ScrollType.RELATIVE); |
| } |
| |
| 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 (isColumnMode() || e.isAltDown()) { |
| if (myCaretModel.supportsMultipleCarets()) { |
| if (myLastMousePressedLocation != null && (myCurrentDragIsSubstantial || !newLogicalCaret.equals(myLastMousePressedLocation))) { |
| setBlockSelectionAndBlockActions(e, myLastMousePressedLocation, newLogicalCaret); |
| } |
| } else { |
| final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; |
| if (blockStart != null) { |
| setBlockSelectionAndBlockActions(e, blockStart, getCaretModel().getLogicalPosition()); |
| } |
| } |
| } |
| else { |
| if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { |
| if (caretShift < 0) { |
| int newSelection = newCaretOffset; |
| if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { |
| newSelection = myCaretModel.getWordAtCaretStart(); |
| } |
| else { |
| if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { |
| newSelection = |
| logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0))); |
| } |
| } |
| if (newSelection < 0) newSelection = newCaretOffset; |
| selectionModel.setSelection(mySavedSelectionEnd, newSelection); |
| getCaretModel().moveToOffset(newSelection); |
| } |
| else { |
| int newSelection = newCaretOffset; |
| if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { |
| newSelection = myCaretModel.getWordAtCaretEnd(); |
| } |
| else { |
| if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { |
| newSelection = |
| logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0))); |
| } |
| } |
| if (newSelection < 0) newSelection = newCaretOffset; |
| selectionModel.setSelection(mySavedSelectionStart, newSelection); |
| getCaretModel().moveToOffset(newSelection); |
| } |
| cancelAutoResetForMouseSelectionState(); |
| return; |
| } |
| |
| if (!myMousePressedInsideSelection) { |
| // There is a possible case that lead selection position should be adjusted in accordance with the mouse move direction. |
| // E.g. consider situation when user selects the whole line by clicking at 'line numbers' area. 'Line end' is considered |
| // to be lead selection point then. However, when mouse is dragged down we want to consider 'line start' to be |
| // lead selection point. |
| if ((myMousePressArea == EditorMouseEventArea.LINE_NUMBERS_AREA |
| || myMousePressArea == EditorMouseEventArea.LINE_MARKERS_AREA) |
| && selectionModel.hasSelection()) { |
| if (newCaretOffset >= selectionModel.getSelectionEnd()) { |
| oldSelectionStart = selectionModel.getSelectionStart(); |
| oldVisLeadSelectionStart = selectionModel.getSelectionStartPosition(); |
| } |
| else if (newCaretOffset <= selectionModel.getSelectionStart()) { |
| oldSelectionStart = selectionModel.getSelectionEnd(); |
| oldVisLeadSelectionStart = selectionModel.getSelectionEndPosition(); |
| } |
| } |
| if (oldVisLeadSelectionStart != null) { |
| setSelectionAndBlockActions(e, oldVisLeadSelectionStart, oldSelectionStart, newVisualCaret, newCaretOffset); |
| } |
| else { |
| setSelectionAndBlockActions(e, oldSelectionStart, newCaretOffset); |
| } |
| cancelAutoResetForMouseSelectionState(); |
| } |
| else { |
| if (caretShift != 0) { |
| if (myMousePressedEvent != null) { |
| if (mySettings.isDndEnabled()) { |
| boolean isCopy = UIUtil.isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); |
| mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; |
| getContentComponent().getTransferHandler() |
| .exportAsDrag(getContentComponent(), e, isCopy ? TransferHandler.COPY : TransferHandler.MOVE); |
| } |
| else { |
| selectionModel.removeSelection(); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| else { |
| myScrollingTimer.start(dx, dy); |
| onSubstantialDrag(e); |
| } |
| } |
| |
| private 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 setBlockSelectionAndBlockActions(@NotNull MouseEvent mouseDragEvent, @NotNull LogicalPosition startPosition, @NotNull LogicalPosition endPosition) { |
| mySelectionModel.setBlockSelection(startPosition, 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() { |
| if (myCaretModel.supportsMultipleCarets()) { |
| final List<CaretRectangle> caretPoints = new ArrayList<CaretRectangle>(); |
| for (Caret caret : getCaretModel().getAllCarets()) { |
| VisualPosition caretPosition = caret.getVisualPosition(); |
| Point pos1 = visualPositionToXY(caretPosition); |
| Point pos2 = visualPositionToXY(new VisualPosition(caretPosition.line, caretPosition.column + 1)); |
| caretPoints.add(new CaretRectangle(pos1, pos2.x - pos1.x, caret)); |
| } |
| myCaretCursor.setPositions(caretPoints.toArray(new CaretRectangle[caretPoints.size()])); |
| } |
| else { |
| VisualPosition caretPosition = getCaretModel().getVisualPosition(); |
| Point pos1 = visualPositionToXY(caretPosition); |
| Point pos2 = visualPositionToXY(new VisualPosition(caretPosition.line, caretPosition.column + 1)); |
| myCaretCursor.setPosition(pos1, pos2.x - pos1.x); |
| } |
| } |
| |
| @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(); |
| } |
| |
| @Override |
| public void stopOptimizedScrolling() { |
| } |
| |
| private static class CaretRectangle { |
| private final Point myPoint; |
| private final int myWidth; |
| private final Caret myCaret; |
| |
| private CaretRectangle(Point point, int width, Caret caret) { |
| myPoint = point; |
| myWidth = Math.max(width, 2); |
| myCaret = caret; |
| } |
| } |
| |
| private class CaretCursor { |
| private Point myLocation; |
| private int myWidth; |
| private CaretRectangle[] myLocations; |
| private boolean myEnabled; |
| |
| @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) |
| private boolean myIsShown = false; |
| private long myStartTime = 0; |
| |
| private CaretCursor() { |
| myLocation = new Point(0, 0); |
| myLocations = new CaretRectangle[] {new CaretRectangle(new Point(0, 0), 0, null)}; |
| 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 setPosition(Point location, int width) { |
| myStartTime = System.currentTimeMillis(); |
| myLocation = location; |
| myWidth = Math.max(width, 2); |
| myIsShown = true; |
| repaint(); |
| } |
| |
| private void setPositions(CaretRectangle[] locations) { |
| myStartTime = System.currentTimeMillis(); |
| myLocations = locations; |
| myIsShown = true; |
| repaint(); |
| } |
| |
| private void repaint() { |
| if (myCaretModel.supportsMultipleCarets()) { |
| for (CaretRectangle location : myLocations) { |
| myEditorComponent.repaintEditorComponent(location.myPoint.x, location.myPoint.y, location.myWidth, getLineHeight()); |
| } |
| } |
| else { |
| myEditorComponent.repaintEditorComponent(myLocation.x, myLocation.y, myWidth, getLineHeight()); |
| } |
| } |
| |
| private void paint(@NotNull Graphics g) { |
| if (!isEnabled() || !myIsShown || !IJSwingUtilities.hasFocus(getContentComponent()) || isRendererMode()) return; |
| |
| if (myCaretModel.supportsMultipleCarets()) { |
| for (CaretRectangle location : myLocations) { |
| paintAt(g, location.myPoint.x, location.myPoint.y, location.myWidth, location.myCaret); |
| } |
| } |
| else { |
| paintAt(g, myLocation.x, myLocation.y, myWidth, null); |
| } |
| } |
| |
| 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)); |
| |
| if (!paintBlockCaret()) { |
| if (UIUtil.isRetina()) { |
| g.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); |
| g.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(CURSOR_FOREGROUND); |
| 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(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); |
| mySelectionModel.setBlockSelection(myLastMousePressedLocation, newLogicalPosition); |
| } |
| else if (mySelectionModel.hasBlockSelection()) { |
| mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); |
| } |
| 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; |
| @Nullable private RepaintCallback myRepaintCallback; |
| |
| 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); |
| } |
| |
| @Override |
| public void paint(Graphics g) { |
| super.paint(g); |
| if (myRepaintCallback != null) { |
| myRepaintCallback.call(g); |
| } |
| } |
| |
| /** |
| * 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 RepaintCallback callback) { |
| myRepaintCallback = callback; |
| } |
| } |
| |
| private MyEditable getViewer() { |
| if (myEditable == null) { |
| myEditable = new MyEditable(); |
| } |
| return myEditable; |
| } |
| |
| @Override |
| public CopyProvider getCopyProvider() { |
| return getViewer(); |
| } |
| |
| @Override |
| public CutProvider getCutProvider() { |
| return getViewer(); |
| } |
| |
| @Override |
| public PasteProvider getPasteProvider() { |
| |
| return getViewer(); |
| } |
| |
| @Override |
| public DeleteProvider getDeleteProvider() { |
| return getViewer(); |
| } |
| |
| private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { |
| @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) || getSelectionModel().hasBlockSelection(); |
| } |
| |
| @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) || getSelectionModel().hasBlockSelection(); |
| } |
| |
| @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; |
| } |
| |
| void assertIsDispatchThread() { |
| ApplicationManager.getApplication().assertIsDispatchThread(); |
| } |
| |
| private static void assertReadAccess() { |
| ApplicationManager.getApplication().assertReadAccessAllowed(); |
| } |
| |
| @Override |
| public void setVerticalScrollbarOrientation(int type) { |
| assertIsDispatchThread(); |
| int currentHorOffset = myScrollingModel.getHorizontalScrollOffset(); |
| myScrollBarOrientation = type; |
| if (type == VERTICAL_SCROLLBAR_LEFT) { |
| myScrollPane.setLayout(new LeftHandScrollbarLayout()); |
| } |
| else { |
| myScrollPane.setLayout(new ScrollPaneLayout()); |
| } |
| myScrollingModel.scrollHorizontally(currentHorOffset); |
| } |
| |
| @Override |
| public void setVerticalScrollbarVisible(boolean b) { |
| myScrollPane |
| .setVerticalScrollBarPolicy(b ? ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS : ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); |
| } |
| |
| @Override |
| public void setHorizontalScrollbarVisible(boolean b) { |
| myScrollPane.setHorizontalScrollBarPolicy( |
| b ? ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED : ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); |
| } |
| |
| @Override |
| public int getVerticalScrollbarOrientation() { |
| return myScrollBarOrientation; |
| } |
| |
| public boolean isMirrored() { |
| return myScrollBarOrientation != EditorEx.VERTICAL_SCROLLBAR_RIGHT; |
| } |
| |
| @NotNull |
| MyScrollBar getVerticalScrollBar() { |
| return myVerticalScrollBar; |
| } |
| |
| 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; |
| } |
| |
| @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(); |
| } |
| }); |
| } |
| |
| @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 ""; |
| } |
| |
| @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) { |
| 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) { |
| 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()); |
| myCurrentDragIsSubstantial = false; |
| |
| final int clickOffset = logicalPositionToOffset(myLastMousePressedLocation); |
| putUserData(EditorActionUtil.EXPECTED_CARET_OFFSET, clickOffset); |
| |
| mySelectionTweaked = false; |
| myMousePressedEvent = e; |
| EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); |
| |
| for (EditorMouseListener mouseListener : myMouseListeners) { |
| mouseListener.mousePressed(event); |
| } |
| putUserData(EditorActionUtil.EXPECTED_CARET_OFFSET, null); |
| |
| 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 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; |
| boolean isNavigation = false; |
| 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); |
| } |
| }; |
| getFoldingModel().runBatchFoldingOperation(processor); |
| y = myGutterComponent.getHeadCenterY(range); |
| getScrollingModel().scrollVertically(y - scrollShift); |
| myGutterComponent.updateSize(); |
| validateMousePointer(e); |
| e.consume(); |
| return isNavigation; |
| } |
| } |
| |
| 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 isNavigation; |
| } |
| x = 0; |
| } |
| |
| int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); |
| |
| final int oldStart = mySelectionModel.getSelectionStart(); |
| final int oldEnd = mySelectionModel.getSelectionEnd(); |
| |
| boolean toggleCaret = myCaretModel.supportsMultipleCarets() && 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 { |
| getCaretModel().removeSecondaryCarets(); |
| getCaretModel().moveToLogicalPosition(pos); |
| } |
| } |
| |
| if (e.isPopupTrigger()) return isNavigation; |
| |
| requestFocus(); |
| |
| int caretOffset = getCaretModel().getOffset(); |
| |
| int newStart = mySelectionModel.getSelectionStart(); |
| int newEnd = mySelectionModel.getSelectionEnd(); |
| if (oldStart != newStart && oldEnd != newEnd) { |
| if (oldStart == oldEnd && newStart == newEnd) { |
| isNavigation = true; |
| } |
| } |
| |
| myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); |
| myMousePressedInsideSelection = mySelectionModel.hasSelection() && caretOffset >= mySelectionModel.getSelectionStart() && |
| caretOffset <= mySelectionModel.getSelectionEnd(); |
| |
| if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { |
| int[] starts = mySelectionModel.getBlockSelectionStarts(); |
| int[] ends = mySelectionModel.getBlockSelectionEnds(); |
| for (int i = 0; i < starts.length; i++) { |
| if (caretOffset >= starts[i] && caretOffset < ends[i]) { |
| myMousePressedInsideSelection = true; |
| break; |
| } |
| } |
| } |
| |
| 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() || getSelectionModel().hasBlockSelection())) { |
| 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 isToggleCaretEvent(MouseEvent e) { |
| KeymapManager keymapManager = KeymapManager.getInstance(); |
| if (keymapManager == null) { |
| return false; |
| } |
| Keymap keymap = keymapManager.getActiveKeymap(); |
| if (keymap == null) { |
| return false; |
| } |
| String[] actionIds = keymap.getActionIds(new MouseShortcut(e.getButton(), e.getModifiersEx(), 1)); |
| if (actionIds == null) { |
| return false; |
| } |
| for (String actionId : actionIds) { |
| if (IdeActions.ACTION_EDITOR_ADD_OR_REMOVE_CARET.equals(actionId)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| 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 EditorColorsScheme getGlobal() { |
| return getDelegate(); |
| } |
| |
| protected void initFonts() { |
| String editorFontName = getEditorFontName(); |
| int editorFontSize = getEditorFontSize(); |
| |
| myFontPreferences.clear(); |
| myFontPreferences.register(editorFontName, editorFontSize); |
| |
| 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); |
| |
| reinitSettings(); |
| } |
| |
| @Override |
| public TextAttributes getAttributes(TextAttributesKey key) { |
| if (myOwnAttributes.containsKey(key)) return myOwnAttributes.get(key); |
| return getGlobal().getAttributes(key); |
| } |
| |
| @Override |
| public void setAttributes(TextAttributesKey key, TextAttributes attributes) { |
| myOwnAttributes.put(key, attributes); |
| } |
| |
| @Override |
| public Color getColor(ColorKey key) { |
| if (myOwnColors.containsKey(key)) return myOwnColors.get(key); |
| return getGlobal().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 getGlobal().getEditorFontSize(); |
| } |
| return myFontSize; |
| } |
| |
| @Override |
| public void setEditorFontSize(int fontSize) { |
| if (fontSize < MIN_FONT_SIZE) fontSize = MIN_FONT_SIZE; |
| if (fontSize > myMaxFontSize) fontSize = myMaxFontSize; |
| myFontSize = fontSize; |
| initFonts(); |
| } |
| |
| @NotNull |
| @Override |
| public FontPreferences getFontPreferences() { |
| return myFontPreferences.getEffectiveFontFamilies().isEmpty() ? getGlobal().getFontPreferences() : myFontPreferences; |
| } |
| |
| @Override |
| public void setFontPreferences(@NotNull FontPreferences preferences) { |
| preferences.copyTo(myFontPreferences); |
| initFonts(); |
| } |
| |
| @Override |
| public String getEditorFontName() { |
| if (myFaceName == null) { |
| return getGlobal().getEditorFontName(); |
| } |
| return myFaceName; |
| } |
| |
| @Override |
| public void setEditorFontName(String fontName) { |
| myFaceName = fontName; |
| initFonts(); |
| } |
| |
| @Override |
| public Font getFont(EditorFontType key) { |
| if (myFontsMap != null) { |
| Font font = myFontsMap.get(key); |
| if (font != null) return font; |
| } |
| return getGlobal().getFont(key); |
| } |
| |
| @Override |
| public void setFont(EditorFontType key, Font font) { |
| if (myFontsMap == null) { |
| initFonts(); |
| } |
| 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 = getGlobal().getEditorFontSize(); |
| myMaxFontSize = Math.max(OptionsConstants.MAX_EDITOR_FONT_SIZE, globalFontSize); |
| } |
| |
| @Override |
| public void setConsoleFontSize(int fontSize) { |
| getGlobal().setConsoleFontSize(fontSize); |
| reinitSettings(); |
| } |
| } |
| |
| private static class MyTransferHandler extends TransferHandler { |
| private RangeMarker myDraggedRange = null; |
| |
| private static Editor getEditor(@NotNull JComponent comp) { |
| EditorComponentImpl editorComponent = (EditorComponentImpl)comp; |
| return editorComponent.getEditor(); |
| } |
| |
| @Override |
| public boolean importData(final JComponent comp, @NotNull final Transferable t) { |
| final EditorImpl editor = (EditorImpl)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 (myDraggedRange != null && myDraggedRange.getStartOffset() <= caretOffset && caretOffset < myDraggedRange.getEndOffset()) { |
| return false; |
| } |
| |
| if (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 (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(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) { |
| Editor editor = getEditor(c); |
| String s = editor.getSelectionModel().getSelectedText(); |
| if (s == null) return null; |
| int selectionStart = editor.getSelectionModel().getSelectionStart(); |
| int selectionEnd = editor.getSelectionModel().getSelectionEnd(); |
| myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); |
| |
| return new StringSelection(s); |
| } |
| |
| @Override |
| public int getSourceActions(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 Editor editor = getEditor(source); |
| if (action == MOVE && !editor.isViewer() && myDraggedRange != null) { |
| if (!FileDocumentManager.getInstance().requestWriting(editor.getDocument(), editor.getProject())) { |
| return; |
| } |
| CommandProcessor.getInstance().executeCommand(((EditorImpl)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(myDraggedRange.getStartOffset(), 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()); |
| } |
| |
| myDraggedRange = null; |
| } |
| } |
| |
| 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) 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 mySize; |
| } |
| |
| private int getContentHeight() { |
| return getVisibleLineCount() * getLineHeight(); |
| } |
| } |
| |
| @Override |
| @NotNull |
| public EditorGutter getGutter() { |
| return getGutterComponentEx(); |
| } |
| |
| @Override |
| public int calcColumnNumber(@NotNull CharSequence text, int start, int offset, int tabSize) { |
| 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().PRESENTATION_MODE && 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() { |
| setViewportBorder(new EmptyBorder(0, 0, 0, 0)); |
| } |
| |
| @Override |
| public void layout() { |
| super.layout(); |
| if (isInDistractionFreeMode()) { |
| // re-calc gutter extra size after editor size is set |
| // & layout once again to avoid blinking |
| myGutterComponent.updateSize(); |
| 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 |
| protected void setupCorners() { |
| super.setupCorners(); |
| |
| setBorder(new TablessBorder()); |
| |
| setCorner(getVerticalScrollbarOrientation() == EditorEx.VERTICAL_SCROLLBAR_LEFT ? |
| LOWER_RIGHT_CORNER : |
| LOWER_LEFT_CORNER, new JPanel() { |
| @Override |
| public void paint(@NotNull Graphics g) { |
| if (UISettings.getInstance().PRESENTATION_MODE) { |
| return; |
| } |
| |
| final Rectangle bounds = getBounds(); |
| int width = bounds.width; |
| int height = bounds.height; |
| |
| g.setColor(ButtonlessScrollBarUI.getTrackBackground()); |
| g.fillRect(0, 0, width, height); |
| |
| int shortner = 0; |
| if (myGutterComponent.isFoldingOutlineShown()) { |
| shortner = myGutterComponent.getFoldingAreaWidth() / 2; |
| } |
| |
| g.setColor(myGutterComponent.getBackground()); |
| g.fillRect(0, 0, width - shortner, height); |
| |
| g.setColor(ButtonlessScrollBarUI.getTrackBorderColor()); |
| g.drawLine(width - 1 - shortner, 0, width - 1 - shortner, height); |
| g.drawLine(0, 0, width - 1, 0); |
| } |
| }); |
| } |
| } |
| |
| 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.drawLine(x, y, x + width, y); |
| g.setColor(Gray._0.withAlpha(90)); |
| g.drawLine(x, y, x + width, y); |
| } |
| } |
| } |
| |
| @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(); |
| return splitters == null ? super.getBorderInsets(c) : new Insets(thereIsSomethingAbove ? 1 : 0, 0, 0, 0); |
| } |
| |
| public boolean toolWindowIsNotEmpty() { |
| if (myProject == null) return false; |
| ToolWindowManagerEx m = ToolWindowManagerEx.getInstanceEx(myProject); |
| if (m == null) return false; |
| return !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); |
| } |
| } |
| } |