feat(client/ingredients): make into dropdown

It is now possible to see a list of existing ingredients to choose from
in a dropdown menu.

Adds a server dependency to the ingredient list cell. Not ideal but
could be revamped in the future.
This commit is contained in:
Zhongheng Liu 2026-01-16 18:27:49 +01:00
commit f87b4e7d37
Signed by: steven
GPG key ID: F69B980899C1C09D
2 changed files with 94 additions and 11 deletions

View file

@ -1,17 +1,28 @@
package client.scenes.recipe; package client.scenes.recipe;
import client.utils.ServerUtils;
import com.google.inject.Inject;
import commons.FormalIngredient; import commons.FormalIngredient;
import commons.Ingredient; import commons.Ingredient;
import commons.RecipeIngredient; import commons.RecipeIngredient;
import commons.Unit; import commons.Unit;
import commons.VagueIngredient; import commons.VagueIngredient;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.scene.control.ChoiceBox; import javafx.scene.control.ChoiceBox;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.io.IOException;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.logging.Logger;
/** /**
@ -22,6 +33,13 @@ import java.util.Optional;
* @see IngredientListCtrl * @see IngredientListCtrl
*/ */
public class IngredientListCell extends OrderedEditableListCell<RecipeIngredient> { public class IngredientListCell extends OrderedEditableListCell<RecipeIngredient> {
private final ServerUtils server;
private final Logger logger = Logger.getLogger(IngredientListCell.class.getName());
private final SimpleObjectProperty<Ingredient> selectedIngredient = new SimpleObjectProperty<>();
@Inject
public IngredientListCell(ServerUtils server) {
this.server = server;
}
@Override @Override
public void commitEdit(RecipeIngredient newValue) { public void commitEdit(RecipeIngredient newValue) {
super.commitEdit(newValue); super.commitEdit(newValue);
@ -64,29 +82,89 @@ public class IngredientListCell extends OrderedEditableListCell<RecipeIngredient
this.setText(null); this.setText(null);
this.setGraphic(container); this.setGraphic(container);
} }
private <T> void makeMenuItems(
MenuButton menu,
Iterable<T> items,
Function<T, String> labelMapper,
Consumer<T> onSelect,
HBox container) {
MenuItem newIngredientItem = new MenuItem();
newIngredientItem.setText("Something new...");
newIngredientItem.setOnAction(_ -> {
menu.setText("New Ingredient");
selectedIngredient.setValue(null);
logger.info("Making new ingredient");
// TODO printError() integration
TextField newIngredientNameInput = makeNewIngredientNameField(selectedIngredient::set);
container.getChildren().add(newIngredientNameInput);
});
menu.getItems().addAll(newIngredientItem, new SeparatorMenuItem());
for (T item : items) {
MenuItem mi = new MenuItem();
mi.setText(labelMapper.apply(item));
mi.setOnAction(_ -> {
menu.setText(labelMapper.apply(item));
onSelect.accept(item);
});
menu.getItems().add(mi);
}
}
private HBox makeInputLine(RecipeIngredient ingredient, TextField amountInput, ChoiceBox<Unit> unitChoice) { private HBox makeInputLine(RecipeIngredient ingredient, TextField amountInput, ChoiceBox<Unit> unitChoice) {
TextField nameInput = new TextField(ingredient.ingredient.name); MenuButton ingredientSelect = new MenuButton("Select your ingredient");
HBox container = new HBox(amountInput, unitChoice, nameInput); HBox container = new HBox(amountInput, unitChoice, ingredientSelect);
try {
makeMenuItems(ingredientSelect, server.getIngredients(), Ingredient::getName, i -> {
selectedIngredient.set(i);
logger.info("Selected ingredient: " + i);
}, container);
} catch (IOException | InterruptedException e) {
logger.severe("Exception while making ingredient selection");
}
container.setOnKeyReleased(event -> { container.setOnKeyReleased(event -> {
if (event.getCode() != KeyCode.ENTER) { if (event.getCode() != KeyCode.ENTER) {
return; return;
} }
Unit unit = unitChoice.getValue(); Unit unit = unitChoice.getValue();
String name = nameInput.getText(); Ingredient i = selectedIngredient.getValue();
if (i == null) {
return; // The user is forced to kindly try again until something valid comes up.
}
if (unit == null || !unit.isFormal()) { if (unit == null || !unit.isFormal()) {
String desc = amountInput.getText(); String desc = amountInput.getText();
if (desc.isEmpty() || name.isEmpty()) { if (desc.isEmpty()) {
// TODO printError() integration logger.warning("Invalid description");
return; // The user is forced to kindly try again until something valid comes up. return; // The user is forced to kindly try again until something valid comes up.
} }
Ingredient newIngredient = new Ingredient(name, 0., 0., 0.); commitEdit(new VagueIngredient(i, desc));
commitEdit(new VagueIngredient(newIngredient, desc));
return; return;
} }
double amount = Double.parseDouble(amountInput.getText()); double amount = Double.parseDouble(amountInput.getText());
Ingredient newIngredient = new Ingredient(name, 0., 0., 0.); commitEdit(new FormalIngredient(i, amount, unit.suffix));
commitEdit(new FormalIngredient(newIngredient, amount, unit.suffix));
}); });
return container; return container;
} }
private @NonNull TextField makeNewIngredientNameField(Consumer<Ingredient> onSubmit) {
TextField newIngredientNameInput = new TextField("New ingredient...");
newIngredientNameInput.setOnKeyReleased(e -> {
if (e.getCode() != KeyCode.ENTER) {
return;
}
String newIngredientName = newIngredientNameInput.getText();
if (newIngredientName.isEmpty()) {
// printerr
logger.warning("invalid name received!");
return;
}
Ingredient newIngredient = new Ingredient(
newIngredientName,
0.,
0.,
0.);
onSubmit.accept(newIngredient);
});
return newIngredientNameInput;
}
} }

View file

@ -3,6 +3,7 @@ package client.scenes.recipe;
import client.utils.DefaultValueFactory; import client.utils.DefaultValueFactory;
import client.utils.LocaleAware; import client.utils.LocaleAware;
import client.utils.LocaleManager; import client.utils.LocaleManager;
import client.utils.ServerUtils;
import com.google.inject.Inject; import com.google.inject.Inject;
import commons.FormalIngredient; import commons.FormalIngredient;
import commons.Recipe; import commons.Recipe;
@ -28,8 +29,12 @@ import javafx.scene.control.ListView.EditEvent;
*/ */
public class IngredientListCtrl implements LocaleAware { public class IngredientListCtrl implements LocaleAware {
private final LocaleManager localeManager; private final LocaleManager localeManager;
private final ServerUtils server;
@Inject @Inject
public IngredientListCtrl(LocaleManager localeManager) { public IngredientListCtrl(
ServerUtils server,
LocaleManager localeManager) {
this.server = server;
this.localeManager = localeManager; this.localeManager = localeManager;
} }
@ -161,7 +166,7 @@ public class IngredientListCtrl implements LocaleAware {
this.ingredientListView.setEditable(true); this.ingredientListView.setEditable(true);
this.ingredientListView.setCellFactory( this.ingredientListView.setCellFactory(
list -> { list -> {
return new IngredientListCell(); return new IngredientListCell(this.server);
}); });
this.ingredientListView.setOnEditCommit(this::handleIngredientEdit); this.ingredientListView.setOnEditCommit(this::handleIngredientEdit);