Skip to content

Commit

Permalink
Implement popup window alignment with respect to caret or selection.
Browse files Browse the repository at this point in the history
Closes #36.
  • Loading branch information
TomasMikula committed Jun 1, 2014
1 parent 96d7cee commit ff8f222
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import javafx.stage.Stage;

import org.fxmisc.richtext.InlineCssTextArea;
import org.fxmisc.richtext.PopupAlignment;

public class PopupDemo extends Application {

Expand All @@ -24,7 +25,8 @@ public void start(Stage primaryStage) {
Popup popup = new Popup();
popup.getContent().add(new Button("I am a popup button!"));
area.setPopupWindow(popup);
area.setPopupWindowAnchorOffset(new Point2D(4, 0));
area.setPopupAlignment(PopupAlignment.SELECTION_BOTTOM_CENTER);
area.setPopupAnchorOffset(new Point2D(4, 4));

primaryStage.setScene(new Scene(new StackPane(area), 200, 200));
primaryStage.show();
Expand Down
54 changes: 54 additions & 0 deletions richtextfx/src/main/java/org/fxmisc/richtext/PopupAlignment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.fxmisc.richtext;

import static org.fxmisc.richtext.PopupAlignment.HorizontalAlignment.*;
import static org.fxmisc.richtext.PopupAlignment.AnchorObject.*;
import static org.fxmisc.richtext.PopupAlignment.VerticalAlignment.*;

public enum PopupAlignment {
CARET_TOP(CARET, TOP, H_CENTER),
CARET_CENTER(CARET, V_CENTER, H_CENTER),
CARET_BOTTOM(CARET, BOTTOM, H_CENTER),
SELECTION_TOP_LEFT(SELECTION, TOP, LEFT),
SELECTION_TOP_CENTER(SELECTION, TOP, H_CENTER),
SELECTION_TOP_RIGHT(SELECTION, TOP, RIGHT),
SELECTION_CENTER_LEFT(SELECTION, V_CENTER, LEFT),
SELECTION_CENTER(SELECTION, V_CENTER, H_CENTER),
SELECTION_CENTER_RIGHT(SELECTION, V_CENTER, RIGHT),
SELECTION_BOTTOM_LEFT(SELECTION, BOTTOM, LEFT),
SELECTION_BOTTOM_CENTER(SELECTION, BOTTOM, H_CENTER),
SELECTION_BOTTOM_RIGHT(SELECTION, BOTTOM, RIGHT);

public static enum AnchorObject {
CARET,
SELECTION,
}

public static enum VerticalAlignment {
TOP,
V_CENTER,
BOTTOM,
}

public static enum HorizontalAlignment {
LEFT,
H_CENTER,
RIGHT,
}

private AnchorObject anchor;
private VerticalAlignment vAlign;
private HorizontalAlignment hAlign;

private PopupAlignment(
AnchorObject anchor,
VerticalAlignment vAlign,
HorizontalAlignment hAlign) {
this.anchor = anchor;
this.vAlign = vAlign;
this.hAlign = hAlign;
}

public AnchorObject getAnchorObject() { return anchor; }
public VerticalAlignment getVerticalAlignment() { return vAlign; }
public HorizontalAlignment getHorizontalAlignment() { return hAlign; }
}
22 changes: 14 additions & 8 deletions richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.fxmisc.richtext;

import static org.fxmisc.richtext.PopupAlignment.*;
import static org.fxmisc.richtext.TwoDimensional.Bias.*;

import java.util.ArrayList;
Expand Down Expand Up @@ -133,15 +134,20 @@ public void setUndoManager(UndoManagerFactory undoManagerFactory) {
@Deprecated
public ObjectProperty<PopupWindow> popupAtCaretProperty() { return popupWindow; }

private final ObjectProperty<Point2D> popupWindowAnchorOffset = new SimpleObjectProperty<>();
public void setPopupWindowAnchorOffset(Point2D offset) { popupWindowAnchorOffset.set(offset); }
public Point2D getPopupWindowAnchorOffset() { return popupWindowAnchorOffset.get(); }
public ObjectProperty<Point2D> popupWindowAnchorOffsetProperty() { return popupWindowAnchorOffset; }
private final ObjectProperty<Point2D> popupAnchorOffset = new SimpleObjectProperty<>();
public void setPopupAnchorOffset(Point2D offset) { popupAnchorOffset.set(offset); }
public Point2D getPopupAnchorOffset() { return popupAnchorOffset.get(); }
public ObjectProperty<Point2D> popupAnchorOffsetProperty() { return popupAnchorOffset; }

private final ObjectProperty<UnaryOperator<Point2D>> popupWindowAnchorAdjustment = new SimpleObjectProperty<>();
public void setPopupWindowAnchorAdjustment(UnaryOperator<Point2D> f) { popupWindowAnchorAdjustment.set(f); }
public UnaryOperator<Point2D> getPopupWindowAnchorAdjustment() { return popupWindowAnchorAdjustment.get(); }
public ObjectProperty<UnaryOperator<Point2D>> popupWindowAnchorAdjustmentProperty() { return popupWindowAnchorAdjustment; }
private final ObjectProperty<UnaryOperator<Point2D>> popupAnchorAdjustment = new SimpleObjectProperty<>();
public void setPopupAnchorAdjustment(UnaryOperator<Point2D> f) { popupAnchorAdjustment.set(f); }
public UnaryOperator<Point2D> getPopupAnchorAdjustment() { return popupAnchorAdjustment.get(); }
public ObjectProperty<UnaryOperator<Point2D>> popupAnchorAdjustmentProperty() { return popupAnchorAdjustment; }

private final ObjectProperty<PopupAlignment> popupAlignment = new SimpleObjectProperty<>(CARET_TOP);
public void setPopupAlignment(PopupAlignment pos) { popupAlignment.set(pos); }
public PopupAlignment getPopupAlignment() { return popupAlignment.get(); }
public ObjectProperty<PopupAlignment> popupAlignmentProperty() { return popupAlignment; }


/**************************************************************************
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Optional;
import java.util.function.BiConsumer;

import javafx.beans.binding.Bindings;
Expand All @@ -39,7 +40,6 @@
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.geometry.VPos;
import javafx.scene.control.IndexRange;
import javafx.scene.paint.Color;
Expand Down Expand Up @@ -183,9 +183,18 @@ public double getCaretOffsetX() {
return (bounds.getMinX() + bounds.getMaxX()) / 2;
}

public Point2D getCaretLocationOnScreen() {
Bounds bounds = caretShape.getBoundsInLocal();
return caretShape.localToScreen(bounds.getMaxX(), bounds.getMinY());
public Bounds getCaretBoundsOnScreen() {
Bounds localBounds = caretShape.getBoundsInLocal();
return caretShape.localToScreen(localBounds);
}

public Optional<Bounds> getSelectionBoundsOnScreen() {
if(selection.getLength() == 0) {
return Optional.empty();
} else {
Bounds localBounds = selectionShape.getBoundsInLocal();
return Optional.of(selectionShape.localToScreen(localBounds));
}
}

public int getLineCount() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import java.util.function.IntSupplier;
import java.util.function.IntUnaryOperator;
import java.util.function.UnaryOperator;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
Expand All @@ -22,6 +24,8 @@
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableObjectProperty;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.control.IndexRange;
import javafx.scene.input.MouseDragEvent;
Expand All @@ -36,6 +40,7 @@
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.monadic.MonadicObservableValue;
import org.fxmisc.richtext.Paragraph;
import org.fxmisc.richtext.PopupAlignment;
import org.fxmisc.richtext.StyledTextArea;
import org.fxmisc.richtext.TwoDimensional.Position;
import org.fxmisc.richtext.TwoLevelNavigator;
Expand Down Expand Up @@ -140,12 +145,13 @@ public StyledTextAreaSkin(final StyledTextArea<S> styledTextArea, BiConsumer<Tex
// follow the caret every time the caret position or paragraphs change
EventStream<Void> caretPosDirty = invalidationsOf(styledTextArea.caretPositionProperty());
EventStream<Void> paragraphsDirty = invalidationsOf(listView.getItems());
EventStream<Void> caretDirty = merge(caretPosDirty, paragraphsDirty);
EventStream<Void> selectionDirty = invalidationsOf(styledTextArea.selectionProperty());
// need to reposition popup even when caret hasn't moved, but selection has changed (been deselected)
EventStream<Void> caretDirty = merge(caretPosDirty, paragraphsDirty, selectionDirty);
EventSource<Void> positionPopupImpulse = new EventSource<>();
subscribeTo(caretDirty.emitOn(areaDoneUpdating), x -> followCaret(() -> positionPopupImpulse.push(null)));

// update selection in paragraphs
EventStream<Void> selectionDirty = invalidationsOf(styledTextArea.selectionProperty());
subscribeTo(selectionDirty.emitOn(areaDoneUpdating), x -> {
IndexRange visibleRange = listView.getVisibleRange();
int startPar = visibleRange.getStart();
Expand Down Expand Up @@ -184,22 +190,23 @@ public StyledTextAreaSkin(final StyledTextArea<S> styledTextArea, BiConsumer<Tex
// Adjust popup anchor by either a user-provided function,
// or user-provided offset, or don't adjust at all.
MonadicObservableValue<UnaryOperator<Point2D>> userFunction =
EasyBind.monadic(styledTextArea.popupWindowAnchorAdjustmentProperty());
EasyBind.monadic(styledTextArea.popupAnchorAdjustmentProperty());
MonadicObservableValue<UnaryOperator<Point2D>> userOffset =
EasyBind.monadic(styledTextArea.popupWindowAnchorOffsetProperty())
EasyBind.monadic(styledTextArea.popupAnchorOffsetProperty())
.map(offset -> anchor -> anchor.add(offset));
ObservableValue<UnaryOperator<Point2D>> popupAnchorAdjustment = userFunction
.orElse(userOffset)
.orElse(UnaryOperator.identity());

// Position popup window whenever the window itself
// Position popup window whenever the window itself, its alignment,
// or the position adjustment function changes.
manageSubscription(EventStreams.combine(
EventStreams.valuesOf(styledTextArea.popupWindowProperty()),
EventStreams.valuesOf(styledTextArea.popupAlignmentProperty()),
EventStreams.valuesOf(popupAnchorAdjustment))
.repeatOn(positionPopupImpulse)
.filter((p, f) -> p != null)
.subscribe((p, f) -> positionPopup(p, f)));
.filter((w, al, adj) -> w != null)
.subscribe((w, al, adj) -> positionPopup(w, al, adj)));
}


Expand Down Expand Up @@ -355,18 +362,57 @@ private void followCaret(Runnable callback) {
});
}

private void positionPopup(PopupWindow popup, UnaryOperator<Point2D> adjustment) {
getCaretLocationOnScreen()
.map(adjustment)
.ifPresent(screenPos -> {
popup.setAnchorX(screenPos.getX());
popup.setAnchorY(screenPos.getY());
});
private void positionPopup(PopupWindow popup, PopupAlignment alignment, UnaryOperator<Point2D> adjustment) {
Optional<Bounds> bounds = null;
switch(alignment.getAnchorObject()) {
case CARET: bounds = getCaretBoundsOnScreen(); break;
case SELECTION: bounds = getSelectionBoundsOnScreen(); break;
}
bounds.ifPresent(b -> {
double x = 0, y = 0;
switch(alignment.getHorizontalAlignment()) {
case LEFT: x = b.getMinX(); break;
case H_CENTER: x = (b.getMinX() + b.getMaxX()) / 2; break;
case RIGHT: x = b.getMaxX(); break;
}
switch(alignment.getVerticalAlignment()) {
case TOP: y = b.getMinY();
case V_CENTER: y = (b.getMinY() + b.getMaxY()) / 2; break;
case BOTTOM: y = b.getMaxY(); break;
}
Point2D anchor = adjustment.apply(new Point2D(x, y));
popup.setAnchorX(anchor.getX());
popup.setAnchorY(anchor.getY());
});
}

private Optional<Point2D> getCaretLocationOnScreen() {
private Optional<Bounds> getCaretBoundsOnScreen() {
return listView.getVisibleCell(getSkinnable().getCurrentParagraph())
.map(cell -> cell.getParagraphGraphic().getCaretLocationOnScreen());
.map(cell -> cell.getParagraphGraphic().getCaretBoundsOnScreen());
}

private Optional<Bounds> getSelectionBoundsOnScreen() {
IndexRange selection = getSkinnable().getSelection();
if(selection.getLength() == 0) {
return getCaretBoundsOnScreen();
}

IndexRange visibleRange = listView.getVisibleRange();
Bounds[] bounds = IntStream.range(visibleRange.getStart(), visibleRange.getEnd())
.<Optional<Bounds>>mapToObj(i -> listView.getVisibleCell(i)
.flatMap(cell -> cell.getParagraphGraphic().getSelectionBoundsOnScreen()))
.filter(opt -> opt.isPresent())
.map(opt -> opt.get())
.toArray(n -> new Bounds[n]);

if(bounds.length == 0) {
return Optional.empty();
}
double minX = Stream.of(bounds).mapToDouble(Bounds::getMinX).min().getAsDouble();
double maxX = Stream.of(bounds).mapToDouble(Bounds::getMaxX).max().getAsDouble();
double minY = Stream.of(bounds).mapToDouble(Bounds::getMinY).min().getAsDouble();
double maxY = Stream.of(bounds).mapToDouble(Bounds::getMaxY).max().getAsDouble();
return Optional.of(new BoundingBox(minX, minY, maxX-minX, maxY-minY));
}

private void listenTo(Observable observable, InvalidationListener listener) {
Expand Down

0 comments on commit ff8f222

Please sign in to comment.