diff options
author | Christoph Caks | 2016-01-19 11:09:13 +0000 |
---|---|---|
committer | Christoph Caks | 2016-01-19 11:09:13 +0000 |
commit | 52ff6dabf0d8d0095e381ec85b125b39a176b907 (patch) | |
tree | 566b7fac48b1ce6402ed0dc076e427e3307cdda0 | |
parent | 316c969cfc1c772b16dea884e34f220c9a13b821 (diff) | |
parent | 265a4de7cf0be24caed1f8010fb4eadbd9dfd4d4 (diff) | |
download | org.eclipse.efxclipse-52ff6dabf0d8d0095e381ec85b125b39a176b907.tar.gz org.eclipse.efxclipse-52ff6dabf0d8d0095e381ec85b125b39a176b907.tar.xz org.eclipse.efxclipse-52ff6dabf0d8d0095e381ec85b125b39a176b907.zip |
Merge remote-tracking branch 'origin/codeeditor'
4 files changed, 1481 insertions, 302 deletions
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/StyledTextArea.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/StyledTextArea.java index 81385f997..28b6d4d4e 100644 --- a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/StyledTextArea.java +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/StyledTextArea.java @@ -6,6 +6,7 @@ * http://www.eclipse.org/legal/epl-v10.html * * Contributors: + * Christoph Caks <ccaks@bestsolution.at> - improved editor behavior * Tom Schindl<tom.schindl@bestsolution.at> - initial API and implementation * IBM Corporation - initial API and implementation *******************************************************************************/ @@ -71,6 +72,12 @@ public class StyledTextArea extends Control { @NonNull final ObjectProperty<@NonNull StyledTextContent> contentProperty; +// @NonNull +// final SetProperty<@NonNull StyledTextAnnotation> annotationsProperty = new SimpleSetProperty<>(this, "annotations", FXCollections.observableSet()); //$NON-NLS-1$ +// public SetProperty<@NonNull StyledTextAnnotation> getAnnotations() { +// return this.annotationsProperty; +// } + TextChangeListener textChangeListener = new TextChangeListener() { @Override public void textChanging(TextChangingEvent event) { @@ -229,7 +236,11 @@ public class StyledTextArea extends Control { ((StyledTextSkin) getSkin()).recalculateItems(); } - updateSelection(this.lastTextChangeStart, this.lastTextChangeReplaceCharCount, this.lastTextChangeNewCharCount); + // TODO We need to re enable this in the future. + // For each change coming from outside (for example refactoring) + // we need to update the caret + +// updateSelection(this.lastTextChangeStart, this.lastTextChangeReplaceCharCount, this.lastTextChangeNewCharCount); // lastCharCount += lastTextChangeNewCharCount; // lastCharCount -= lastTextChangeReplaceCharCount; @@ -241,7 +252,12 @@ public class StyledTextArea extends Control { setSelection(new TextSelection(startOffset + newLength, 0)/*, true, false*/); } else { // move selection to keep same text selected - setSelection(new TextSelection(getSelection().offset + newLength - replacedLength, getSelection().length)/*, true, false*/); + + int computedOffset = getSelection().offset + newLength - replacedLength; + if (computedOffset >= 0 && computedOffset < getCharCount()) { + // we only set this if the offset is valid!! + setSelection(new TextSelection(computedOffset, getSelection().length)/*, true, false*/); + } if( getSelection().length > 0 ) { int delta = this.lastTextChangeNewCharCount - this.lastTextChangeReplaceCharCount; this.caretOffsetProperty.set(Math.max(0,Math.min(getCharCount()-1,getCaretOffset() + delta))); @@ -1421,6 +1437,34 @@ public class StyledTextArea extends Control { return getContent().getTextRange(start, end - start + 1); } + public boolean isSelectionEmpty() { + return getSelection().length == 0; + } + + /** + * inserts text at the caret location or replaces a given selection + * @param text + */ + public void insert(CharSequence text) { + System.err.println("insert('"+text+"')"); + if (text == null) throw new NullPointerException(); + + int start, replaceLength; + if (!isSelectionEmpty()) { + // replace selection + start = this.getSelection().offset; + replaceLength = this.getSelection().length; + } + else { + // insert at caret + start = this.getCaretOffset(); + replaceLength = 0; + } + this.getContent().replaceTextRange(start, replaceLength, text.toString()); + this.setCaretOffset(start + text.length()); + } + + /** * Paste the clipboard content */ @@ -1429,8 +1473,7 @@ public class StyledTextArea extends Control { if (clipboard.hasString()) { final String text = clipboard.getString(); if (text != null) { - // TODO Once we have a real selection we need - getContent().replaceTextRange(getCaretOffset(), 0, text); + insert(text); } } } diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/StyledTextLayoutContainer.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/StyledTextLayoutContainer.java index 76c7bb4c9..dd9c78be1 100644 --- a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/StyledTextLayoutContainer.java +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/StyledTextLayoutContainer.java @@ -7,6 +7,7 @@ *
* Contributors:
* Tom Schindl<tom.schindl@bestsolution.at> - initial API and implementation
+ * Christoph Caks <ccaks@bestsolution.at> - improved editor behavior
*******************************************************************************/
package org.eclipse.fx.ui.controls.styledtext;
@@ -14,8 +15,7 @@ import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable;
import javafx.animation.Animation;
-import javafx.animation.KeyFrame;
-import javafx.animation.Timeline;
+import javafx.animation.FadeTransition;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
@@ -26,9 +26,8 @@ import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
-import javafx.event.ActionEvent;
-import javafx.event.EventHandler;
import javafx.geometry.Point2D;
+import javafx.scene.Node;
import javafx.scene.layout.Region;
import javafx.scene.shape.Line;
import javafx.scene.text.TextFlow;
@@ -45,8 +44,20 @@ public class StyledTextLayoutContainer extends Region { private final ObservableList<@NonNull StyledTextNode> textNodes = FXCollections.observableArrayList();
@NonNull
+ private final IntegerProperty caretIndex = new SimpleIntegerProperty(this, "caretIndex"); //$NON-NLS-1$
+
+ @NonNull
private final IntegerProperty startOffset = new SimpleIntegerProperty(this, "startOffset"); //$NON-NLS-1$
+
+// @NonNull
+// final SetProperty<@NonNull StyledTextAnnotation> annotationsProperty = new SimpleSetProperty<>(this, "annotations", FXCollections.observableSet()); //$NON-NLS-1$
+// public SetProperty<@NonNull StyledTextAnnotation> getAnnotations() {
+// return this.annotationsProperty;
+// }
+//
+// private Map<StyledTextAnnotation, Rectangle> annotationMarkers = new HashMap<>();
+
/**
* The start offset if used in a bigger context like {@link StyledTextArea}
*
@@ -57,6 +68,37 @@ public class StyledTextLayoutContainer extends Region { }
/**
+ * the the caret index property.
+ *
+ * <p>the index or <code>-1</code> if caret is to be hidden</p>
+ *
+ * @return the caret index property
+ */
+ public final IntegerProperty caretIndexProperty() {
+ return this.caretIndex;
+ }
+
+ /**
+ * Set the caret index
+ *
+ * @param index
+ * the index or <code>-1</code> if caret is to be hidden
+ */
+ public void setCaretIndex(int index) {
+ this.caretIndex.set(index);
+ }
+
+ /**
+ * Returns the caret index
+ *
+ * @return the index or <code>-1</code> if caret is to be hidden
+ */
+ public int getCaretIndex() {
+ return this.caretIndex.get();
+ }
+
+
+ /**
* The start offset if used in a bigger context like {@link StyledTextArea}
*
* @return the offset
@@ -114,8 +156,7 @@ public class StyledTextLayoutContainer extends Region { private double selectionStartX;
private double selectionEndX;
- private Timeline flashTimeline;
- int caretIndex = -1;
+ private Animation caretAnimation;
private final ReadOnlyBooleanProperty ownerFocusedProperty;
@@ -127,6 +168,15 @@ public class StyledTextLayoutContainer extends Region { this(new SimpleBooleanProperty(true));
}
+ private static Animation createCaretAnimation(Node caret) {
+ FadeTransition t = new FadeTransition(Duration.millis(400), caret);
+ t.setAutoReverse(true);
+ t.setFromValue(1);
+ t.setToValue(0);
+ t.setCycleCount(Animation.INDEFINITE);
+ return t;
+ }
+
/**
* Create a container to layout text and allows to show e.g. a selection
* range
@@ -148,31 +198,72 @@ public class StyledTextLayoutContainer extends Region { this.caret.setManaged(false);
this.caret.getStyleClass().add("text-caret"); //$NON-NLS-1$
- this.flashTimeline = new Timeline();
- this.flashTimeline.setCycleCount(Animation.INDEFINITE);
-
- EventHandler<ActionEvent> startEvent = e -> {
- if( ! ownerFocusedProperty.get() ) {
- StyledTextLayoutContainer.this.caret.setVisible(false);
- } else {
- StyledTextLayoutContainer.this.caret.setVisible(StyledTextLayoutContainer.this.caretIndex != -1);
- }
- };
-
- EventHandler<ActionEvent> endEvent = e -> {
- StyledTextLayoutContainer.this.caret.setVisible(false);
- };
-
- this.flashTimeline.getKeyFrames().addAll(new KeyFrame(Duration.ZERO, startEvent), new KeyFrame(Duration.millis(500), endEvent), new KeyFrame(Duration.millis(1000)));
+ this.caretAnimation = createCaretAnimation(this.caret);
Bindings.bindContent(this.textLayoutNode.getChildren(), this.textNodes);
getChildren().setAll(this.selectionMarker, this.textLayoutNode, this.caret);
selectionProperty().addListener(this::handleSelectionChange);
- ownerFocusedProperty.addListener( o -> {
- if( ! ownerFocusedProperty.get() ) {
- this.caret.setVisible(false);
- }
- });
+
+ this.ownerFocusedProperty.addListener(this::updateCaretVisibility);
+ this.caretIndex.addListener(this::updateCaretVisibility);
+
+// this.annotationsProperty.addListener(new SetChangeListener<StyledTextAnnotation>() {
+// @Override
+// public void onChanged(javafx.collections.SetChangeListener.Change<? extends StyledTextAnnotation> change) {
+// System.err.println("ON ANNOTATION MARKER CHANGE");
+// if (change.getElementAdded() != null) {
+// StyledTextAnnotation a = change.getElementAdded();
+//
+// Rectangle child = new Rectangle();
+//
+// if (a.getType().contains("ERROR")) {
+// child.setFill(Color.RED);
+// }
+// else if (a.getType().contains("WARN")) {
+// child.setFill(Color.YELLOW);
+// }
+// child.setOpacity(0.3);
+//
+// Tooltip tt = new Tooltip();
+// tt.setText(a.getText());
+// getChildren().add(child);
+// Tooltip.install(child, tt);
+//
+// annotationMarkers.put(a, child);
+// }
+// if (change.getElementRemoved() != null) {
+// StyledTextAnnotation a = change.getElementRemoved();
+//
+// Rectangle child = annotationMarkers.remove(a);
+// if (child != null) {
+// getChildren().remove(child);
+// }
+// }
+//
+// requestLayout();
+// }
+// });
+ }
+
+ private void updateCaretVisibility(Observable o) {
+ if (this.ownerFocusedProperty.get() && this.caretIndex.get() != -1) {
+ showCaret();
+ }
+ else {
+ hideCaret();
+ }
+ }
+
+ private void showCaret() {
+ this.caret.setVisible(true);
+ this.caretAnimation.play();
+ requestLayout();
+ }
+
+ private void hideCaret() {
+ this.caret.setVisible(false);
+ this.caretAnimation.stop();
+ requestLayout();
}
private int getEndOffset() {
@@ -241,25 +332,63 @@ public class StyledTextLayoutContainer extends Region { return d;
}
+ private double findX(int localOffset) {
+
+ double len = 0;
+ for (StyledTextNode t : this.textNodes) {
+ if (t.getStartOffset() <= localOffset && t.getEndOffset() > localOffset || this.textNodes.get(this.textNodes.size() - 1) == t) {
+ return len + t.getCharLocation(localOffset - t.getStartOffset());
+ }
+ len += t.getWidth();
+ }
+ return -1;
+ }
+
@Override
protected void layoutChildren() {
super.layoutChildren();
this.textLayoutNode.relocate(getInsets().getLeft(), getInsets().getTop());
+// for (Entry<StyledTextAnnotation, Rectangle> e : annotationMarkers.entrySet()) {
+// System.err.println("LAYOUTING MARKER: " + e.getKey().getText());
+// final int globalBeginIndex = e.getKey().getStartOffset();
+// final int globalEndIndex = e.getKey().getStartOffset() + e.getKey().getLength();
+//
+// System.err.println("global: " + globalBeginIndex + " - " + globalEndIndex);
+//
+// final int localBeginIndex = Math.max(0, globalBeginIndex - getStartOffset());
+// final int localEndIndex = Math.min(getText().length(), globalEndIndex - getStartOffset());
+//
+// System.err.println("local: " + localBeginIndex + " - " + localEndIndex);
+//
+// double xBegin = findX(localBeginIndex);
+// double xEnd = findX(localEndIndex);
+//
+//// System.err.println(xBegin + ", " + getInsets().getTop() + ", " + (xEnd - xBegin)+ ", " + textLayoutNode.prefHeight(-1));
+//// e.getValue().resizeRelocate(xBegin, getInsets().getTop(), xEnd - xBegin, textLayoutNode.prefHeight(-1));
+// e.getValue().setX(xBegin);
+// e.getValue().setY(getInsets().getTop());
+// e.getValue().setWidth(xEnd - xBegin);
+// e.getValue().setHeight(textLayoutNode.prefHeight(-1));
+// e.getValue().toFront();
+// System.err.println(" -> " + e.getValue());
+// }
+
+
if (this.selectionStartNode != null && this.selectionEndNode != null) {
double x1 = this.textLayoutNode.localToParent(this.selectionStartNode.getBoundsInParent().getMinX(), 0).getX() + this.selectionStartX;
double x2 = this.textLayoutNode.localToParent(this.selectionEndNode.getBoundsInParent().getMinX(), 0).getX() + this.selectionEndX;
this.selectionMarker.resizeRelocate(x1, 0, x2 - x1, getHeight());
}
- if (this.caretIndex >= 0) {
+ if (this.getCaretIndex() >= 0) {
this.textLayoutNode.layout();
this.textLayoutNode.applyCss();
for (StyledTextNode t : this.textNodes) {
t.applyCss();
- if (t.getStartOffset() <= this.caretIndex && (t.getEndOffset() > this.caretIndex || this.textNodes.get(this.textNodes.size() - 1) == t)) {
- double caretX = t.getCharLocation(this.caretIndex - t.getStartOffset());
+ if (t.getStartOffset() <= this.getCaretIndex() && (t.getEndOffset() > this.getCaretIndex() || this.textNodes.get(this.textNodes.size() - 1) == t)) {
+ double caretX = t.getCharLocation(this.getCaretIndex() - t.getStartOffset());
double x = this.textLayoutNode.localToParent(t.getBoundsInParent().getMinX(), 0).getX() + caretX;
double h = t.prefHeight(-1);
@@ -319,30 +448,6 @@ public class StyledTextLayoutContainer extends Region { }
/**
- * Set the caret to show at the given index
- *
- * @param index
- * the index or <code>-1</code> if caret is to be hidden
- */
- public void setCaretIndex(int index) {
- if (index >= 0) {
- this.caretIndex = index;
- if( this.ownerFocusedProperty.get() ) {
- this.caret.setVisible(true);
- } else {
- this.caret.setVisible(false);
- }
- this.flashTimeline.play();
- requestLayout();
- } else {
- this.caretIndex = -1;
- this.flashTimeline.stop();
- this.caret.setVisible(false);
- requestLayout();
- }
- }
-
- /**
* Find the position of a the caret at a given index
*
* @param index
@@ -363,7 +468,6 @@ public class StyledTextLayoutContainer extends Region { * Releases resources immediately
*/
public void dispose() {
- this.flashTimeline.stop();
- this.flashTimeline.getKeyFrames().clear();
+ this.caretAnimation.stop();
}
}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/behavior/StyledTextBehavior.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/behavior/StyledTextBehavior.java index 17428e2cd..af96ea859 100644 --- a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/behavior/StyledTextBehavior.java +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/behavior/StyledTextBehavior.java @@ -6,25 +6,67 @@ * http://www.eclipse.org/legal/epl-v10.html * * Contributors: + * Christoph Caks <ccaks@bestsolution.at> - improved editor behavior * Tom Schindl<tom.schindl@bestsolution.at> - initial API and implementation *******************************************************************************/ package org.eclipse.fx.ui.controls.styledtext.behavior; +import static javafx.scene.input.KeyCode.A; +import static javafx.scene.input.KeyCode.BACK_SPACE; +import static javafx.scene.input.KeyCode.C; +import static javafx.scene.input.KeyCode.D; +import static javafx.scene.input.KeyCode.DELETE; +import static javafx.scene.input.KeyCode.DOWN; +import static javafx.scene.input.KeyCode.E; +import static javafx.scene.input.KeyCode.END; +import static javafx.scene.input.KeyCode.ENTER; +import static javafx.scene.input.KeyCode.HOME; +import static javafx.scene.input.KeyCode.LEFT; +import static javafx.scene.input.KeyCode.RIGHT; +import static javafx.scene.input.KeyCode.TAB; +import static javafx.scene.input.KeyCode.UP; +import static javafx.scene.input.KeyCode.V; +import static javafx.scene.input.KeyCode.X; +import static org.eclipse.fx.ui.controls.styledtext.behavior.StyledTextBehavior.KeyMapping.MetaKey.AltKey; +import static org.eclipse.fx.ui.controls.styledtext.behavior.StyledTextBehavior.KeyMapping.MetaKey.ControlKey; +import static org.eclipse.fx.ui.controls.styledtext.behavior.StyledTextBehavior.KeyMapping.MetaKey.MetaKey; +import static org.eclipse.fx.ui.controls.styledtext.behavior.StyledTextBehavior.KeyMapping.MetaKey.ShiftKey; + +import java.text.BreakIterator; +import java.text.StringCharacterIterator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; import org.eclipse.fx.core.Util; import org.eclipse.fx.ui.controls.styledtext.ActionEvent; import org.eclipse.fx.ui.controls.styledtext.ActionEvent.ActionType; import org.eclipse.fx.ui.controls.styledtext.StyledTextArea; +import org.eclipse.fx.ui.controls.styledtext.StyledTextContent; import org.eclipse.fx.ui.controls.styledtext.StyledTextLayoutContainer; import org.eclipse.fx.ui.controls.styledtext.TextSelection; import org.eclipse.fx.ui.controls.styledtext.VerifyEvent; +import org.eclipse.fx.ui.controls.styledtext.behavior.StyledTextBehavior.KeyMapping.InputAction; +import org.eclipse.fx.ui.controls.styledtext.behavior.StyledTextBehavior.KeyMapping.KeyCombo; +import org.eclipse.fx.ui.controls.styledtext.skin.StyledTextSkin; import org.eclipse.fx.ui.controls.styledtext.skin.StyledTextSkin.LineCell; +import org.eclipse.jdt.annotation.NonNull; import javafx.event.Event; import javafx.geometry.Bounds; +import javafx.scene.Node; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; /** @@ -33,6 +75,8 @@ import javafx.scene.input.MouseEvent; public class StyledTextBehavior { private final StyledTextArea styledText; + private KeyMapping keyMapping = new KeyMapping(); + /** * Create a new behavior * @@ -41,7 +85,259 @@ public class StyledTextBehavior { */ public StyledTextBehavior(StyledTextArea styledText) { this.styledText = styledText; - styledText.addEventHandler(KeyEvent.ANY, this::callActionForEvent); + styledText.addEventHandler(KeyEvent.KEY_PRESSED, this::onKeyPressed); + styledText.addEventHandler(KeyEvent.KEY_TYPED, this::onKeyTyped); + + styledText.addEventHandler(MouseEvent.MOUSE_PRESSED, this::onMousePressed); + + initKeymapping(keyMapping); + } + + // called from skin + public void installContentListeners(final Node contentNode) { + contentNode.addEventHandler(MouseEvent.MOUSE_PRESSED, this::onContentMousePressed); + contentNode.addEventHandler(MouseEvent.DRAG_DETECTED, this::onContentDragDetected); + contentNode.addEventHandler(MouseEvent.MOUSE_DRAGGED, this::onContentMouseDragged); + contentNode.addEventHandler(MouseEvent.MOUSE_RELEASED, this::onContentMouseReleased); + contentNode.addEventHandler(MouseEvent.MOUSE_CLICKED, this::onContentMouseClicked); + } + + // text manipulation utils + + static int computeStart(StyledTextContent content, int firstLine) { + return content.getOffsetAtLine(firstLine); + } + + static int computeEnd(StyledTextContent content, int lastLine) { + int endIndex; + if (content.getLineCount() > lastLine + 1) { + endIndex = content.getOffsetAtLine(lastLine + 1); + } + else { + endIndex = content.getOffsetAtLine(lastLine) + content.getLine(lastLine).length(); + } + return endIndex; + } + + static int computeLength(StyledTextContent content, int firstLine, int lastLine) { + return computeEnd(content, lastLine) - computeStart(content, firstLine); + } + + private class LineRegion extends Region { + public final int firstLine; + public final int lastLine; + + public LineRegion(int firstLine, int lastLine) { + super(computeStart(getControl().getContent(), firstLine), computeLength(getControl().getContent(), firstLine, lastLine)); + this.firstLine = firstLine; + this.lastLine = lastLine; + } + + public LineRegion(int singleLineIndex) { + this(singleLineIndex, singleLineIndex); + } + + } + + private @NonNull LineRegion getLineRegion(TextSelection selection) { + int firstLine = getControl().getLineAtOffset(selection.offset); + int lastLine = getControl().getLineAtOffset(selection.offset + selection.length); + int lastLineBegin = getControl().getOffsetAtLine(lastLine); + // dont count the last line if the caret is at index 0 + if (lastLineBegin == selection.offset + selection.length) { + lastLine -= 1; + } + // limit + lastLine = Math.min(getControl().getContent().getLineCount()-1, lastLine); + lastLine = Math.max(firstLine, lastLine); + + return new LineRegion(firstLine, lastLine); + } + + private class Region { + public final int start; + public final int end; + public final int length; + + Region(int startIndex, int length) { + this.start = startIndex; + this.end = startIndex + length; + this.length = length; + } + + public String read() { + return getControl().getContent().getTextRange(start, length); + } + + public void replace(String replacement) { + getControl().getContent().replaceTextRange(start, length, replacement); + } + + public void select() { + System.err.println("selecting " + start + " l " + length); + moveCaretAbsolute(start); + getControl().setSelection(new TextSelection(start, length)); + } + + + } + + private volatile boolean dragMoveTextMode = false; + private volatile boolean dragSelectionMode = false; + + + // state for dnd stuff + + private volatile int dragMoveTextOffset = -1; + private volatile int dragMoveTextLength = -1; + + private int mousePressedOffset = -1; + + + private boolean isInRange(int offset, int rangeOffset, int rangeLength) { + return offset >= rangeOffset && offset < (rangeOffset + rangeLength); + } + + private boolean isInSelection(int offset) { + int selOffset = getControl().getSelection().offset; + int selLength = getControl().getSelection().length; + boolean r = selLength > 0 && isInRange(offset, selOffset, selLength); + System.err.println("isInSelection(" + offset + ")" + "[" + selOffset + ", " + (selOffset + selLength) + ") -> " + r); + return r; + } + + + + private void onKeyPressed(KeyEvent event) { + VerifyEvent evt = new VerifyEvent(getControl(), getControl(), event); + Event.fireEvent(getControl(), evt); + + // Bug in JavaFX who enables the menu when ALT is pressed + if (Util.isMacOS()) { + if (event.getCode() == KeyCode.ALT || event.isAltDown()) { + event.consume(); + } + } + + if (evt.isConsumed()) { + event.consume(); + return; + } + + Optional<InputAction> keyAction = this.keyMapping.get(event); + keyAction.ifPresent(a->{ + a.run(); + event.consume(); + }); + } + + private void onKeyTyped(KeyEvent event) { + if (getControl().getEditable()) { + + String character = event.getCharacter(); + if (character.length() == 0) { + return; + } + + // check the modifiers + // - OS-X: ALT+L ==> @ + // - win32/linux: ALTGR+Q ==> @ + if (event.isControlDown() || event.isAltDown() || (Util.isMacOS() && event.isMetaDown())) { + if (!((event.isControlDown() || Util.isMacOS()) && event.isAltDown())) + return; + } + + if (character.charAt(0) > 31 // No ascii control chars + && character.charAt(0) != 127 // no delete key + && !event.isMetaDown()) { + + getControl().insert(character); + } + + } + } + + + private void onContentMousePressed(MouseEvent event) { + this.mousePressedOffset = computeCursorOffset(event); + System.err.println("MOUSE_PRESSED @ " + mousePressedOffset); + + if (isInSelection(mousePressedOffset)) { + System.err.println(" -> starting dragMoveTextMode"); + event.setDragDetect(true); + this.dragMoveTextMode = true; + this.dragMoveTextOffset = getControl().getSelection().offset; + this.dragMoveTextLength = getControl().getSelection().length; + } + else { + System.err.println(" -> setting Caret"); + moveCaretAbsolute(this.mousePressedOffset, event.isShiftDown()); + } + } + private void onContentDragDetected(MouseEvent event) { + if (!this.dragMoveTextMode && ! this.dragSelectionMode) { + this.dragSelectionMode = true; + } + } + private void onContentMouseDragged(MouseEvent event) { + if (this.dragSelectionMode) { +// System.err.println("MOUSE_DRAGGED [dragSelectionMode]"); + int offset = computeCursorOffset(event); + moveCaretAbsolute(offset, true); + event.consume(); + } + else if (this.dragMoveTextMode) { + //System.err.println("MOUSE_DRAGGED [dragMoveTextMode]"); + // nothing to do + event.consume(); + } + } + private void onContentMouseReleased(MouseEvent event) { + System.err.println("MOUSE_RELEASED"); + if (this.dragSelectionMode) { + this.dragSelectionMode = false; + event.consume(); + } + else if (this.dragMoveTextMode) { + int targetOffset = computeCursorOffset(event); + + // replace + if (isInRange(targetOffset, dragMoveTextOffset, dragMoveTextLength)) { + targetOffset = dragMoveTextOffset; + } + // after + else if (targetOffset >= dragMoveTextOffset + dragMoveTextLength) { + targetOffset -= dragMoveTextLength; + } + + // read text + @NonNull + String text = getControl().getContent().getTextRange(dragMoveTextOffset, dragMoveTextLength); + + // remove + getControl().getContent().replaceTextRange(dragMoveTextOffset, dragMoveTextLength, ""); + + // insert + getControl().getContent().replaceTextRange(targetOffset, 0, text); + + // move caret to end of insertion + moveCaretAbsolute(targetOffset + text.length()); + + this.dragMoveTextMode = false; + event.consume(); + } + } + + private void onContentMouseClicked(MouseEvent event) { + if (event.isStillSincePress()) { + System.err.println("MOUSE_CLICKED @ " + mousePressedOffset); + if (event.getClickCount() == 2 && event.getButton() == MouseButton.PRIMARY) { + System.err.println(" -> executing double click"); + // double click + this.ACTION_SELECT_WORD.run(); + event.consume(); + } + } } /** @@ -51,295 +347,978 @@ public class StyledTextBehavior { return this.styledText; } + private void onMousePressed(MouseEvent event) { + getControl().requestFocus(); + } + + private int computeCurrentLineNumber() { + final int offset = getControl().getCaretOffset(); + return getControl().getLineAtOffset(offset); + } + + private int computeCurrentLineStartOffset() { + final int lineNumber = computeCurrentLineNumber(); + return getControl().getOffsetAtLine(lineNumber); + } + /** - * Handle key event - * - * @param event - * the event + * Action to go the start of the text */ - protected void callActionForEvent(KeyEvent event) { - if (event.getEventType() == KeyEvent.KEY_PRESSED) { - _keyPressed(event); - } else if (event.getEventType() == KeyEvent.KEY_TYPED) { - _keyTyped(event); - } + protected final StyledTextInputAction ACTION_NAVIGATE_TEXT_START = new StyledTextInputAction(ActionType.TEXT_START, this::defaultNavigateTextStart); + /** + * default implementation for {@link #ACTION_NAVIGATE_TEXT_START} + */ + protected void defaultNavigateTextStart() { + getControl().setCaretOffset(0); } /** - * Handle the mouse event that happens on the content - * - * @param event - * the event - * @param visibleCells - * the visible cell + * Action to go the start of the text */ - public void handleContentMouseEvent(MouseEvent event, List<LineCell> visibleCells) { - if (event.getEventType() == MouseEvent.MOUSE_PRESSED) { - updateCursor(event, visibleCells, event.isShiftDown()); + protected final InputAction ACTION_NAVIGATE_TEXT_END = new StyledTextInputAction(ActionType.TEXT_END, this::defaultNavigateTextEnd); + /** + * default implementation for {@link #ACTION_NAVIGATE_TEXT_END} + */ + protected void defaultNavigateTextEnd() { + getControl().setCaretOffset(getControl().getContent().getCharCount()); + } - // The consuming does not help because it looks like the - // selection change happens earlier => should be push a new - // ListViewBehavior? - event.consume(); + /** + * Action to go to the line start + */ + protected final InputAction ACTION_NAVIGATE_LINE_START = new StyledTextInputAction(ActionType.LINE_START, this::defaultNavigateLineStart); + /** + * default implementation for {@link #ACTION_NAVIGATE_LINE_START} + */ + protected void defaultNavigateLineStart() { + //TODO Should be position to the first none whitespace char?? + moveCaretAbsolute(computeCurrentLineStartOffset()); + } + + /** + * Action to go to the line end + */ + protected final InputAction ACTION_NAVIGATE_LINE_END = new StyledTextInputAction(ActionType.LINE_END, this::defaultNavigateLineEnd); + /** + * default implementation for {@link #ACTION_NAVIGATE_LINE_END} + */ + protected void defaultNavigateLineEnd() { + final int caretLine = computeCurrentLineNumber(); + moveCaretAbsolute(getControl().getContent().getOffsetAtLine(caretLine) + getControl().getContent().getLine(caretLine).length()); + } + + /** + * Action to go to the next word + */ + protected final InputAction ACTION_NAVIGATE_WORD_NEXT = new StyledTextInputAction(ActionType.WORD_NEXT, this::defaultNavigateWordNext); + /** + * default implementation for {@link #ACTION_NAVIGATE_WORD_NEXT} + */ + protected void defaultNavigateWordNext() { + BreakIterator wordInstance = BreakIterator.getWordInstance(); + wordInstance.setText(new StringCharacterIterator(getControl().getContent().getTextRange(0, getControl().getContent().getCharCount()))); + int following = wordInstance.following(getControl().getCaretOffset()); + if( following != BreakIterator.DONE ) { + moveCaretAbsolute(following); } } /** - * handle the mouse pressed - * - * @param arg0 - * the mouse event + * Action to go to the previous word */ - public void mousePressed(MouseEvent arg0) { - getControl().requestFocus(); + protected final InputAction ACTION_NAVIGATE_WORD_PREVIOUS = new StyledTextInputAction(ActionType.WORD_PREVIOUS, this::defaultNavigateWordPrevious); + /** + * default implementation for {@link #ACTION_NAVIGATE_WORD_PREVIOUS} + */ + protected void defaultNavigateWordPrevious() { + BreakIterator wordInstance = BreakIterator.getWordInstance(); + wordInstance.setText(new StringCharacterIterator(getControl().getContent().getTextRange(0, getControl().getContent().getCharCount()))); + int previous = wordInstance.preceding(getControl().getCaretOffset()); + if( previous != BreakIterator.DONE ) { + moveCaretAbsolute(previous); + } } /** - * Invoke an action - * - * @param action - * the action + * Action to select to the start of the text */ - public void invokeAction(ActionType action) { - ActionEvent evt = new ActionEvent(getControl(), getControl(), action); - Event.fireEvent(getControl(), evt); + protected final InputAction ACTION_SELECT_TEXT_START = new StyledTextInputAction(ActionType.SELECT_TEXT_START, this::defaultSelectTextStart); + /** + * default implementation for {@link #ACTION_SELECT_TEXT_START} + */ + protected void defaultSelectTextStart() { + moveCaretAbsolute(0, true); } - @SuppressWarnings("deprecation") - private void _keyPressed(KeyEvent event) { - VerifyEvent evt = new VerifyEvent(getControl(), getControl(), event); - Event.fireEvent(getControl(), evt); + /** + * Action to select to the end of the text + */ + protected final InputAction ACTION_SELECT_TEXT_END = new StyledTextInputAction(ActionType.SELECT_TEXT_END, this::defaultSelectTextEnd); + /** + * default implementation for {@link #ACTION_SELECT_TEXT_END} + */ + protected void defaultSelectTextEnd() { + moveCaretAbsolute(getControl().getCharCount(),true); + } - // Bug in JavaFX who enables the menu when ALT is pressed - if (Util.isMacOS()) { - if (event.getCode() == KeyCode.ALT || event.isAltDown()) { - event.consume(); - } + /** + * Action to select until the start of the line + */ + protected final InputAction ACTION_SELECT_LINE_START = new StyledTextInputAction(ActionType.SELECT_LINE_START, this::defaultSelectLineStart); + /** + * default implementation for {@link #ACTION_SELECT_LINE_START} + */ + protected void defaultSelectLineStart() { + //TODO Should be position to the first none whitespace char?? + moveCaretAbsolute(computeCurrentLineStartOffset(), true); + } + + /** + * Action to select until the end of the line + */ + protected final InputAction ACTION_SELECT_LINE_END = new StyledTextInputAction(ActionType.SELECT_LINE_END, this::defaultSelectLineEnd); + /** + * default implementation for {@link #ACTION_SELECT_LINE_END} + */ + protected void defaultSelectLineEnd() { + int caretLine = getControl().getContent().getLineAtOffset(getControl().getCaretOffset()); + int end = getControl().getContent().getOffsetAtLine(caretLine) + getControl().getContent().getLine(caretLine).length(); + moveCaretAbsolute(end, true); + } + + /** + * Action to select the next word + */ + protected final InputAction ACTION_SELECT_WORD_NEXT = new StyledTextInputAction(ActionType.SELECT_WORD_NEXT, this::defaultSelectWordNext); + /** + * default implementation for {@link #ACTION_SELECT_WORD_NEXT} + */ + protected void defaultSelectWordNext() { + BreakIterator wordInstance = BreakIterator.getWordInstance(); + wordInstance.setText(new StringCharacterIterator(getControl().getContent().getTextRange(0, getControl().getContent().getCharCount()))); + int following = wordInstance.following(getControl().getCaretOffset()); + if( following != BreakIterator.DONE ) { + moveCaretAbsolute(following, true); } + } - if (evt.isConsumed()) { - event.consume(); - return; + /** + * Action to select the previous word + */ + protected final InputAction ACTION_SELECT_WORD_PREVIOUS = new StyledTextInputAction(ActionType.SELECT_WORD_PREVIOUS, this::defaultSelectWordPrevious); + /** + * default implementation for {@link #ACTION_SELECT_WORD_PREVIOUS} + */ + protected void defaultSelectWordPrevious() { + BreakIterator wordInstance = BreakIterator.getWordInstance(); + wordInstance.setText(new StringCharacterIterator(getControl().getContent().getTextRange(0, getControl().getContent().getCharCount()))); + int previous = wordInstance.preceding(getControl().getCaretOffset()); + if( previous != BreakIterator.DONE ) { + moveCaretAbsolute(previous, true); } + } - int currentRowIndex = getControl().getContent().getLineAtOffset(getControl().getCaretOffset()); + /** + * Action to select the word at the current cursor + */ + protected final InputAction ACTION_SELECT_WORD = new StyledTextInputAction(ActionType.SELECT_WORD, this::defaultSelectWord); + /** + * default implementation for {@link #ACTION_SELECT_WORD} + */ + protected void defaultSelectWord() { + BreakIterator wordInstance = BreakIterator.getWordInstance(); + wordInstance.setText(new StringCharacterIterator(getControl().getContent().getTextRange(0, getControl().getContent().getCharCount()))); + int previous = wordInstance.preceding(getControl().getCaretOffset()); + int next = wordInstance.following(getControl().getCaretOffset()); + if( previous != BreakIterator.DONE && next != BreakIterator.DONE ) { + moveCaretAbsolute(previous); + moveCaretAbsolute(next, true); + } + } - final int offset = getControl().getCaretOffset(); + /** + * Action to delete current line + */ + protected final InputAction ACTION_DELETE_LINE = new StyledTextInputAction(this::defaultDeleteLine); + /** + * default implementation for {@link #ACTION_DELETE_WORD_NEXT} + */ + protected void defaultDeleteLine() { + LineRegion lineRegion = getLineRegion(getControl().getSelection()); + lineRegion.replace(""); //$NON-NLS-1$ + moveCaretAbsolute(lineRegion.start); - switch (event.getCode()) { - case SHIFT: - case ALT: - case CONTROL: - break; - case LEFT: { - if (event.isAltDown()) { - invokeAction(ActionType.WORD_PREVIOUS); - } else { - if (offset == 0) { - event.consume(); - break; - } - int newOffset = offset - 1; - @SuppressWarnings("unused") - int currentLine = getControl().getContent().getLineAtOffset(offset); - @SuppressWarnings("unused") - int newLine = getControl().getContent().getLineAtOffset(newOffset); - getControl().impl_setCaretOffset(newOffset, event.isShiftDown()); - event.consume(); +// @NonNull +// LineSelection lineSelection = getLineSelection(); +// +// System.err.println("delete " + lineSelection.firstLine + " to " + lineSelection.lastLine); +// int beginIndex = getControl().getOffsetAtLine(lineSelection.firstLine); +// int endIndex; +// if (getControl().getContent().getLineCount() > lineSelection.lastLine + 1) { +// endIndex = getControl().getOffsetAtLine(lineSelection.lastLine + 1); +// } +// else { +// endIndex = getControl().getOffsetAtLine(lineSelection.lastLine) + getControl().getContent().getLine(lineSelection.lastLine).length(); +// } +// +// getControl().getContent().replaceTextRange(beginIndex, endIndex - beginIndex, ""); //$NON-NLS-1$ +// moveCaretAbsolute(beginIndex); + } + + /** + * Action to delete next word + */ + protected final InputAction ACTION_DELETE_WORD_NEXT = new StyledTextInputAction(ActionType.DELETE_WORD_NEXT, this::defaultDeleteWordNext); + /** + * default implementation for {@link #ACTION_DELETE_WORD_NEXT} + */ + protected void defaultDeleteWordNext() { + int offset = getControl().getCaretOffset(); + BreakIterator wordInstance = BreakIterator.getWordInstance(); + wordInstance.setText(new StringCharacterIterator(getControl().getContent().getTextRange(0, getControl().getContent().getCharCount()))); + int following = wordInstance.following(getControl().getCaretOffset()); + if( following != BreakIterator.DONE ) { + getControl().getContent().replaceTextRange(getControl().getCaretOffset(), following - offset, ""); //$NON-NLS-1$ + } + } + + /** + * Action to delete previous word + */ + protected final InputAction ACTION_DELETE_WORD_PREVIOUS = new StyledTextInputAction(ActionType.DELETE_WORD_PREVIOUS, this::defaultDeleteWordPrevious); + /** + * default implementation for {@link #ACTION_DELETE_WORD_PREVIOUS} + */ + protected void defaultDeleteWordPrevious() { + int offset = getControl().getCaretOffset(); + BreakIterator wordInstance = BreakIterator.getWordInstance(); + wordInstance.setText(new StringCharacterIterator(getControl().getContent().getTextRange(0, getControl().getContent().getCharCount()))); + int previous = wordInstance.preceding(getControl().getCaretOffset()); + if( previous != BreakIterator.DONE ) { + getControl().setCaretOffset(previous); + getControl().getContent().replaceTextRange(previous, offset - previous, ""); //$NON-NLS-1$ + } + } + + /** + * Action to move selected lines up + */ + protected final InputAction ACTION_MOVE_LINES_UP = new StyledTextInputAction(this::defaultMoveLinesUp); + /** + * default implementation for {@link #ACTION_MOVE_LINES_UP} + */ + protected void defaultMoveLinesUp() { + LineRegion moveTarget = getLineRegion(getControl().getSelection()); + if (moveTarget.firstLine > 0) { + LineRegion above = new LineRegion(moveTarget.firstLine -1); + LineRegion all = new LineRegion(above.firstLine, moveTarget.lastLine); + + String aboveText = above.read(); + String moveTargetText = moveTarget.read(); + // we reach the last line + if (moveTarget.lastLine + 1 == getControl().getContent().getLineCount()) { + moveTargetText += getControl().getLineSeparator().getValue(); + aboveText = aboveText.replaceFirst("\r?\n$", ""); //$NON-NLS-1$ //$NON-NLS-2$ } - break; + + all.replace( moveTargetText + aboveText); + new LineRegion(moveTarget.firstLine -1, moveTarget.lastLine -1).select(); } - case RIGHT: { - if (event.isAltDown()) { - invokeAction(ActionType.WORD_NEXT); - } else if (event.isMetaDown()) { - int currentLine = getControl().getContent().getLineAtOffset(offset); - int lineOffset = getControl().getContent().getOffsetAtLine(currentLine); - String lineContent = getControl().getContent().getLine(currentLine); + } - getControl().impl_setCaretOffset(lineOffset + lineContent.length(), event.isShiftDown()); - event.consume(); - } else { - if (offset + 1 > getControl().getContent().getCharCount()) { - break; - } - int newOffset = offset + 1; - // @SuppressWarnings("unused") - // int currentLine = - // getControl().getContent().getLineAtOffset(offset); - // @SuppressWarnings("unused") - // int newLine = - // getControl().getContent().getLineAtOffset(newOffset); - getControl().impl_setCaretOffset(newOffset, event.isShiftDown()); - event.consume(); + /** + * Action to move selected lines down + */ + protected final InputAction ACTION_MOVE_LINES_DOWN = new StyledTextInputAction(this::defaultMoveLinesDown); + /** + * default implementation for {@link #ACTION_MOVE_LINES_DOWN} + */ + protected void defaultMoveLinesDown() { + LineRegion moveTarget = getLineRegion(getControl().getSelection()); + if (moveTarget.lastLine + 1 < getControl().getContent().getLineCount()) { + + LineRegion below = new LineRegion(moveTarget.lastLine + 1); + LineRegion all = new LineRegion(moveTarget.firstLine, below.lastLine); + + String belowText = below.read(); + String moveTargetText = moveTarget.read(); + // we reach the last line + if (below.lastLine + 1 == getControl().getContent().getLineCount()) { + belowText += getControl().getLineSeparator().getValue(); + moveTargetText = moveTargetText.replaceFirst("\r?\n$", ""); //$NON-NLS-1$ //$NON-NLS-2$ } - break; + + all.replace(belowText + moveTargetText); + new LineRegion(moveTarget.firstLine +1, moveTarget.lastLine +1).select(); } - case UP: { - int rowIndex = currentRowIndex; + } + - if (rowIndex == 0) { + /** + * Action to create a new line + */ + protected final InputAction ACTION_NEW_LINE = new StyledTextInputAction(ActionType.NEW_LINE, this::defaultNewLine); + /** + * default implementation for {@link #ACTION_NEW_LINE} + */ + protected void defaultNewLine() { + int offset = getControl().getCaretOffset(); + int line = getControl().getContent().getLineAtOffset(offset); + String lineContent = getControl().getContent().getLine(line); + + // Should we make this configurable + char[] chars = lineContent.toCharArray(); + String prefix = ""; //$NON-NLS-1$ + for (int i = 0; i < chars.length; i++) { + if (chars[i] == ' ' || chars[i] == '\t') { + prefix += chars[i]; + } else { break; } + } + getControl().getContent().replaceTextRange(getControl().getCaretOffset(), 0, getControl().getLineSeparator().getValue() + prefix); + getControl().setCaretOffset(offset + getControl().getLineSeparator().getValue().length() + prefix.length()); - int colIdx = offset - getControl().getContent().getOffsetAtLine(rowIndex); - rowIndex -= 1; + } - int lineOffset = getControl().getContent().getOffsetAtLine(rowIndex); - int newCaretPosition = lineOffset + colIdx; - int maxPosition = lineOffset + getControl().getContent().getLine(rowIndex).length(); + /** + * Action to select all + */ + protected final InputAction ACTION_SELECT_ALL = new StyledTextInputAction(this::defaultSelectAll); + /** + * default implementation for {@link #ACTION_SELECT_ALL} + */ + protected void defaultSelectAll() { + int length = getControl().getContent().getCharCount(); + getControl().setSelectionRange(0, length); + } - getControl().impl_setCaretOffset(Math.min(newCaretPosition, maxPosition), event.isShiftDown()); - event.consume(); - break; + /** + * Action to copy + */ + protected final InputAction ACTION_COPY = new StyledTextInputAction(this::defaultCopy); + /** + * default implementation for {@link #ACTION_COPY} + */ + protected void defaultCopy() { + getControl().copy(); + } + + /** + * Action to paste + */ + protected final InputAction ACTION_PASTE = new StyledTextInputAction(this::defaultPaste); + /** + * default implementation for {@link #ACTION_PASTE} + */ + protected void defaultPaste() { + if (getControl().getEditable()) { + getControl().paste(); } - case DOWN: { - int rowIndex = currentRowIndex; - if (rowIndex + 1 == getControl().getContent().getLineCount()) { - break; + } + + /** + * Action to cut + */ + protected final InputAction ACTION_CUT = new StyledTextInputAction(this::defaultCut); + /** + * default implementation for {@link #ACTION_CUT} + */ + protected void defaultCut() { + if (getControl().getEditable()) { + getControl().cut(); + } + } + + /** + * Action to delete + */ + protected final InputAction ACTION_DELETE = new StyledTextInputAction(this::defaultDelete); + /** + * default implementation for {@link #ACTION_DELETE} + */ + protected void defaultDelete() { + int offset = getControl().getCaretOffset(); + TextSelection selection = getControl().getSelection(); + if (selection.length > 0) { + getControl().getContent().replaceTextRange(selection.offset, selection.length, ""); //$NON-NLS-1$ + getControl().setCaretOffset(selection.offset); + } else { + getControl().getContent().replaceTextRange(getControl().getCaretOffset(), 1, ""); //$NON-NLS-1$ + getControl().setCaretOffset(offset); + } + } + + /** + * Action to delete backwards + */ + protected final InputAction ACTION_BACKSPACE = new StyledTextInputAction(this::defaultBackspace); + /** + * default implementation for {@link #ACTION_BACKSPACE} + */ + protected void defaultBackspace() { + int offset = getControl().getCaretOffset(); + TextSelection selection = getControl().getSelection(); + if (selection.length > 0) { + getControl().getContent().replaceTextRange(selection.offset, selection.length, ""); //$NON-NLS-1$ + getControl().setCaretOffset(selection.offset); + } else { + getControl().getContent().replaceTextRange(getControl().getCaretOffset() - 1, 1, ""); //$NON-NLS-1$ + getControl().setCaretOffset(offset - 1); + } + } + + + private boolean isMultilineSelection() { + return getControl().getLineAtOffset(getControl().getSelection().offset) != getControl().getLineAtOffset(getControl().getSelection().offset + getControl().getSelection().length); + } + + + /** + * Action to indent + */ + protected final InputAction ACTION_INDENT = new StyledTextInputAction(ActionType.INDENT, this::defaultIndent); + /** + * default implementation for {@link #ACTION_INDENT} + */ + protected void defaultIndent() { + if (isMultilineSelection()) { + + // TODO use LineRegion + // TODO only replace selected lines + + String allContent = getControl().getContent().getTextRange(0, getControl().getCharCount()); + StringBuffer dataBuffer = new StringBuffer(allContent); + + final int caret = getControl().getCaretOffset(); + final int selectionOffset = getControl().getSelection().offset; + final int selectionLength = getControl().getSelection().length; + + final int firstLine = getControl().getLineAtOffset(selectionOffset); + int lastLine = getControl().getLineAtOffset(selectionOffset + selectionLength); + + if (getControl().getOffsetAtLine(lastLine) < selectionOffset + selectionLength) { + // we need to indent this line too + lastLine += 1; } - int colIdx = offset - getControl().getContent().getOffsetAtLine(rowIndex); - rowIndex += 1; + int added = 0; - int lineOffset = getControl().getContent().getOffsetAtLine(rowIndex); - int newCaretPosition = lineOffset + colIdx; - int maxPosition = lineOffset + getControl().getContent().getLine(rowIndex).length(); + int firstLineDelta = 0; - getControl().impl_setCaretOffset(Math.min(newCaretPosition, maxPosition), event.isShiftDown()); - event.consume(); - break; - } - case ENTER: - if (getControl().getEditable()) { - int line = getControl().getContent().getLineAtOffset(getControl().getCaretOffset()); - String lineContent = getControl().getContent().getLine(line); - - // FIXME Temp hack - char[] chars = lineContent.toCharArray(); - String prefix = ""; //$NON-NLS-1$ - for (int i = 0; i < chars.length; i++) { - if (chars[i] == ' ') { - prefix += " "; //$NON-NLS-1$ - } else { - break; + for (int lineNumber = firstLine; lineNumber < lastLine; lineNumber++) { + int lineStart = getControl().getOffsetAtLine(lineNumber) + added; + dataBuffer.replace(lineStart, lineStart + 0, "\t"); //$NON-NLS-1$ + added += 1; + if (lineNumber == firstLine) { + if (selectionOffset > lineStart) { + firstLineDelta = 1; } } + } + + getControl().getContent().setText(dataBuffer.toString()); + getControl().setCaretOffset(caret + added); + getControl().setSelectionRange(selectionOffset + firstLineDelta, selectionLength + added - firstLineDelta); + } + } + + /** + * Action to outdent (opposite of indentation) + */ + protected final StyledTextInputAction ACTION_OUTDENT = new StyledTextInputAction(ActionType.OUTDENT, this::defaultOutdent); + /** + * default implementation for {@link #ACTION_OUTDENT} + */ + protected void defaultOutdent() { + // TODO use LineRegion + // TODO only replace selected lines + + String allContent = getControl().getContent().getTextRange(0, getControl().getCharCount()); + StringBuffer dataBuffer = new StringBuffer(allContent); + + final int caret = getControl().getCaretOffset(); + final int selectionOffset = getControl().getSelection().offset; + final int selectionLength = getControl().getSelection().length; + + final int firstLine = getControl().getLineAtOffset(selectionOffset); + int lastLine = getControl().getLineAtOffset(selectionOffset + selectionLength); + + if (getControl().getOffsetAtLine(lastLine) < selectionOffset + selectionLength) { + // we need to indent this line too + lastLine += 1; + } - String newLine = System.getProperty("line.separator"); //$NON-NLS-1$ + int firstLineDelta = 0; - getControl().getContent().replaceTextRange(getControl().getCaretOffset(), 0, newLine + prefix); - // listView.getSelectionModel().select(listView.getSelectionModel().getSelectedIndex()+1); - getControl().setCaretOffset(offset + newLine.length() + prefix.length()); + for (int lineNumber = firstLine; lineNumber < lastLine; lineNumber++) { + int lineStart = getControl().getOffsetAtLine(lineNumber); + if (dataBuffer.charAt(lineStart)!='\t') { + return; } - break; - case DELETE: - if (getControl().getEditable()) { - if (event.isMetaDown()) { - invokeAction(ActionType.DELETE_WORD_NEXT); - } else { - getControl().getContent().replaceTextRange(getControl().getCaretOffset(), 1, ""); //$NON-NLS-1$ - getControl().setCaretOffset(offset); + if (lineNumber == firstLine) { + if (selectionOffset > lineStart) { + firstLineDelta = 1; } - break; } - case BACK_SPACE: - if (getControl().getEditable()) { - if (event.isMetaDown()) { - invokeAction(ActionType.DELETE_WORD_PREVIOUS); - } else { - TextSelection selection = getControl().getSelection(); - if (selection.length > 0) { - getControl().getContent().replaceTextRange(selection.offset, selection.length, ""); //$NON-NLS-1$ - getControl().setCaretOffset(selection.offset); - } else { - getControl().getContent().replaceTextRange(getControl().getCaretOffset() - 1, 1, ""); //$NON-NLS-1$ - getControl().setCaretOffset(offset - 1); - } - } - break; + } + + int removed = 0; + + for (int lineNumber = lastLine-1; lineNumber >= firstLine; lineNumber--) { + int lineStart = getControl().getOffsetAtLine(lineNumber); + dataBuffer.replace(lineStart, lineStart + 1, ""); + removed += 1; + } + + getControl().getContent().setText(dataBuffer.toString()); + getControl().setCaretOffset(caret - removed); + getControl().setSelectionRange(selectionOffset - firstLineDelta, selectionLength - removed + firstLineDelta); + } + + /** + * Action to move caret upwards + */ + protected final StyledTextInputAction ACTION_MOVE_UP = new StyledTextInputAction(()->defaultUp(false)); + /** + * Action to move caret upwards while selecting + */ + protected final StyledTextInputAction ACTION_SELECT_UP = new StyledTextInputAction(()->defaultUp(true)); + + /** + * default implementation for {@link #ACTION_MOVE_UP} and {@link #ACTION_SELECT_UP} + * @param select whether to change the selection + */ + protected void defaultUp(boolean select) { + int currentRowIndex = getControl().getContent().getLineAtOffset(getControl().getCaretOffset()); + + final int offset = getControl().getCaretOffset(); + + int rowIndex = currentRowIndex; + + if (rowIndex == 0) { + return; + } + + int colIdx = offset - getControl().getContent().getOffsetAtLine(rowIndex); + rowIndex -= 1; + + int lineOffset = getControl().getContent().getOffsetAtLine(rowIndex); + int newCaretPosition = lineOffset + colIdx; + int maxPosition = lineOffset + getControl().getContent().getLine(rowIndex).length(); + + moveCaretAbsolute(Math.min(newCaretPosition, maxPosition), select); + } + + /** + * Action to move caret down + */ + protected final StyledTextInputAction ACTION_MOVE_DOWN = new StyledTextInputAction(()->defaultDown(false)); + /** + * Action to move caret down while selecting + */ + protected final StyledTextInputAction ACTION_SELECT_DOWN = new StyledTextInputAction(()->defaultDown(true)); + + /** + * default implementation for {@link #ACTION_MOVE_DOWN} and {@link #ACTION_SELECT_DOWN} + * @param select whether to change the selection + */ + protected void defaultDown(boolean select) { + int currentRowIndex = getControl().getContent().getLineAtOffset(getControl().getCaretOffset()); + + final int offset = getControl().getCaretOffset(); + + int rowIndex = currentRowIndex; + if (rowIndex + 1 == getControl().getContent().getLineCount()) { + return; + } + + int colIdx = offset - getControl().getContent().getOffsetAtLine(rowIndex); + rowIndex += 1; + + int lineOffset = getControl().getContent().getOffsetAtLine(rowIndex); + int newCaretPosition = lineOffset + colIdx; + int maxPosition = lineOffset + getControl().getContent().getLine(rowIndex).length(); + + moveCaretAbsolute(Math.min(newCaretPosition, maxPosition), select); + } + + /** + * Action to move caret left + */ + protected final StyledTextInputAction ACTION_MOVE_LEFT = new StyledTextInputAction(()->defaultLeft(false)); + /** + * Action to move caret left while selecting + */ + protected final StyledTextInputAction ACTION_SELECT_LEFT = new StyledTextInputAction(()->defaultLeft(true)); + + /** + * default implementation for {@link #ACTION_MOVE_LEFT} and {@link #ACTION_SELECT_LEFT} + * @param select whether to change the selection + */ + protected void defaultLeft(boolean select) { + moveCaretRelative(-1, select); + } + + /** + * Action to move caret right + */ + protected final StyledTextInputAction ACTION_MOVE_RIGHT = new StyledTextInputAction(()->defaultRight(false)); + /** + * Action to move caret right while selecting + */ + protected final StyledTextInputAction ACTION_SELECT_RIGHT = new StyledTextInputAction(()->defaultRight(true)); + + /** + * default implementation for {@link #ACTION_MOVE_RIGHT} and {@link #ACTION_SELECT_RIGHT} + * @param select whether to change the selection + */ + protected void defaultRight(boolean select) { + moveCaretRelative(1, select); + } + + + /** + * Action to scroll one line up + */ + protected final StyledTextInputAction ACTION_SCROLL_LINE_UP = new StyledTextInputAction(this::defaultScrollLineUp); + /** + * default implementation for {@link #ACTION_SCROLL_LINE_UP} + */ + protected void defaultScrollLineUp() { + ((StyledTextSkin)getControl().getSkin()).scrollLineUp(); + } + + /** + * Action to scroll one line down + */ + protected final StyledTextInputAction ACTION_SCROLL_LINE_DOWN = new StyledTextInputAction(this::defaultScrollLineDown); + /** + * default implementation for {@link #ACTION_SCROLL_LINE_DOWN} + */ + protected void defaultScrollLineDown() { + ((StyledTextSkin)getControl().getSkin()).scrollLineDown(); + } + + + private void moveCaretAbsolute(int absoluteOffset) { + int offset = Math.max(0, absoluteOffset); + offset = Math.min(getControl().getCharCount(), offset); + getControl().setCaretOffset(offset); + } + + @SuppressWarnings("deprecation") + private void moveCaretAbsolute(int absoluteOffset, boolean select) { + int offset = Math.max(0, absoluteOffset); + offset = Math.min(getControl().getCharCount(), offset); + getControl().impl_setCaretOffset(offset, select); + } + + private void moveCaretRelative(int deltaOffset, boolean select) { + int offset = getControl().getCaretOffset() + deltaOffset; + moveCaretAbsolute(offset, select); + } + + protected static class KeyMapping { + public static enum MetaKey { + AltKey, + ControlKey, + MetaKey, + ShiftKey + } + + public static class KeyCombo { + public final KeyCode code; + public final Set<MetaKey> meta; + + public KeyCombo(KeyCode code, MetaKey... meta) { + this.code = code; + this.meta = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(meta))); } - case TAB: - if (getControl().getEditable()) { - event.consume(); - if (event.isShiftDown()) { - // TODO Remove first 4 white space chars??? - break; - } else { - getControl().getContent().replaceTextRange(getControl().getCaretOffset(), 0, "\t"); //$NON-NLS-1$ - getControl().setCaretOffset(offset + 1); - break; - } + + @Override + public String toString() { + return code + " " + meta; } - case V: - if (getControl().getEditable()) { - if (event.isShortcutDown()) { - getControl().paste(); - event.consume(); - break; - } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((code == null) ? 0 : code.hashCode()); + result = prime * result + ((meta == null) ? 0 : meta.hashCode()); + return result; } - case X: - if (getControl().getEditable()) { - if (event.isShortcutDown()) { - getControl().cut(); - event.consume(); - break; - } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + KeyCombo other = (KeyCombo) obj; + if (code != other.code) + return false; + if (meta == null) { + if (other.meta != null) + return false; + } else if (!meta.equals(other.meta)) + return false; + return true; } - case C: { - if (event.isShortcutDown()) { - getControl().copy(); - event.consume(); - break; + + } + + public static interface Mapping { + Supplier<Boolean> getCondition(); + InputAction getAction(); + } + + public static class SimpleMapping implements Mapping { + private final Supplier<Boolean> condition; + private final InputAction action; + + @Override + public Supplier<Boolean> getCondition() { + return condition; + } + + @Override + public InputAction getAction() { + return action; + } + + public SimpleMapping(Supplier<Boolean> condition, InputAction action) { + this.condition = condition; + this.action = action; } + } - default: - break; + + public interface InputAction extends Runnable { } - } - private void _keyTyped(KeyEvent event) { - if (getControl().getEditable()) { + private Map<KeyCombo, List<Mapping>> comboMapping = new HashMap<>(); - String character = event.getCharacter(); - if (character.length() == 0) { - return; + /** + * maps a key combination to an action + * @param combo + * @param action + */ + public void mapKey(KeyCombo combo, InputAction action) { + List<Mapping> list = this.comboMapping.get(combo); + if (list == null) { + list = new ArrayList<>(); + this.comboMapping.put(combo, list); } + list.add(new SimpleMapping(()->true, action)); + } - // check the modifiers - // - OS-X: ALT+L ==> @ - // - win32/linux: ALTGR+Q ==> @ - if (event.isControlDown() || event.isAltDown() || (Util.isMacOS() && event.isMetaDown())) { - if (!((event.isControlDown() || Util.isMacOS()) && event.isAltDown())) - return; + /** + * maps a key combination to an action + * @param combo + * @param action + * @param condition + */ + public void mapKey(KeyCombo combo, InputAction action, Supplier<Boolean> condition) { + List<Mapping> list = this.comboMapping.get(combo); + if (list == null) { + list = new ArrayList<>(); + this.comboMapping.put(combo, list); } + list.add(new SimpleMapping(condition, action)); + } - if (character.charAt(0) > 31 // No ascii control chars - && character.charAt(0) != 127 // no delete key - && !event.isMetaDown()) { - final int offset = getControl().getCaretOffset(); - getControl().getContent().replaceTextRange(getControl().getCaretOffset(), 0, character); - getControl().setCaretOffset(offset + 1); + /** + * looks up the action for the specified combo. + * @param combo + * @return the input action + */ + public Optional<InputAction> get(KeyCombo combo) { + final List<Mapping> list = this.comboMapping.getOrDefault(combo, Collections.emptyList()); + return list.stream().filter(m->m.getCondition().get()).findFirst().map(m->m.getAction()); + } + + /** + * looks up the action for the specified key event + * @param event + * @return the input action + */ + public Optional<InputAction> get(KeyEvent event) { + return this.get(createFromEvent(event)); + } + + /** + * unmaps all keys + */ + public void clearKeyMappings() { + this.comboMapping.clear(); + } + + /** + * unmaps a key + * @param combo + */ + public void unmapKey(KeyCombo combo) { + this.comboMapping.remove(combo); + } + + public static KeyCombo createFromEvent(KeyEvent event) { + Set<MetaKey> metaKeys = new HashSet<>(); + if (event.isShiftDown()) metaKeys.add(ShiftKey); + if (event.isControlDown()) metaKeys.add(ControlKey); + if (event.isAltDown()) metaKeys.add(AltKey); + if (event.isMetaDown()) metaKeys.add(MetaKey); + + return new KeyCombo(event.getCode(), metaKeys.toArray(new MetaKey[] {})); + } + + } + + private class StyledTextInputAction implements KeyMapping.InputAction { + private final ActionType event; + private final Runnable fallback; + + private final Runnable action; + + public StyledTextInputAction(ActionType event, Runnable fallback) { + this.event = event; + this.fallback = fallback; + this.action = null; + } + + public StyledTextInputAction(Runnable action) { + this.event = null; + this.fallback = null; + this.action = action; + } + + + @Override + public void run() { + if (this.event != null) { + ActionEvent evt = new ActionEvent(getControl(), getControl(), this.event); + Event.fireEvent(getControl(), evt); + + if ( !evt.isConsumed() ) { + this.fallback.run(); + } + } + else { + this.action.run(); } } } /** - * Send a mouse pressed + * initializes the key mappings. * + * @param keyMapping the mapping + */ + protected void initKeymapping(KeyMapping keyMapping) { + + if (Util.isMacOS()) { + + keyMapping.mapKey(new KeyCombo(LEFT, ControlKey), this.ACTION_NAVIGATE_LINE_START); + keyMapping.mapKey(new KeyCombo(A, ControlKey), this.ACTION_NAVIGATE_LINE_START); + keyMapping.mapKey(new KeyCombo(RIGHT, ControlKey), this.ACTION_NAVIGATE_LINE_END); + keyMapping.mapKey(new KeyCombo(E, ControlKey), this.ACTION_NAVIGATE_LINE_END); + keyMapping.mapKey(new KeyCombo(UP, ControlKey), this.ACTION_NAVIGATE_TEXT_START); + keyMapping.mapKey(new KeyCombo(DOWN, ControlKey), this.ACTION_NAVIGATE_TEXT_END); + keyMapping.mapKey(new KeyCombo(RIGHT, AltKey), this.ACTION_NAVIGATE_WORD_NEXT); + keyMapping.mapKey(new KeyCombo(LEFT, AltKey), this.ACTION_NAVIGATE_WORD_PREVIOUS); + + keyMapping.mapKey(new KeyCombo(LEFT, ControlKey, ShiftKey), this.ACTION_SELECT_LINE_START); + keyMapping.mapKey(new KeyCombo(A, ControlKey, ShiftKey), this.ACTION_SELECT_LINE_START); + keyMapping.mapKey(new KeyCombo(RIGHT, ControlKey, ShiftKey), this.ACTION_SELECT_LINE_END); + keyMapping.mapKey(new KeyCombo(E, ControlKey, ShiftKey), this.ACTION_SELECT_LINE_END); + keyMapping.mapKey(new KeyCombo(UP, ControlKey, ShiftKey), this.ACTION_SELECT_TEXT_START); + keyMapping.mapKey(new KeyCombo(DOWN, ControlKey, ShiftKey), this.ACTION_SELECT_TEXT_END); + keyMapping.mapKey(new KeyCombo(RIGHT, AltKey, ShiftKey), this.ACTION_SELECT_WORD_NEXT); + keyMapping.mapKey(new KeyCombo(LEFT, AltKey, ShiftKey), this.ACTION_SELECT_WORD_PREVIOUS); + + keyMapping.mapKey(new KeyCombo(DELETE, AltKey), this.ACTION_DELETE_WORD_NEXT); + keyMapping.mapKey(new KeyCombo(BACK_SPACE, AltKey), this.ACTION_DELETE_WORD_PREVIOUS); + keyMapping.mapKey(new KeyCombo(D, MetaKey), this.ACTION_DELETE_LINE); + + keyMapping.mapKey(new KeyCombo(C, MetaKey), this.ACTION_COPY); + keyMapping.mapKey(new KeyCombo(V, MetaKey), this.ACTION_PASTE); + keyMapping.mapKey(new KeyCombo(X, MetaKey), this.ACTION_CUT); + + } + else { + keyMapping.mapKey(new KeyCombo(RIGHT, ControlKey), this.ACTION_NAVIGATE_WORD_NEXT); + keyMapping.mapKey(new KeyCombo(LEFT, ControlKey), this.ACTION_NAVIGATE_WORD_PREVIOUS); + + keyMapping.mapKey(new KeyCombo(RIGHT, ShiftKey, ControlKey), this.ACTION_SELECT_WORD_NEXT); + keyMapping.mapKey(new KeyCombo(LEFT, ShiftKey, ControlKey), this.ACTION_SELECT_WORD_PREVIOUS); + + keyMapping.mapKey(new KeyCombo(HOME), this.ACTION_NAVIGATE_LINE_START); + keyMapping.mapKey(new KeyCombo(HOME, ShiftKey), this.ACTION_SELECT_LINE_START); + keyMapping.mapKey(new KeyCombo(HOME, ControlKey), this.ACTION_NAVIGATE_TEXT_START); + keyMapping.mapKey(new KeyCombo(HOME, ControlKey, ShiftKey), this.ACTION_SELECT_TEXT_START); + + keyMapping.mapKey(new KeyCombo(END), this.ACTION_NAVIGATE_LINE_END); + keyMapping.mapKey(new KeyCombo(END, ShiftKey), this.ACTION_SELECT_LINE_END); + keyMapping.mapKey(new KeyCombo(END, ControlKey), this.ACTION_NAVIGATE_TEXT_END); + keyMapping.mapKey(new KeyCombo(END, ControlKey, ShiftKey), this.ACTION_SELECT_TEXT_END); + + keyMapping.mapKey(new KeyCombo(DELETE, ControlKey), this.ACTION_DELETE_WORD_NEXT); + keyMapping.mapKey(new KeyCombo(BACK_SPACE, ControlKey), this.ACTION_DELETE_WORD_PREVIOUS); + + keyMapping.mapKey(new KeyCombo(C, ControlKey), this.ACTION_COPY); + keyMapping.mapKey(new KeyCombo(V, ControlKey), this.ACTION_PASTE); + keyMapping.mapKey(new KeyCombo(X, ControlKey), this.ACTION_CUT); + + keyMapping.mapKey(new KeyCombo(A, ControlKey), this.ACTION_SELECT_ALL); + + keyMapping.mapKey(new KeyCombo(UP, ControlKey), this.ACTION_SCROLL_LINE_UP); + keyMapping.mapKey(new KeyCombo(DOWN, ControlKey), this.ACTION_SCROLL_LINE_DOWN); + + keyMapping.mapKey(new KeyCombo(D, ControlKey), this.ACTION_DELETE_LINE); + + keyMapping.mapKey(new KeyCombo(UP, AltKey), this.ACTION_MOVE_LINES_UP); + keyMapping.mapKey(new KeyCombo(DOWN, AltKey), this.ACTION_MOVE_LINES_DOWN); + + } + + keyMapping.mapKey(new KeyCombo(TAB), this.ACTION_INDENT, this::isMultilineSelection); + keyMapping.mapKey(new KeyCombo(TAB, ShiftKey), this.ACTION_OUTDENT); + + keyMapping.mapKey(new KeyCombo(DELETE), this.ACTION_DELETE); + keyMapping.mapKey(new KeyCombo(BACK_SPACE), this.ACTION_BACKSPACE); + + keyMapping.mapKey(new KeyCombo(ENTER), this.ACTION_NEW_LINE); + + keyMapping.mapKey(new KeyCombo(UP), this.ACTION_MOVE_UP); + keyMapping.mapKey(new KeyCombo(DOWN), this.ACTION_MOVE_DOWN); + keyMapping.mapKey(new KeyCombo(LEFT), this.ACTION_MOVE_LEFT); + keyMapping.mapKey(new KeyCombo(RIGHT), this.ACTION_MOVE_RIGHT); + + keyMapping.mapKey(new KeyCombo(UP, ShiftKey), this.ACTION_SELECT_UP); + keyMapping.mapKey(new KeyCombo(DOWN, ShiftKey), this.ACTION_SELECT_DOWN); + keyMapping.mapKey(new KeyCombo(LEFT, ShiftKey), this.ACTION_SELECT_LEFT); + keyMapping.mapKey(new KeyCombo(RIGHT, ShiftKey), this.ACTION_SELECT_RIGHT); + + + // action for insert tab support + keyMapping.mapKey(new KeyCombo(TAB), ()->getControl().insert("\t")); //$NON-NLS-1$ + } + + /** + * computes the text offset under the mouse cursor. * @param event - * the event - * @param visibleCells - * the visible cells - * @param selection - * are we in selection mode - * @return if the cursor update succeeded + * @return the offset */ - @SuppressWarnings("deprecation") - public boolean updateCursor(MouseEvent event, List<LineCell> visibleCells, boolean selection) { + protected int computeCursorOffset(MouseEvent event) { + List<LineCell> visibleCells = ((StyledTextSkin) getControl().getSkin()).getCurrentVisibleCells(); + LineCell lastCell = null; + int result = getControl().getContent().getCharCount(); + for (LineCell tmp : visibleCells) { Bounds boundsInParent = tmp.getBoundsInParent(); if (boundsInParent.getMinY() > event.getY()) { @@ -352,25 +1331,21 @@ public class StyledTextBehavior { if (n.localToScene(n.getBoundsInLocal()).contains(event.getSceneX(), event.getSceneY())) { int index = n.getCaretIndexAtPoint(n.sceneToLocal(event.getSceneX(), event.getSceneY())); if (index >= 0) { - getControl().impl_setCaretOffset(n.getStartOffset() + index, selection); - return true; + return n.getStartOffset() + index; } } - int offset = lastCell.getDomainElement().getLineOffset() + lastCell.getDomainElement().getLineLength(); - getControl().impl_setCaretOffset(offset, selection); + final double minX = n.localToScene(n.getBoundsInLocal()).getMinX(); + final double mouseX = event.getSceneX(); + final boolean left = minX >= mouseX; + result = lastCell.getDomainElement().getLineOffset() + (left ? 0 : lastCell.getDomainElement().getLineLength()); } break; } lastCell = tmp; } - getControl().requestFocus(); - Event.fireEvent(getControl(), event.copyFor(getControl(), getControl())); - return false; + return result; } - // public void mouseDragged(MouseEvent event, List<LineCell> visibleCells) { - // - // } } diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/skin/StyledTextSkin.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/skin/StyledTextSkin.java index d881a91af..587e5b164 100644 --- a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/skin/StyledTextSkin.java +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/skin/StyledTextSkin.java @@ -6,6 +6,7 @@ * http://www.eclipse.org/legal/epl-v10.html * * Contributors: + * Christoph Caks <ccaks@bestsolution.at> - improved editor behavior * Tom Schindl<tom.schindl@bestsolution.at> - initial API and implementation *******************************************************************************/ package org.eclipse.fx.ui.controls.styledtext.skin; @@ -36,7 +37,6 @@ import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import javafx.event.EventHandler; import javafx.geometry.Point2D; import javafx.geometry.Pos; import javafx.scene.Node; @@ -45,7 +45,6 @@ import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.Skin; import javafx.scene.control.SkinBase; -import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; @@ -104,7 +103,6 @@ public class StyledTextSkin extends SkinBase<StyledTextArea> { return new MyListViewSkin(this); } }; - styledText.addEventHandler(MouseEvent.MOUSE_PRESSED, behavior::mousePressed); initializeContentViewer(this.contentView); @@ -117,6 +115,22 @@ public class StyledTextSkin extends SkinBase<StyledTextArea> { this.rootContainer.getChildren().addAll(this.contentView); getChildren().addAll(this.rootContainer); +// styledText.getAnnotations().addListener(new SetChangeListener<StyledTextAnnotation>() { +// @Override +// public void onChanged(javafx.collections.SetChangeListener.Change<? extends StyledTextAnnotation> change) { +// if (change.getElementAdded() != null) { +// StyledTextAnnotation a = change.getElementAdded(); +// addAnnotation(a); +// System.err.println("removed " + a.getId() + " " + a.getType() + ": " + a.getText() + " @ " + a.getStartOffset() + ", " + a.getLength()); +// } +// if (change.getElementRemoved() != null) { +// StyledTextAnnotation a = change.getElementRemoved(); +// removeAnnotation(a); +// System.err.println("added " + a.getId() + " " + a.getType() + ": " + a.getText() + " @ " + a.getStartOffset() + ", " + a.getLength()); +// } +// } +// }); + styledText.caretOffsetProperty().addListener(new ChangeListener<Number>() { @Override @@ -179,6 +193,41 @@ public class StyledTextSkin extends SkinBase<StyledTextArea> { }); } +// private void addAnnotation(StyledTextAnnotation annotation) { +// int startOffset = annotation.getStartOffset(); +// int endOffset = annotation.getStartOffset() + annotation.getLength(); +// +// int startLineIndex = getSkinnable().getContent().getLineAtOffset(startOffset); +// int endLineIndex = getSkinnable().getContent().getLineAtOffset(endOffset); +// +// Set<Line> affectedLines = IntStream.range(startLineIndex, endLineIndex + 1).mapToObj(StyledTextSkin.this.lineList::get).collect(Collectors.toSet()); +// +// for (LineCell c : getCurrentVisibleCells()) { +// if (affectedLines.contains(c.domainElement)) { +// StyledTextLayoutContainer p = (StyledTextLayoutContainer) c.getGraphic(); +// p.getAnnotations().add(annotation); +// } +// } +// } +// +// private void removeAnnotation(StyledTextAnnotation annotation) { +// int startOffset = annotation.getStartOffset(); +// int endOffset = annotation.getStartOffset() + annotation.getLength(); +// +// int startLineIndex = getSkinnable().getContent().getLineAtOffset(startOffset); +// int endLineIndex = getSkinnable().getContent().getLineAtOffset(endOffset); +// +// Set<Line> affectedLines = IntStream.range(startLineIndex, endLineIndex + 1).mapToObj(StyledTextSkin.this.lineList::get).collect(Collectors.toSet()); +// +// for (LineCell c : getCurrentVisibleCells()) { +// if (affectedLines.contains(c.domainElement)) { +// StyledTextLayoutContainer p = (StyledTextLayoutContainer) c.getGraphic(); +// p.getAnnotations().remove(annotation); +// } +// } +// } + + StyledTextBehavior getBehavior() { return this.behavior; } @@ -217,15 +266,7 @@ public class StyledTextSkin extends SkinBase<StyledTextArea> { // this.contentView.setFixedCellSize(value); // this.contentView.setFixedCellSize(15); - contentView.setOnMousePressed( e -> getBehavior().handleContentMouseEvent(e, getCurrentVisibleCells())); - contentView.setOnMouseDragged(new EventHandler<MouseEvent>() { - - @Override - public void handle(MouseEvent event) { - getBehavior().updateCursor(event, getCurrentVisibleCells(), true); - event.consume(); - } - }); + getBehavior().installContentListeners(contentView); } private StyledTextLayoutContainer currentActiveNode; @@ -352,13 +393,29 @@ public class StyledTextSkin extends SkinBase<StyledTextArea> { } } - List<LineCell> getCurrentVisibleCells() { + public List<LineCell> getCurrentVisibleCells() { if (this.contentView == null || this.contentView.getSkin() == null) { return Collections.emptyList(); } return ((MyListViewSkin) this.contentView.getSkin()).getFlow().getCells(); } + public void scrollLineUp() { + MyListViewSkin s = ((MyListViewSkin) this.contentView.getSkin()); + + LineCell top = s.getFlow().getFirstVisibleCellWithinViewPort(); + + this.contentView.scrollTo(top.getIndex() - 1); + } + + public void scrollLineDown() { + MyListViewSkin s = ((MyListViewSkin) this.contentView.getSkin()); + + LineCell top = s.getFlow().getFirstVisibleCellWithinViewPort(); + + this.contentView.scrollTo(top.getIndex() + 1); + } + /** * A line cell */ |