feat(client/nutrition): updated nutrition UI modelling
Uses "idiomatic JavaFX" to model automatic update propagation by object view models.
This commit is contained in:
parent
b1bb4ad242
commit
1bfc103ecc
5 changed files with 173 additions and 19 deletions
|
|
@ -0,0 +1,71 @@
|
|||
package client.Ingredient;
|
||||
|
||||
import commons.Ingredient;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.DoubleBinding;
|
||||
import javafx.beans.property.*;
|
||||
|
||||
public class IngredientViewModel {
|
||||
private final LongProperty id = new SimpleLongProperty();
|
||||
private final StringProperty name = new SimpleStringProperty();
|
||||
private final DoubleProperty protein = new SimpleDoubleProperty();
|
||||
private final DoubleProperty fat = new SimpleDoubleProperty();
|
||||
private final DoubleProperty carbs = new SimpleDoubleProperty();
|
||||
|
||||
private final ReadOnlyDoubleWrapper kcal = new ReadOnlyDoubleWrapper();
|
||||
public IngredientViewModel() {
|
||||
DoubleBinding computeKcal = Bindings.createDoubleBinding(
|
||||
() -> toIngredient().kcalPer100g(),
|
||||
protein, carbs, fat
|
||||
);
|
||||
|
||||
// bind the read-only wrapper to the binding
|
||||
kcal.bind(computeKcal);
|
||||
|
||||
}
|
||||
|
||||
public IngredientViewModel(Ingredient ing) {
|
||||
updateFrom(ing);
|
||||
}
|
||||
|
||||
public void updateFrom(Ingredient ing) {
|
||||
if (ing == null) return;
|
||||
id.set(ing.getId());
|
||||
name.set(ing.getName());
|
||||
protein.set(ing.getProteinPer100g());
|
||||
fat.set(ing.getFatPer100g());
|
||||
carbs.set(ing.getCarbsPer100g());
|
||||
}
|
||||
|
||||
// property getters
|
||||
public LongProperty idProperty() {
|
||||
return id;
|
||||
}
|
||||
public StringProperty nameProperty() {
|
||||
return name;
|
||||
}
|
||||
public DoubleProperty proteinProperty() {
|
||||
return protein;
|
||||
}
|
||||
public DoubleProperty fatProperty() {
|
||||
return fat;
|
||||
}
|
||||
public DoubleProperty carbsProperty() {
|
||||
return carbs;
|
||||
}
|
||||
|
||||
public Ingredient toIngredient() {
|
||||
Ingredient i = new Ingredient();
|
||||
i.setId(id.get());
|
||||
i.setName(name.get());
|
||||
i.setProteinPer100g(protein.get());
|
||||
i.setFatPer100g(fat.get());
|
||||
i.setCarbsPer100g(carbs.get());
|
||||
return i;
|
||||
}
|
||||
public ReadOnlyDoubleProperty kcalProperty() {
|
||||
return kcal.getReadOnlyProperty(); }
|
||||
public double getKcal() {
|
||||
return kcal.get(); }
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package client.exception;
|
||||
|
||||
public class IllegalInputFormatException extends RuntimeException {
|
||||
public IllegalInputFormatException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import javafx.stage.Stage;
|
|||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
//TODO and check for capital letter milk and MILK are seen as different
|
||||
|
||||
|
|
@ -21,6 +22,9 @@ import java.util.Optional;
|
|||
public class IngredientListCtrl {
|
||||
|
||||
private final ServerUtils server;
|
||||
private final Logger logger = Logger.getLogger(IngredientListCtrl.class.getName());
|
||||
@FXML
|
||||
private ListView<Ingredient> ingredientListView;
|
||||
@FXML
|
||||
private NutritionDetailsCtrl nutritionDetailsCtrl;
|
||||
@FXML
|
||||
|
|
@ -52,6 +56,7 @@ public class IngredientListCtrl {
|
|||
if (newValue == null) {
|
||||
return;
|
||||
}
|
||||
logger.info("Selected ingredient " + newValue.getName() + ", propagating to Nutrition Details controller...");
|
||||
nutritionDetailsCtrl.setItem(newValue);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,43 @@
|
|||
package client.scenes.nutrition;
|
||||
|
||||
import client.Ingredient.IngredientViewModel;
|
||||
import client.exception.IllegalInputFormatException;
|
||||
import client.utils.LocaleAware;
|
||||
import client.utils.LocaleManager;
|
||||
import client.utils.ServerUtils;
|
||||
import com.google.inject.Inject;
|
||||
import commons.Ingredient;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventType;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.converter.NumberStringConverter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.IllegalFormatConversionException;
|
||||
import java.util.IllegalFormatException;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class NutritionDetailsCtrl implements LocaleAware {
|
||||
@FXML
|
||||
public GridPane nutritionValueContainer;
|
||||
|
||||
private boolean visible;
|
||||
private final LocaleManager manager;
|
||||
private Ingredient ingredient;
|
||||
private final ServerUtils server;
|
||||
private final SimpleObjectProperty<IngredientViewModel> ingredient =
|
||||
new SimpleObjectProperty<>(new IngredientViewModel());
|
||||
private final Logger logger = Logger.getLogger(this.getClass().getName());
|
||||
|
||||
public Label ingredientName;
|
||||
public Label fatInputLabel;
|
||||
|
|
@ -30,10 +55,45 @@ public class NutritionDetailsCtrl implements LocaleAware {
|
|||
|
||||
@Inject
|
||||
public NutritionDetailsCtrl(
|
||||
LocaleManager manager
|
||||
LocaleManager manager,
|
||||
ServerUtils server
|
||||
) {
|
||||
this.manager = manager;
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initializeComponents() {
|
||||
Platform.runLater(() -> {
|
||||
IngredientViewModel vm = this.ingredient.get();
|
||||
this.ingredientName.textProperty().bind(vm.nameProperty());
|
||||
this.fatInputElement.textProperty().bindBidirectional(vm.fatProperty(), new NumberStringConverter());
|
||||
this.proteinInputElement.textProperty().bindBidirectional(vm.proteinProperty(), new NumberStringConverter());
|
||||
this.carbInputElement.textProperty().bindBidirectional(vm.carbsProperty(), new NumberStringConverter());
|
||||
this.estimatedKcalLabel.textProperty().bind(Bindings.createStringBinding(
|
||||
() -> String.format("Estimated energy value: %.1f", vm.getKcal()), vm.kcalProperty()
|
||||
));
|
||||
});
|
||||
this.nutritionValueContainer.addEventHandler(KeyEvent.KEY_RELEASED, event -> {
|
||||
if (event.getCode() != KeyCode.ENTER) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Ingredient newIngredient = server.updateIngredient(updateIngredient());
|
||||
Platform.runLater(() -> {
|
||||
this.ingredient.get().updateFrom(newIngredient);
|
||||
logger.info("Updated ingredient to " + newIngredient);
|
||||
});
|
||||
} catch (IllegalInputFormatException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateText() {
|
||||
|
||||
|
|
@ -47,16 +107,30 @@ public class NutritionDetailsCtrl implements LocaleAware {
|
|||
visible = !visible;
|
||||
}
|
||||
public void setItem(Ingredient ingredient) {
|
||||
this.ingredient = ingredient;
|
||||
this.ingredientName.setText(ingredient.getName());
|
||||
this.fatInputElement.setText(Double.toString(ingredient.getFatPer100g()));
|
||||
this.proteinInputElement.setText(Double.toString(ingredient.getProteinPer100g()));
|
||||
this.carbInputElement.setText(Double.toString(ingredient.getCarbsPer100g()));
|
||||
this.ingredient.get().updateFrom(ingredient);
|
||||
setVisible(true);
|
||||
}
|
||||
private Ingredient updateIngredient() throws IllegalInputFormatException {
|
||||
Ingredient current = this.ingredient.get().toIngredient();
|
||||
try {
|
||||
double f = Double.parseDouble(this.fatInputElement.getText());
|
||||
double p = Double.parseDouble(this.proteinInputElement.getText());
|
||||
double c = Double.parseDouble(this.carbInputElement.getText());
|
||||
current.setCarbsPer100g(c);
|
||||
current.setProteinPer100g(p);
|
||||
current.setFatPer100g(f);
|
||||
return current;
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalInputFormatException("Invalid F/P/C value");
|
||||
}
|
||||
|
||||
}
|
||||
@Override
|
||||
public LocaleManager getLocaleManager() {
|
||||
return manager;
|
||||
}
|
||||
public void handleNutritionSaveClick(ActionEvent actionEvent) throws IOException, InterruptedException {
|
||||
Ingredient newIngredient = updateIngredient();
|
||||
this.ingredient.get().updateFrom(server.updateIngredient(newIngredient));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,18 +13,15 @@
|
|||
fx:id="nutritionDetails"
|
||||
visible="false">
|
||||
<Label fx:id="ingredientName" />
|
||||
<HBox>
|
||||
<Label fx:id="fatInputLabel">Fat: </Label>
|
||||
<TextField fx:id="fatInputElement" />
|
||||
</HBox>
|
||||
<HBox>
|
||||
<Label fx:id="proteinInputLabel">Protein: </Label>
|
||||
<TextField fx:id="proteinInputElement" />
|
||||
</HBox>
|
||||
<HBox>
|
||||
<Label fx:id="carbInputLabel">Carbohydrates: </Label>
|
||||
<TextField fx:id="carbInputElement" />
|
||||
</HBox>
|
||||
<GridPane fx:id="nutritionValueContainer">
|
||||
<Label GridPane.columnIndex="0" GridPane.rowIndex="0" fx:id="fatInputLabel">Fat: </Label>
|
||||
<TextField GridPane.columnIndex="1" GridPane.rowIndex="0" fx:id="fatInputElement" />
|
||||
<Label GridPane.columnIndex="0" GridPane.rowIndex="1" fx:id="proteinInputLabel">Protein: </Label>
|
||||
<TextField GridPane.columnIndex="1" GridPane.rowIndex="1" fx:id="proteinInputElement" />
|
||||
<Label GridPane.columnIndex="0" GridPane.rowIndex="2" fx:id="carbInputLabel">Carbohydrates: </Label>
|
||||
<TextField GridPane.columnIndex="1" GridPane.rowIndex="2" fx:id="carbInputElement" />
|
||||
</GridPane>
|
||||
<Button onAction="#handleNutritionSaveClick">Save values</Button>
|
||||
<Label fx:id="estimatedKcalLabel">Estimated: 0kcal</Label>
|
||||
<Label fx:id="usageLabel">Not used in any recipes</Label>
|
||||
</VBox>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue