From 1cb54d0592428971cbb3ca637078dea6b3048a03 Mon Sep 17 00:00:00 2001 From: Natalia Cholewa Date: Fri, 28 Nov 2025 15:34:35 +0100 Subject: [PATCH] feat: recipe step controller, cell and fxml, add docs for everything, slightly refactor IngredientListCtrl --- .../scenes/recipe/IngredientListCell.java | 24 ++- .../scenes/recipe/IngredientListCtrl.java | 203 +++++++++++------- .../scenes/recipe/RecipeStepListCell.java | 96 +++++++++ .../scenes/recipe/RecipeStepListCtrl.java | 150 +++++++++++++ .../client/scenes/recipe/IngredientList.fxml | 2 +- .../client/scenes/recipe/RecipeStepList.fxml | 41 ++++ 6 files changed, 426 insertions(+), 90 deletions(-) create mode 100644 client/src/main/java/client/scenes/recipe/RecipeStepListCell.java create mode 100644 client/src/main/java/client/scenes/recipe/RecipeStepListCtrl.java create mode 100644 client/src/main/resources/client/scenes/recipe/RecipeStepList.fxml diff --git a/client/src/main/java/client/scenes/recipe/IngredientListCell.java b/client/src/main/java/client/scenes/recipe/IngredientListCell.java index a5d2992..ac317d8 100644 --- a/client/src/main/java/client/scenes/recipe/IngredientListCell.java +++ b/client/src/main/java/client/scenes/recipe/IngredientListCell.java @@ -2,8 +2,18 @@ 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 { + // TODO: change all this to use actual Ingredient objects + // and all that :) + @Override protected void updateItem(String item, boolean empty) { super.updateItem(item, empty); @@ -28,12 +38,8 @@ public class IngredientListCell extends ListCell { // Cancel edit on Escape key press textField.setOnKeyReleased(event -> { - switch (event.getCode()) { - case ESCAPE: - this.cancelEdit(); - break; - default: - break; + if (event.getCode() == KeyCode.ESCAPE) { + this.cancelEdit(); } }); @@ -41,6 +47,12 @@ public class IngredientListCell extends ListCell { 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(); } diff --git a/client/src/main/java/client/scenes/recipe/IngredientListCtrl.java b/client/src/main/java/client/scenes/recipe/IngredientListCtrl.java index c8bfa7b..74193e6 100644 --- a/client/src/main/java/client/scenes/recipe/IngredientListCtrl.java +++ b/client/src/main/java/client/scenes/recipe/IngredientListCtrl.java @@ -15,99 +15,136 @@ 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 { - private ObservableList ingredients; - private Function, Void> updateCallback; + /** + *

+ * The list of ingredients currently displayed. + *
+ * As the ingredient list in {@link Recipe} is immutable, + * this copies that list upon initialization to this mutable list. + *
+ * Please use the {@link #setUpdateCallback(Function)} function to listen for + * changes and update the recipe accordingly. + *

+ */ + private ObservableList ingredients; - @FXML ListView ingredientListView; + /** + * A callback function that is called when the ingredient list is updated. + */ + private Function, Void> updateCallback; - @FXML Button addIngredientButton; - @FXML Button deleteIngredientButton; + @FXML + ListView ingredientListView; - /** - * Set the recipe and refetch data from it. - * - * @param recipe The recipe to fetch data from. - */ - public void refetchFromRecipe(Recipe recipe) { - if (recipe == null) { - this.ingredients = FXCollections.observableArrayList(new ArrayList<>()); - } else { - List ingredientList = recipe.getIngredients(); - this.ingredients = FXCollections.observableArrayList(ingredientList); + @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 ingredientList = recipe.getIngredients(); + this.ingredients = FXCollections.observableArrayList(ingredientList); + } + + this.ingredientListView.setItems(this.ingredients); + this.refresh(); } - 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, Void> callback) { - this.updateCallback = callback; - } - - private void refresh() { ingredientListView.refresh(); } - - /** - * Handle ingredient addition. Automatically calls update callback. - */ - 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. - */ - private void handleIngredientEdit(EditEvent 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. - */ - 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; + /** + * Set a callback that's called when the ingredient list changes. + * + * @param callback The function to call upon each update. + */ + public void setUpdateCallback(Function, Void> callback) { + this.updateCallback = callback; } - this.ingredients.remove(selectedIndex); - this.refresh(); - this.updateCallback.apply(this.ingredients); - } + /** + * Refresh the ingredient list view. + */ + private void refresh() { + ingredientListView.refresh(); + } - @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) + /** + * 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); - this.ingredientListView.setEditable(true); - this.ingredientListView.setCellFactory( - list -> { return new IngredientListCell(); }); + var select = this.ingredientListView.getSelectionModel(); + select.select(this.ingredients.size() - 1); + } - this.ingredientListView.setOnEditCommit( - event -> handleIngredientEdit(event)); - this.addIngredientButton.setOnAction(event -> handleIngredientAdd(event)); - this.deleteIngredientButton.setOnAction( - event -> handleIngredientDelete(event)); + /** + * Handle ingredient edits. Automatically calls update callback. + * + * @param event The edit event. + */ + private void handleIngredientEdit(EditEvent event) { + int index = event.getIndex(); + String newValue = event.getNewValue(); - this.refresh(); - } + 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(); + } } diff --git a/client/src/main/java/client/scenes/recipe/RecipeStepListCell.java b/client/src/main/java/client/scenes/recipe/RecipeStepListCell.java new file mode 100644 index 0000000..6d6e992 --- /dev/null +++ b/client/src/main/java/client/scenes/recipe/RecipeStepListCell.java @@ -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 { + /** + * 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); + } +} diff --git a/client/src/main/java/client/scenes/recipe/RecipeStepListCtrl.java b/client/src/main/java/client/scenes/recipe/RecipeStepListCtrl.java new file mode 100644 index 0000000..7ed9c36 --- /dev/null +++ b/client/src/main/java/client/scenes/recipe/RecipeStepListCtrl.java @@ -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 { + /** + *

+ * The list of recipe steps currently displayed. + *
+ * As the step list in {@link Recipe} is immutable, + * this copies that list upon initialization to this mutable list. + *
+ * Please use the {@link #setUpdateCallback(Function)} function to listen for + * changes and update the recipe accordingly. + *

+ */ + private ObservableList steps; + + /** + * A callback function that is called when the step list is updated. + */ + private Function, Void> updateCallback; + + @FXML + ListView 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 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, 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 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(); + } +} diff --git a/client/src/main/resources/client/scenes/recipe/IngredientList.fxml b/client/src/main/resources/client/scenes/recipe/IngredientList.fxml index 1ad02e4..72ea2a9 100644 --- a/client/src/main/resources/client/scenes/recipe/IngredientList.fxml +++ b/client/src/main/resources/client/scenes/recipe/IngredientList.fxml @@ -9,7 +9,7 @@ - + diff --git a/client/src/main/resources/client/scenes/recipe/RecipeStepList.fxml b/client/src/main/resources/client/scenes/recipe/RecipeStepList.fxml new file mode 100644 index 0000000..1b5e936 --- /dev/null +++ b/client/src/main/resources/client/scenes/recipe/RecipeStepList.fxml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + +