package mi.hdm.controllers; import javafx.fxml.FXML; import javafx.scene.control.*; import javafx.scene.image.ImageView; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import mi.hdm.components.CategoryCheckBox; import mi.hdm.components.CategoryPreviewLabel; import mi.hdm.components.IngredientSearchResultLabel; import mi.hdm.components.SelectedIngredientLabel; import mi.hdm.exceptions.InvalidRecipeException; import mi.hdm.helpers.Validation; import mi.hdm.recipes.*; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.IntStream; import static mi.hdm.helpers.Validation.isInteger; public class RecipeEditorController extends BaseController { private static final int ELEMENTS_PER_SEARCH_PAGE = 100; private Recipe recipe; private final RecipeManager recipeManager; private final CategoryManager categoryManager; private final IngredientManager ingredientManager; private final RecipeSearch recipeSearch; private List<RecipeComponent> searchResults; private final List<Category> selectedCategories; private final List<RecipeComponent> selectedIngredients; private List<SelectedIngredientLabel> selectedIngredientLabels; private int currentPage = 0; private int maxPages = 0; private static final Logger log = LogManager.getLogger(RecipeEditorController.class); @FXML private TextField nameTextField; @FXML private TextArea descriptionTextArea; @FXML private TextField prepTimeTextField; @FXML private TextField imagePathTextField; @FXML private HBox categories; @FXML private FlowPane allCategories; @FXML private TextField ingredientSearch; @FXML private ScrollPane searchResultsPane; @FXML private TextArea preparationTextArea; @FXML private ScrollPane selectedIngredientsScrollPane; private final VBox selectedIngredientsVbox; public RecipeEditorController(Recipe recipe, RecipeManager recipeManager, IngredientManager ingredientManager, CategoryManager categoryManager) { this.recipe = recipe; this.recipeManager = recipeManager; this.categoryManager = categoryManager; this.ingredientManager = ingredientManager; selectedCategories = new ArrayList<>(categoryManager.getCategoriesFromKeys(recipe.getCategoryCodes())); log.info("Added categories to selectedCategories: {}", selectedCategories); searchResults = new ArrayList<>(); selectedIngredients = new ArrayList<>(); ingredientManager.getIngredientsFromKeys(recipe.getIngredients()).forEach((i, amount) -> { if (i instanceof Ingredient) { selectedIngredients.add(i); } else { log.error("No ingredient with code {}", i.getUniqueCode()); } }); recipeSearch = new RecipeSearch(recipeManager, ingredientManager); selectedIngredientsVbox = new VBox(); selectedIngredientLabels = new ArrayList<>(); } @FXML public void initialize() { mapCategories(); displayRecipe(); ingredientSearch.textProperty().addListener(((e) -> searchIngredients())); //adds onChange listener to the search field selectedIngredientsScrollPane.setContent(selectedIngredientsVbox); } private void displayRecipe() { nameTextField.setText(recipe.getName()); descriptionTextArea.setText(recipe.getDescription()); recipe.getIngredients().forEach((k, v) -> { HBox ingredientHBox = new HBox(); Button deleteIngredientButton = new Button("X"); deleteIngredientButton.setStyle("-fx-text-fill: d91c1c;" + "-fx-font-size: 12;" + "-fx-background-size: small;"); deleteIngredientButton.setOnAction(h -> { selectedIngredients.remove(ingredientManager.getIngredient(k)); log.debug("User deleted ingredient '{}' from recipe.", ingredientManager.getIngredient(k).get().getName()); selectedIngredientsVbox.getChildren().remove(ingredientHBox); }); SelectedIngredientLabel label = new SelectedIngredientLabel(ingredientManager.getIngredient(k).get(), v); selectedIngredientLabels.add(label); ingredientHBox.getChildren().addAll(label, deleteIngredientButton); selectedIngredientsVbox.getChildren().add(ingredientHBox); }); preparationTextArea.setText(String.join("\n", recipe.getPreparation())); prepTimeTextField.setText(String.valueOf(recipe.getPreparationTimeMins())); imagePathTextField.setText(recipe.getImageURL().toString()); } private void mapCategories() { for (final Category category : categoryManager.getAllCategories()) { CategoryCheckBox checkbox = new CategoryCheckBox(category); if(selectedCategories.contains(category)) { checkbox.setSelected(true); drawSelectedCategories(); } checkbox.setOnAction(e -> updateSelectedCategories(checkbox)); allCategories.getChildren().add(checkbox); } } private void updateSelectedCategories(CategoryCheckBox checkbox) { if (checkbox.isSelected()) { selectedCategories.add(checkbox.getAssociatedCategory()); log.debug("Added '{}' to list of selected categories.", checkbox.getAssociatedCategory().getName()); } else { selectedCategories.remove(checkbox.getAssociatedCategory()); log.debug("Removed '{}' from list of selected categories.", checkbox.getAssociatedCategory().getName()); } drawSelectedCategories(); } private void drawSelectedCategories() { log.debug("Drawing selected categories"); categories.getChildren().clear(); for (final Category category : selectedCategories) { CategoryPreviewLabel label = new CategoryPreviewLabel(category); categories.getChildren().add(label); } } @FXML public void searchIngredients() { String input = ingredientSearch.getText(); log.debug("Input in search field changed, searching through recipe components with query '{}'.", input); searchResults = recipeSearch.searchThroughNames(input); currentPage = 0; maxPages = searchResults.size() / ELEMENTS_PER_SEARCH_PAGE; drawIngredientSearchResults(); } private void drawIngredientSearchResults() { log.debug("Drawing search results"); VBox resultContainer = new VBox(); int start = currentPage * ELEMENTS_PER_SEARCH_PAGE; int end = Math.min(searchResults.size(), start + ELEMENTS_PER_SEARCH_PAGE); IntStream.range(start, end) .mapToObj(searchResults::get) .forEach( result -> { IngredientSearchResultLabel resultLabel = new IngredientSearchResultLabel(result); resultLabel.setOnMouseClicked(e -> { log.debug("User added ingredient '{}' to recipe.", result.getName()); if (!selectedIngredients.contains(result)) { selectedIngredients.add(result); HBox ingredientHBox = new HBox(); Button deleteIngredientButton = new Button("X"); deleteIngredientButton.setStyle("-fx-text-fill: d91c1c;" + "-fx-font-size: 12;" + "-fx-background-size: small;"); deleteIngredientButton.setOnAction(h -> { selectedIngredients.remove(result); log.debug("User deleted ingredient '{}' from recipe.", result.getName()); selectedIngredientsVbox.getChildren().remove(ingredientHBox); }); SelectedIngredientLabel label = new SelectedIngredientLabel(result); selectedIngredientLabels.add(label); ingredientHBox.getChildren().addAll(label, deleteIngredientButton); selectedIngredientsVbox.getChildren().add(ingredientHBox); } }); resultContainer.getChildren().add(resultLabel); } ); searchResultsPane.setContent(resultContainer); } @FXML public void confirmEditRecipe() { log.debug("User confirmed recipe editing."); recipe.setName(nameTextField.getText()); Map<RecipeComponent, Integer> ingredients = new HashMap<>(); selectedIngredientLabels.forEach(label -> { ingredients.put(label.getComponent(), label.getAmount()); }); try { recipe.setIngredientFromRecipeComponents(ingredients); recipe.setDescription(descriptionTextArea.getText()); recipe.setPreparation(List.of(preparationTextArea.getText().split("\n"))); recipe.setCategoriesFromObjects(List.copyOf(selectedCategories)); String prepTime = prepTimeTextField.getText(); recipe.setPreparationTimeMins( isInteger(prepTime) ? Integer.parseInt(prepTime) : null); if (Files.exists(Paths.get(new URI(imagePathTextField.getText())))) { recipe.setImage(new URL(imagePathTextField.getText())); } log.info("Recipe '{}' was edited.", recipe.getName()); changeSceneToRecipe(); } catch (InvalidRecipeException e) { Alert a = new Alert(Alert.AlertType.ERROR); a.setHeaderText("Error creating recipe"); a.setContentText(e.getMessage()); a.show(); log.error(e.getMessage()); log.error("Recipe not created."); } catch (RuntimeException e) { Alert a = new Alert(Alert.AlertType.ERROR); a.setHeaderText("Error creating recipe"); a.setContentText("Something went wrong when creating the recipe. Check all your inputs!"); a.show(); log.error(e.getMessage()); log.error("Recipe not created."); } catch (MalformedURLException | URISyntaxException e) { recipe.setImage(Recipe.class.getResource("/images/dish-fork-and-knife.png")); log.error("Invalid image path, loaded default image."); log.info("Recipe '{}' was created.", recipe.getName()); changeScene(View.RECIPE_VIEW, recipe); } } @FXML public void incrementPageCounter() { //increment the current page and re-render currentPage = Math.min(++currentPage, maxPages); log.debug("User selected page {} / {}", currentPage, maxPages); drawIngredientSearchResults(); } @FXML public void decrementPageCounter() { //decrement the current page and re-render currentPage = Math.max(0, --currentPage); log.debug("User selected page {} / {}", currentPage, maxPages); drawIngredientSearchResults(); } @FXML public void changeSceneToRecipe() { changeScene(View.RECIPE_VIEW, recipe); } }