feat: recipe step controller, cell and fxml, add docs for everything, slightly refactor IngredientListCtrl

This commit is contained in:
Natalia Cholewa 2025-11-28 15:34:35 +01:00
commit 1cb54d0592
6 changed files with 426 additions and 90 deletions

View file

@ -2,8 +2,18 @@ package client.scenes.recipe;
import javafx.scene.control.ListCell; import javafx.scene.control.ListCell;
import javafx.scene.control.TextField; 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> { public class IngredientListCell extends ListCell<String> {
// TODO: change all this to use actual Ingredient objects
// and all that :)
@Override @Override
protected void updateItem(String item, boolean empty) { protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty); super.updateItem(item, empty);
@ -28,12 +38,8 @@ public class IngredientListCell extends ListCell<String> {
// Cancel edit on Escape key press // Cancel edit on Escape key press
textField.setOnKeyReleased(event -> { textField.setOnKeyReleased(event -> {
switch (event.getCode()) { if (event.getCode() == KeyCode.ESCAPE) {
case ESCAPE:
this.cancelEdit(); this.cancelEdit();
break;
default:
break;
} }
}); });
@ -41,6 +47,12 @@ public class IngredientListCell extends ListCell<String> {
this.setGraphic(textField); 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) { private boolean isInputValid(String input) {
return input != null && !input.trim().isEmpty(); return input != null && !input.trim().isEmpty();
} }

View file

@ -15,17 +15,43 @@ import javafx.scene.control.Button;
import javafx.scene.control.ListView; import javafx.scene.control.ListView;
import javafx.scene.control.ListView.EditEvent; 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 { 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; private ObservableList<String> ingredients;
/**
* A callback function that is called when the ingredient list is updated.
*/
private Function<List<String>, Void> updateCallback; private Function<List<String>, Void> updateCallback;
@FXML ListView<String> ingredientListView; @FXML
ListView<String> ingredientListView;
@FXML Button addIngredientButton; @FXML
@FXML Button deleteIngredientButton; Button addIngredientButton;
@FXML
Button deleteIngredientButton;
/** /**
* Set the recipe and refetch data from it. * 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. * @param recipe The recipe to fetch data from.
*/ */
@ -50,10 +76,17 @@ public class IngredientListCtrl implements Initializable {
this.updateCallback = callback; this.updateCallback = callback;
} }
private void refresh() { ingredientListView.refresh(); } /**
* Refresh the ingredient list view.
*/
private void refresh() {
ingredientListView.refresh();
}
/** /**
* Handle ingredient addition. Automatically calls update callback. * Handle ingredient addition. Automatically calls update callback.
*
* @param event The action event.
*/ */
private void handleIngredientAdd(ActionEvent event) { private void handleIngredientAdd(ActionEvent event) {
this.ingredients.add("Ingredient " + (this.ingredients.size() + 1)); this.ingredients.add("Ingredient " + (this.ingredients.size() + 1));
@ -66,6 +99,8 @@ public class IngredientListCtrl implements Initializable {
/** /**
* Handle ingredient edits. Automatically calls update callback. * Handle ingredient edits. Automatically calls update callback.
*
* @param event The edit event.
*/ */
private void handleIngredientEdit(EditEvent<String> event) { private void handleIngredientEdit(EditEvent<String> event) {
int index = event.getIndex(); int index = event.getIndex();
@ -78,6 +113,8 @@ public class IngredientListCtrl implements Initializable {
/** /**
* Handle ingredient deletion. Automatically calls update callback. * Handle ingredient deletion. Automatically calls update callback.
*
* @param event The action event.
*/ */
private void handleIngredientDelete(ActionEvent event) { private void handleIngredientDelete(ActionEvent event) {
var select = this.ingredientListView.getSelectionModel(); var select = this.ingredientListView.getSelectionModel();
@ -100,13 +137,13 @@ public class IngredientListCtrl implements Initializable {
this.ingredientListView.setEditable(true); this.ingredientListView.setEditable(true);
this.ingredientListView.setCellFactory( this.ingredientListView.setCellFactory(
list -> { return new IngredientListCell(); }); list -> {
return new RecipeStepListCell();
});
this.ingredientListView.setOnEditCommit( this.ingredientListView.setOnEditCommit(this::handleIngredientEdit);
event -> handleIngredientEdit(event)); this.addIngredientButton.setOnAction(this::handleIngredientAdd);
this.addIngredientButton.setOnAction(event -> handleIngredientAdd(event)); this.deleteIngredientButton.setOnAction(this::handleIngredientDelete);
this.deleteIngredientButton.setOnAction(
event -> handleIngredientDelete(event));
this.refresh(); this.refresh();
} }

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -9,7 +9,7 @@
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?> <?import javafx.scene.text.Font?>
<AnchorPane xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.scenes.recipe.IngredientListCtrl"> <AnchorPane xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.scenes.recipe.RecipeStepListCtrl">
<children> <children>
<VBox minHeight="200.0" minWidth="200.0"> <VBox minHeight="200.0" minWidth="200.0">
<children> <children>

View file

@ -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>