diff --git a/client/src/main/java/client/scenes/recipe/IngredientListCell.java b/client/src/main/java/client/scenes/recipe/IngredientListCell.java new file mode 100644 index 0000000..ac317d8 --- /dev/null +++ b/client/src/main/java/client/scenes/recipe/IngredientListCell.java @@ -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 { + // 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); + } +} diff --git a/client/src/main/java/client/scenes/recipe/IngredientListCtrl.java b/client/src/main/java/client/scenes/recipe/IngredientListCtrl.java new file mode 100644 index 0000000..74193e6 --- /dev/null +++ b/client/src/main/java/client/scenes/recipe/IngredientListCtrl.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 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 { + /** + *

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