Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to the tagging system #20

Merged
merged 2 commits into from
Jul 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/main/java/link/biosmarcel/baka/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ public void start(Stage stage) {
}
});

final Scene scene = new Scene(tabs, 800, 600);
final var popupPane = new PopupPane(tabs);
final Scene scene = new Scene(popupPane, 800, 600);
scene.getStylesheets().add(Objects.requireNonNull(Main.class.getResource("base.css")).toExternalForm());
stage.setTitle("Baka");
stage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("icon.png"))));
Expand Down
12 changes: 2 additions & 10 deletions src/main/java/link/biosmarcel/baka/data/Classification.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,11 @@ public class Classification {
/**
* Single-Tag so we can properly display a pie-chart later on. The tag will be treated case-insensitive.
*/
public String tag;
public String tag = "";

/**
* Amount, since one payment can make up multiple things. For example if you get 100€ at the bank, you
* could've spent 60€ on food and the other 40€ on weed.
*/
public BigDecimal amount;

public String getTag() {
return tag;
}

public BigDecimal getAmount() {
return amount;
}
public BigDecimal amount = BigDecimal.ZERO;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ public AutocompleteField(

@Override
Point2D computePopupLocation() {
final var textFieldBounds = input.getBoundsInParent();
final var textFieldBounds = input.localToScene(input.getBoundsInLocal());
final var caretBounds = ((TextFieldSkin) input.getSkin()).getCharacterBounds(input.getCaretPosition());
return new Point2D(textFieldBounds.getMinX() + caretBounds.getMinX(), textFieldBounds.getMaxY());
}

@Override
TextInputControl createInput() {
return new TextField();
Expand Down
67 changes: 23 additions & 44 deletions src/main/java/link/biosmarcel/baka/view/AutocompleteInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.TextInputControl;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import org.jspecify.annotations.Nullable;
Expand All @@ -21,33 +20,27 @@
import java.util.function.Function;

public abstract class AutocompleteInput {
private final Pane pane;
private final ListView<String> completionList;
private final Function<String, List<String>> autocompleteGenerator;
private final char[] tokenSeparators;
private boolean insertSpaceAfterCompletion = true;

protected final TextInputControl input;

public AutocompleteInput(
final char[] tokenSeparators,
final Function<String, List<String>> autocompleteGenerator) {
pane = new Pane();
input = createInput();
completionList = new ListView<>();
this.tokenSeparators = tokenSeparators;
this.autocompleteGenerator = autocompleteGenerator;

// This is what is the Z-Index on the web. It allows us to render our popup above everything else.
pane.getChildren().add(input);
pane.getChildren().add(completionList);
pane.maxHeightProperty().bind(input.heightProperty());
pane.maxWidthProperty().bind(input.widthProperty());
input = createInput();
completionList = new ListView<>();

completionList.setFixedCellSize(35.0);
completionList.setMinHeight(completionList.getFixedCellSize() + 2);
completionList.setMaxHeight(8 * completionList.getFixedCellSize() + 2);
completionList.setBackground(Background.fill(Color.WHITE));
completionList.setFocusTraversable(false);
completionList.setManaged(false);
completionList.setVisible(false);
completionList.setCellFactory(_ -> {
final ListCell<@Nullable String> cell = new ListCell<>() {
Expand Down Expand Up @@ -130,7 +123,10 @@ private void complete() {
}

final var completable = textBeforeCaret.substring(autocompleteTo + 1);
String textToInsert = selectedItem.substring(completable.length()) + " ";
String textToInsert = selectedItem.substring(completable.length());
if (insertSpaceAfterCompletion) {
textToInsert = textToInsert + " ";
}

if (input.getSelection().getLength() > 0) {
input.replaceSelection(textToInsert);
Expand Down Expand Up @@ -164,8 +160,11 @@ private void updatePopupItems(final Collection<String> newItems) {
}

private void hidePopup() {
toBack();
completionList.setVisible(false);
final Scene scene = completionList.getScene();
if (scene == null) {
return;
}
((PopupPane) scene.getRoot()).hidePopup();
}

private void refreshPopup() {
Expand All @@ -180,31 +179,18 @@ private void refreshPopup() {
return;
}


// +2 to prevent an unnecessary scrollbar
if (completionList.getSelectionModel().getSelectedIndex() == -1) {
completionList.getSelectionModel().select(0);
}

((PopupPane) input.getScene().getRoot()).showPopup(completionList);
completionList.setPrefHeight(completionList.getItems().size() * completionList.getFixedCellSize() + 2);

final var location = computePopupLocation();
completionList.setTranslateX(location.getX());
completionList.setTranslateY(location.getY());

var bounds = completionList.localToScene(completionList.getBoundsInLocal());
final var xOutOfBounds = bounds.getMaxX() - completionList.getScene().getWidth();
if (xOutOfBounds > 0) {
completionList.setTranslateX(completionList.getTranslateX() - xOutOfBounds);
}

// We are currently not treating y out of bounds since it is a bit more complicated, as the height needs to
// be dynamic. The width is currently static ... which is yet another issue.

// Since we are using a hacky way of rendering the popup, we need to make sure it doesn't render
// behind other components, as it is part of the Pane, but outside the pane bounds.
toFront();

completionList.setVisible(true);
completionList.setLayoutX(location.getX());
completionList.setLayoutY(location.getY());
}

public StringProperty textProperty() {
Expand All @@ -229,23 +215,16 @@ public void indicateError() {
input.getStyleClass().add("text-field-error");
}

private void toFront() {
recursivelySetViewOrder(completionList, -1);
}

private void toBack() {
recursivelySetViewOrder(completionList, 0);
public void setInsertSpaceAfterCompletion(final boolean insertSpaceAfterCompletion) {
this.insertSpaceAfterCompletion = insertSpaceAfterCompletion;
}

private void recursivelySetViewOrder(final Node node, final double viewOrder) {
node.setViewOrder(viewOrder);
if (node.getParent() != null) {
recursivelySetViewOrder(node.getParent(), viewOrder);
}
public void requestFocus() {
input.requestFocus();
}

public Region getNode() {
return pane;
return input;
}

abstract Point2D computePopupLocation();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Point2D computePopupLocation() {
// Required, as the bounds will be outdated otherwise.
input.layout();

final var textFieldBounds = input.getBoundsInParent();
final var textFieldBounds = input.localToScene(input.getBoundsInLocal());
final var bounds = ((TextAreaSkin) input.getSkin()).getCaretBounds();
return new Point2D(textFieldBounds.getMinX() + bounds.getMinX(), textFieldBounds.getMinY() + bounds.getMaxY());
}
Expand Down
145 changes: 145 additions & 0 deletions src/main/java/link/biosmarcel/baka/view/ClassificationCell.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package link.biosmarcel.baka.view;

import javafx.beans.property.ObjectProperty;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.layout.HBox;
import javafx.util.StringConverter;
import org.jspecify.annotations.Nullable;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.util.function.UnaryOperator;

public class ClassificationCell extends ListCell<@Nullable ClassificationFX> {
private static final DecimalFormat CURRENCY_FORMAT;

static {
CURRENCY_FORMAT = (DecimalFormat) DecimalFormat.getNumberInstance();
CURRENCY_FORMAT.setParseBigDecimal(true);
}

private static final StringConverter<BigDecimal> CONVERTER = new StringConverter<>() {
@Override
public @Nullable BigDecimal fromString(@Nullable String value) {
// If the specified value is null or zero-length, return null
if (value == null) {
return BigDecimal.ZERO;
}

value = value.strip();
if (value.isEmpty()) {
return BigDecimal.ZERO;
}

try {
return (BigDecimal) CURRENCY_FORMAT.parse(value);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}

@Override
public String toString(final @Nullable BigDecimal value) {
// If the specified value is null, return a zero-length String
if (value == null) {
return "";
}

return CURRENCY_FORMAT.format(value);
}
};

private static final UnaryOperator<TextFormatter.@Nullable Change> FILTER = change -> {
if (change == null || !change.isContentChange()) {
return change;
}

if (change.getText().chars().allMatch(value -> {
return Character.isDigit(value)
|| CURRENCY_FORMAT.getDecimalFormatSymbols().getDecimalSeparator() == value
|| CURRENCY_FORMAT.getDecimalFormatSymbols().getGroupingSeparator() == value
|| '-' == value;
})) {
return change;
}

return null;
};

private final Node renderer;
private final AutocompleteField tagField;
private final TextField amountField;
private @Nullable ClassificationFX lastItem;

public ClassificationCell(
final TagCompletion tagCompletion
) {
amountField = new TextField();

final TextFormatter<BigDecimal> formatter = new TextFormatter<>(CONVERTER, BigDecimal.ZERO, FILTER);
amountField.setTextFormatter(formatter);
amountField.textProperty().addListener((_, _, newValue) -> {
if (newValue.isEmpty()) {
return;
}

try {
amountField.getTextFormatter().getValueConverter().fromString(newValue);
amountField.getStyleClass().remove("text-field-error");
} catch (final Exception exception) {
if (!amountField.getStyleClass().contains("text-field-error")) {
amountField.getStyleClass().add("text-field-error");
}
}
});
amountField.focusedProperty().addListener((_, _, newValue) -> {
if (!newValue) {
amountField.commitValue();
}
});

tagField = new AutocompleteField(new char[]{' '}, tagCompletion::match);
tagField.setInsertSpaceAfterCompletion(false);
tagField.focusedProperty().addListener((_, _, newValue) -> {
if (!newValue) {
tagField.textProperty().set(tagField.textProperty().get().strip());
}
});

final var amountLabel = new Label("Amount:");
final var tagLabel = new Label("Tag:");
renderer = new HBox(5.0, tagLabel, tagField.getNode(), amountLabel, amountField);
amountLabel.setMaxHeight(Double.MAX_VALUE);
tagLabel.setMaxHeight(Double.MAX_VALUE);
setText(null);
}

@Override
protected void updateItem(final @Nullable ClassificationFX item, final boolean empty) {
super.updateItem(item, empty);

if (lastItem != null) {
((ObjectProperty<BigDecimal>) amountField.getTextFormatter().valueProperty()).unbindBidirectional(lastItem.amount);
tagField.textProperty().unbindBidirectional(lastItem.tag);
lastItem = null;
}

if (empty || item == null) {
setGraphic(null);
return;
}

((ObjectProperty<BigDecimal>) amountField.getTextFormatter().valueProperty()).bindBidirectional(item.amount);
tagField.textProperty().bindBidirectional(item.tag);

setGraphic(renderer);

lastItem = item;
}
}

28 changes: 28 additions & 0 deletions src/main/java/link/biosmarcel/baka/view/ClassificationFX.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package link.biosmarcel.baka.view;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import link.biosmarcel.baka.data.Classification;

import java.math.BigDecimal;

public class ClassificationFX {
public final Classification classification;

public final StringProperty tag = new SimpleStringProperty("");
public final ObjectProperty<BigDecimal> amount = new SimpleObjectProperty<>(BigDecimal.ZERO);

public ClassificationFX(final Classification classification) {
this.classification = classification;

tag.set(classification.tag);
amount.set(classification.amount);
}

public void apply() {
classification.amount = amount.get();
classification.tag = tag.get();
}
}
Loading
Loading