Merge branch 'feature/ingredients-list' into 'main'
Ingredients and recipe steps list See merge request cse1105/2025-2026/teams/csep-team-76!7
This commit is contained in:
commit
5e49706977
6 changed files with 556 additions and 0 deletions
|
|
@ -0,0 +1,78 @@
|
|||
package client.scenes.recipe;
|
||||
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.input.KeyCode;
|
||||
|
||||
/**
|
||||
* A custom ListCell for displaying and editing ingredients in an
|
||||
* IngredientList. Allows inline editing of ingredient names.
|
||||
*
|
||||
* @see IngredientListCtrl
|
||||
*/
|
||||
public class IngredientListCell extends ListCell<String> {
|
||||
// TODO: change all this to use actual Ingredient objects
|
||||
// and all that :)
|
||||
|
||||
@Override
|
||||
protected void updateItem(String item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (empty || item == null) {
|
||||
this.setText(null);
|
||||
} else {
|
||||
this.setText(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startEdit() {
|
||||
super.startEdit();
|
||||
|
||||
TextField textField = new TextField(this.getItem());
|
||||
|
||||
// Commit edit on Enter key press
|
||||
textField.setOnAction(event -> {
|
||||
this.commitEdit(textField.getText());
|
||||
});
|
||||
|
||||
// Cancel edit on Escape key press
|
||||
textField.setOnKeyReleased(event -> {
|
||||
if (event.getCode() == KeyCode.ESCAPE) {
|
||||
this.cancelEdit();
|
||||
}
|
||||
});
|
||||
|
||||
this.setText(null);
|
||||
this.setGraphic(textField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the input is valid (non-empty).
|
||||
*
|
||||
* @param input The input string to validate.
|
||||
* @return true if valid, false otherwise.
|
||||
*/
|
||||
private boolean isInputValid(String input) {
|
||||
return input != null && !input.trim().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelEdit() {
|
||||
super.cancelEdit();
|
||||
|
||||
this.setText(this.getItem());
|
||||
this.setGraphic(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commitEdit(String newValue) {
|
||||
this.setGraphic(null);
|
||||
|
||||
if (!isInputValid(newValue)) {
|
||||
newValue = this.getItem(); // Revert to old value if input is invalid
|
||||
}
|
||||
|
||||
super.commitEdit(newValue);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
package client.scenes.recipe;
|
||||
|
||||
import commons.Recipe;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.function.Function;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.ListView.EditEvent;
|
||||
|
||||
/**
|
||||
* Controller for the ingredient list view in the recipe detail scene.
|
||||
* Manages displaying, adding, editing, and deleting ingredients.
|
||||
*
|
||||
* @see RecipeStepListCell The custom cell implementation.
|
||||
*/
|
||||
public class IngredientListCtrl implements Initializable {
|
||||
/**
|
||||
* <p>
|
||||
* The list of ingredients currently displayed.
|
||||
* <br>
|
||||
* As the ingredient list in {@link Recipe} is immutable,
|
||||
* this copies that list upon initialization to this mutable list.
|
||||
* <br>
|
||||
* Please use the {@link #setUpdateCallback(Function)} function to listen for
|
||||
* changes and update the recipe accordingly.
|
||||
* </p>
|
||||
*/
|
||||
private ObservableList<String> ingredients;
|
||||
|
||||
/**
|
||||
* A callback function that is called when the ingredient list is updated.
|
||||
*/
|
||||
private Function<List<String>, Void> updateCallback;
|
||||
|
||||
@FXML
|
||||
ListView<String> ingredientListView;
|
||||
|
||||
@FXML
|
||||
Button addIngredientButton;
|
||||
@FXML
|
||||
Button deleteIngredientButton;
|
||||
|
||||
/**
|
||||
* Set the recipe and refetch data from it.
|
||||
* This replaces the current ingredient list.
|
||||
* Only use this once per recipe when initializing the detail view.
|
||||
*
|
||||
* @param recipe The recipe to fetch data from.
|
||||
*/
|
||||
public void refetchFromRecipe(Recipe recipe) {
|
||||
if (recipe == null) {
|
||||
this.ingredients = FXCollections.observableArrayList(new ArrayList<>());
|
||||
} else {
|
||||
List<String> ingredientList = recipe.getIngredients();
|
||||
this.ingredients = FXCollections.observableArrayList(ingredientList);
|
||||
}
|
||||
|
||||
this.ingredientListView.setItems(this.ingredients);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a callback that's called when the ingredient list changes.
|
||||
*
|
||||
* @param callback The function to call upon each update.
|
||||
*/
|
||||
public void setUpdateCallback(Function<List<String>, Void> callback) {
|
||||
this.updateCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the ingredient list view.
|
||||
*/
|
||||
private void refresh() {
|
||||
ingredientListView.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle ingredient addition. Automatically calls update callback.
|
||||
*
|
||||
* @param event The action event.
|
||||
*/
|
||||
private void handleIngredientAdd(ActionEvent event) {
|
||||
this.ingredients.add("Ingredient " + (this.ingredients.size() + 1));
|
||||
this.refresh();
|
||||
this.updateCallback.apply(this.ingredients);
|
||||
|
||||
var select = this.ingredientListView.getSelectionModel();
|
||||
select.select(this.ingredients.size() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle ingredient edits. Automatically calls update callback.
|
||||
*
|
||||
* @param event The edit event.
|
||||
*/
|
||||
private void handleIngredientEdit(EditEvent<String> event) {
|
||||
int index = event.getIndex();
|
||||
String newValue = event.getNewValue();
|
||||
|
||||
this.ingredients.set(index, newValue);
|
||||
this.refresh();
|
||||
this.updateCallback.apply(this.ingredients);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle ingredient deletion. Automatically calls update callback.
|
||||
*
|
||||
* @param event The action event.
|
||||
*/
|
||||
private void handleIngredientDelete(ActionEvent event) {
|
||||
var select = this.ingredientListView.getSelectionModel();
|
||||
int selectedIndex = select.getSelectedIndex();
|
||||
// No index is selected, don't do anything
|
||||
if (selectedIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ingredients.remove(selectedIndex);
|
||||
this.refresh();
|
||||
this.updateCallback.apply(this.ingredients);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
// TODO: set up communication with the server
|
||||
// this would probably be best done with the callback (so this class doesn't
|
||||
// interact with the server at all)
|
||||
|
||||
this.ingredientListView.setEditable(true);
|
||||
this.ingredientListView.setCellFactory(
|
||||
list -> {
|
||||
return new RecipeStepListCell();
|
||||
});
|
||||
|
||||
this.ingredientListView.setOnEditCommit(this::handleIngredientEdit);
|
||||
this.addIngredientButton.setOnAction(this::handleIngredientAdd);
|
||||
this.deleteIngredientButton.setOnAction(this::handleIngredientDelete);
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package client.scenes.recipe;
|
||||
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.input.KeyCode;
|
||||
|
||||
/**
|
||||
* A custom ListCell for displaying and editing ingredients in an
|
||||
* RecipeStepList. Allows inline editing of ingredient names.
|
||||
*
|
||||
* @see RecipeStepListCtrl
|
||||
*/
|
||||
public class RecipeStepListCell extends ListCell<String> {
|
||||
/**
|
||||
* Get the display text for the given item, prefixed with its index.
|
||||
* Looks like "1. Step description".
|
||||
*
|
||||
* @param item The step description.
|
||||
* @return The display text.
|
||||
*/
|
||||
private String getDisplayText(String item) {
|
||||
return this.getIndex() + ". " + item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display text for the current item.
|
||||
* Looks like "1. Step description".
|
||||
*
|
||||
* @return The display text.
|
||||
*/
|
||||
private String getDisplayText() {
|
||||
return this.getDisplayText(this.getItem());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(String item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (empty || item == null) {
|
||||
this.setText(null);
|
||||
} else {
|
||||
this.setText(this.getDisplayText(item));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startEdit() {
|
||||
super.startEdit();
|
||||
|
||||
TextField textField = new TextField(this.getItem());
|
||||
|
||||
// Commit edit on Enter key press
|
||||
textField.setOnAction(event -> {
|
||||
this.commitEdit(textField.getText());
|
||||
});
|
||||
|
||||
// Cancel edit on Escape key press
|
||||
textField.setOnKeyReleased(event -> {
|
||||
if (event.getCode() == KeyCode.ESCAPE) {
|
||||
this.cancelEdit();
|
||||
}
|
||||
});
|
||||
|
||||
this.setText(null);
|
||||
this.setGraphic(textField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the input is valid (non-empty).
|
||||
*
|
||||
* @param input The input string to validate.
|
||||
* @return true if valid, false otherwise.
|
||||
*/
|
||||
private boolean isInputValid(String input) {
|
||||
return input != null && !input.trim().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelEdit() {
|
||||
super.cancelEdit();
|
||||
|
||||
this.setText(this.getDisplayText());
|
||||
this.setGraphic(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commitEdit(String newValue) {
|
||||
this.setGraphic(null);
|
||||
|
||||
if (!isInputValid(newValue)) {
|
||||
newValue = this.getItem(); // Revert to old value if input is invalid
|
||||
}
|
||||
|
||||
super.commitEdit(newValue);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
package client.scenes.recipe;
|
||||
|
||||
import commons.Recipe;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.function.Function;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.ListView.EditEvent;
|
||||
|
||||
/**
|
||||
* Controller for the step list view in the recipe detail scene.
|
||||
* Manages displaying, adding, editing, and deleting steps.
|
||||
*
|
||||
* @see RecipeStepListCell The custom cell implementation.
|
||||
*/
|
||||
public class RecipeStepListCtrl implements Initializable {
|
||||
/**
|
||||
* <p>
|
||||
* The list of recipe steps currently displayed.
|
||||
* <br>
|
||||
* As the step list in {@link Recipe} is immutable,
|
||||
* this copies that list upon initialization to this mutable list.
|
||||
* <br>
|
||||
* Please use the {@link #setUpdateCallback(Function)} function to listen for
|
||||
* changes and update the recipe accordingly.
|
||||
* </p>
|
||||
*/
|
||||
private ObservableList<String> steps;
|
||||
|
||||
/**
|
||||
* A callback function that is called when the step list is updated.
|
||||
*/
|
||||
private Function<List<String>, Void> updateCallback;
|
||||
|
||||
@FXML
|
||||
ListView<String> recipeStepListView;
|
||||
|
||||
@FXML
|
||||
Button addStepButton;
|
||||
@FXML
|
||||
Button deleteStepButton;
|
||||
|
||||
/**
|
||||
* Set the recipe and refetch data from it.
|
||||
* This replaces the current step list.
|
||||
* Only use this once per recipe when initializing the detail view.
|
||||
*
|
||||
* @param recipe The recipe to fetch data from.
|
||||
*/
|
||||
public void refetchFromRecipe(Recipe recipe) {
|
||||
if (recipe == null) {
|
||||
this.steps = FXCollections.observableArrayList(new ArrayList<>());
|
||||
} else {
|
||||
List<String> stepList = recipe.getPreparationSteps();
|
||||
this.steps = FXCollections.observableArrayList(stepList);
|
||||
}
|
||||
|
||||
this.recipeStepListView.setItems(this.steps);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a callback that's called when the step list changes.
|
||||
*
|
||||
* @param callback The function to call upon each update.
|
||||
*/
|
||||
public void setUpdateCallback(Function<List<String>, Void> callback) {
|
||||
this.updateCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the step list view.
|
||||
*/
|
||||
private void refresh() {
|
||||
recipeStepListView.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle step addition. Automatically calls update callback.
|
||||
*
|
||||
* @param event The action event.
|
||||
*/
|
||||
private void handleIngredientAdd(ActionEvent event) {
|
||||
this.steps.add("Step " + (this.steps.size() + 1));
|
||||
this.refresh();
|
||||
this.updateCallback.apply(this.steps);
|
||||
|
||||
var select = this.recipeStepListView.getSelectionModel();
|
||||
select.select(this.steps.size() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle step edits. Automatically calls update callback.
|
||||
*
|
||||
* @param event The edit event.
|
||||
*/
|
||||
private void handleIngredientEdit(EditEvent<String> event) {
|
||||
int index = event.getIndex();
|
||||
String newValue = event.getNewValue();
|
||||
|
||||
this.steps.set(index, newValue);
|
||||
this.refresh();
|
||||
this.updateCallback.apply(this.steps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle step deletion. Automatically calls update callback.
|
||||
*
|
||||
* @param event The action event.
|
||||
*/
|
||||
private void handleIngredientDelete(ActionEvent event) {
|
||||
var select = this.recipeStepListView.getSelectionModel();
|
||||
int selectedIndex = select.getSelectedIndex();
|
||||
// No index is selected, don't do anything
|
||||
if (selectedIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.steps.remove(selectedIndex);
|
||||
this.refresh();
|
||||
this.updateCallback.apply(this.steps);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
// TODO: set up communication with the server
|
||||
// this would probably be best done with the callback (so this class doesn't
|
||||
// interact with the server at all)
|
||||
|
||||
this.recipeStepListView.setEditable(true);
|
||||
this.recipeStepListView.setCellFactory(
|
||||
list -> {
|
||||
return new RecipeStepListCell();
|
||||
});
|
||||
|
||||
this.recipeStepListView.setOnEditCommit(this::handleIngredientEdit);
|
||||
this.addStepButton.setOnAction(this::handleIngredientAdd);
|
||||
this.deleteStepButton.setOnAction(this::handleIngredientDelete);
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ListView?>
|
||||
<?import javafx.scene.layout.AnchorPane?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<AnchorPane xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.scenes.recipe.RecipeStepListCtrl">
|
||||
<children>
|
||||
<VBox minHeight="200.0" minWidth="200.0">
|
||||
<children>
|
||||
<HBox alignment="CENTER_LEFT" spacing="20.0">
|
||||
<children>
|
||||
<Label text="Ingredients">
|
||||
<padding>
|
||||
<Insets bottom="5.0" top="5.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font name="System Bold" size="16.0" />
|
||||
</font>
|
||||
</Label>
|
||||
<HBox alignment="CENTER" spacing="10.0">
|
||||
<children>
|
||||
<Button fx:id="addIngredientButton" mnemonicParsing="false" text="Add" />
|
||||
<Button fx:id="deleteIngredientButton" mnemonicParsing="false" text="Delete" />
|
||||
</children>
|
||||
</HBox>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets left="10.0" right="10.0" />
|
||||
</padding>
|
||||
</HBox>
|
||||
<ListView fx:id="ingredientListView" prefHeight="200.0" prefWidth="200.0" />
|
||||
</children>
|
||||
</VBox>
|
||||
</children>
|
||||
</AnchorPane>
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ListView?>
|
||||
<?import javafx.scene.layout.AnchorPane?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<AnchorPane xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.scenes.recipe.RecipeStepListCtrl">
|
||||
<children>
|
||||
<VBox minHeight="200.0" minWidth="200.0">
|
||||
<children>
|
||||
<HBox alignment="CENTER_LEFT" spacing="20.0">
|
||||
<children>
|
||||
<Label text="Steps">
|
||||
<padding>
|
||||
<Insets bottom="5.0" top="5.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font name="System Bold" size="16.0" />
|
||||
</font>
|
||||
</Label>
|
||||
<HBox alignment="CENTER" spacing="10.0">
|
||||
<children>
|
||||
<Button fx:id="addStepButton" mnemonicParsing="false" text="Add" />
|
||||
<Button fx:id="deleteStepButton" mnemonicParsing="false" text="Delete" />
|
||||
</children>
|
||||
</HBox>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets left="10.0" right="10.0" />
|
||||
</padding>
|
||||
</HBox>
|
||||
<ListView fx:id="recipeStepListView" prefHeight="200.0" prefWidth="200.0" />
|
||||
</children>
|
||||
</VBox>
|
||||
</children>
|
||||
</AnchorPane>
|
||||
Loading…
Add table
Add a link
Reference in a new issue