From 90d2de65deb4c949000b38170d6ca9e7b78f2551 Mon Sep 17 00:00:00 2001 From: David Guler Date: Tue, 15 Nov 2022 15:25:51 +0100 Subject: [PATCH 1/4] refactor: simplified dayCellFactory for date selector Instead of generating a list of dates for every single visible date and checking if it is contained in that list, we now use a (admittedly scary-looking) predicate to compare the date to the start and enddates --- .../CropDetailController.java | 32 +++++++------ .../gartenverwaltung/MyGardenController.java | 2 +- .../SelectSowDayController.java | 45 +++++++++++------- .../ch/zhaw/gartenverwaltung/types/Plant.java | 46 ++++++------------- 4 files changed, 62 insertions(+), 63 deletions(-) diff --git a/src/main/java/ch/zhaw/gartenverwaltung/CropDetailController.java b/src/main/java/ch/zhaw/gartenverwaltung/CropDetailController.java index bf5a7bf..7051bfa 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/CropDetailController.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/CropDetailController.java @@ -93,23 +93,27 @@ public class CropDetailController { } - public void setPlantFromCrop(Crop crop) throws HardinessZoneNotSetException, IOException, PlantNotFoundException { + public void setPlantFromCrop(Crop crop) throws PlantNotFoundException { this.crop = crop; - Plant plant = plantList.getPlantById(Settings.getInstance().getCurrentHardinessZone(), crop.getPlantId()) - .orElseThrow(PlantNotFoundException::new); + try { + Plant plant = plantList.getPlantById(Settings.getInstance().getCurrentHardinessZone(), crop.getPlantId()) + .orElseThrow(PlantNotFoundException::new); - cropName_label.setText(plant.name()); - description_label.setText(plant.description()); - light_label.setText(String.valueOf(plant.light())); - soil_label.setText(plant.soil()); - spacing_label.setText(plant.spacing()); - if (plant.image() != null) { - imageView.setImage(plant.image()); + cropName_label.setText(plant.name()); + description_label.setText(plant.description()); + light_label.setText(String.valueOf(plant.light())); + soil_label.setText(plant.soil()); + spacing_label.setText(plant.spacing()); + if (plant.image() != null) { + imageView.setImage(plant.image()); + } + area_label.setText(""); + location_label.setText(""); + createTaskLists(crop); + createPestList(plant); + } catch (HardinessZoneNotSetException | IOException e) { + throw new PlantNotFoundException(); } - area_label.setText(""); - location_label.setText(""); - createTaskLists(crop); - createPestList(plant); } private void createTaskLists(Crop crop) { diff --git a/src/main/java/ch/zhaw/gartenverwaltung/MyGardenController.java b/src/main/java/ch/zhaw/gartenverwaltung/MyGardenController.java index 3568b50..3901377 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/MyGardenController.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/MyGardenController.java @@ -108,7 +108,7 @@ public class MyGardenController { stage.initModality(Modality.APPLICATION_MODAL); stage.setResizable(true); stage.showAndWait(); - } catch (IOException | HardinessZoneNotSetException | PlantNotFoundException e) { + } catch (IOException | PlantNotFoundException e) { // TODO: show error alert LOG.log(Level.SEVERE, "Could not load plant details.", e); } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/SelectSowDayController.java b/src/main/java/ch/zhaw/gartenverwaltung/SelectSowDayController.java index 67338be..60ff2ae 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/SelectSowDayController.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/SelectSowDayController.java @@ -7,7 +7,7 @@ import javafx.scene.control.*; import javafx.util.Callback; import java.time.LocalDate; -import java.util.List; +import java.time.MonthDay; public class SelectSowDayController { private Plant selectedPlant; @@ -61,36 +61,49 @@ public class SelectSowDayController { /** * date picker disable/enable dates according to selected plant: sow or harvest day + * * @return cellFactory of datePicker */ private Callback getDayCellFactory() { return (datePicker) -> new DateCell() { + private final LocalDate today = LocalDate.now(); + @Override public void updateItem(LocalDate item, boolean empty) { super.updateItem(item, empty); setDisable(true); setStyle("-fx-background-color: #ffc0cb;"); - List dates; - LocalDate today = LocalDate.now(); - if (harvest_radio.isSelected()) { - dates = selectedPlant.getDateListOfGrowthPhase(GrowthPhaseType.HARVEST); - } else { - dates = selectedPlant.getDateListOfGrowthPhase(GrowthPhaseType.SOW); - } - for (LocalDate date : dates) { - if (item.getMonth() == date.getMonth() - && item.getDayOfMonth() == date.getDayOfMonth() - && item.compareTo(today) > 0) { + + if (item.compareTo(today) > 0 && (!harvest_radio.isSelected() || selectedPlant.sowDateFromHarvestDate(item, 0).compareTo(today) >= 0)) { + GrowthPhaseType selectedPhase = harvest_radio.isSelected() ? GrowthPhaseType.HARVEST : GrowthPhaseType.SOW; + MonthDay minDate = selectedPlant.getMinDateForGrowthPhase(selectedPhase); + MonthDay maxDate = selectedPlant.getMaxDateForGrowthPhase(selectedPhase); + + if (dateInRange(item, minDate, maxDate)) { setDisable(false); setStyle("-fx-background-color: #32CD32;"); } } - if ((harvest_radio.isSelected() && selectedPlant.sowDateFromHarvestDate(item, 0).compareTo(today) < 0)) { - setDisable(true); - setStyle("-fx-background-color: #ffc0cb;"); - } } }; } + + /** + * Checks if the given {@link LocalDate} is within the given {@link MonthDay} range. + * (regardless of year) + * + * @param subject The date to check + * @param min The start of the date-range + * @param max The end of the date-range + * @return Whether the subject is within the range. + */ + private boolean dateInRange(LocalDate subject, MonthDay min, MonthDay max) { + return subject.getMonth().compareTo(min.getMonth()) >= 0 && + subject.getMonth().compareTo(max.getMonth()) <= 0 && + // if the day is less than the minimum day, the minimum month must not be equal + (subject.getDayOfMonth() >= min.getDayOfMonth() || !subject.getMonth().equals(min.getMonth())) && + // if the day is greater than the maximum day, the maximum month must not be equal + (subject.getDayOfMonth() <= max.getDayOfMonth() || !subject.getMonth().equals(max.getMonth())); + } } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/types/Plant.java b/src/main/java/ch/zhaw/gartenverwaltung/types/Plant.java index 9cb3d08..16bd061 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/types/Plant.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/types/Plant.java @@ -3,7 +3,7 @@ package ch.zhaw.gartenverwaltung.types; import javafx.scene.image.Image; import java.time.LocalDate; -import java.util.LinkedList; +import java.time.MonthDay; import java.util.List; import java.util.stream.Collectors; @@ -69,37 +69,19 @@ public record Plant( return (int) DAYS.between(harvest.startDate().atYear(currentYear), sow.startDate().atYear(currentYear)); } - /** - * filter out the given growthPhase out of the lifecycle - * create list of dates for this growthPhase and return it - * @param growthPhase the wanted growthPhase - * @return a list of dates of the current year - */ - public List getDateListOfGrowthPhase(GrowthPhaseType growthPhase) { - List dates = new LinkedList<>(); - for (GrowthPhase growth : lifecycle) { - if (growth.type().equals(growthPhase)) { - dates = addDatesFromMonthDay(growth); - } - } - return dates; - } - /** - * transform monthDay value of the given growthPhase to localDate - * return a list of dates from start to end of growth phase - * @param growthPhase the current growthPhase - * @return a list of dates of the current year - */ - private List addDatesFromMonthDay(GrowthPhase growthPhase) { - List dates = new LinkedList<>(); - LocalDate today = LocalDate.now(); - LocalDate start = growthPhase.startDate().atYear(today.getYear()); - LocalDate end = growthPhase.endDate().atYear(today.getYear()); - while (!start.isAfter(end)) { - dates.add(start); - start = start.plusDays(1); - } - return dates; + public MonthDay getMaxDateForGrowthPhase(GrowthPhaseType growthPhase) { + return lifecycle.stream() + .filter(phase -> phase.type().equals(growthPhase)) + .findFirst() + .map(GrowthPhase::endDate) + .orElse(MonthDay.of(12, 31)); + } + public MonthDay getMinDateForGrowthPhase(GrowthPhaseType growthPhase) { + return lifecycle.stream() + .filter(phase -> phase.type().equals(growthPhase)) + .findFirst() + .map(GrowthPhase::startDate) + .orElse(MonthDay.of(1, 1)); } } From 9ba252b8283d30458ef3ec9ebf73b111e6a5cf17 Mon Sep 17 00:00:00 2001 From: David Guler Date: Tue, 15 Nov 2022 22:45:01 +0100 Subject: [PATCH 2/4] refactor: fixed and simplified dayCellFactory even more Added method to check if a date is within a GrowthPhaseType to plant, thus removing the need for the ugly getMinDate methods and moving knowledge of the phase-internals to the Plant class. Also removed the need to specify the lifecycle-group to the sowDateFromHarvest method --- .../SelectSowDayController.java | 37 +++++--------- .../ch/zhaw/gartenverwaltung/types/Plant.java | 49 ++++++++++++++----- .../zhaw/gartenverwaltung/SelectSowDay.fxml | 6 +-- 3 files changed, 50 insertions(+), 42 deletions(-) diff --git a/src/main/java/ch/zhaw/gartenverwaltung/SelectSowDayController.java b/src/main/java/ch/zhaw/gartenverwaltung/SelectSowDayController.java index 60ff2ae..e3722fc 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/SelectSowDayController.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/SelectSowDayController.java @@ -7,7 +7,6 @@ import javafx.scene.control.*; import javafx.util.Callback; import java.time.LocalDate; -import java.time.MonthDay; public class SelectSowDayController { private Plant selectedPlant; @@ -17,12 +16,15 @@ public class SelectSowDayController { @FXML private RadioButton harvest_radio; + @FXML + private RadioButton sow_radio; + @FXML + public ToggleGroup phase_group; public LocalDate retrieveResult() { LocalDate sowDate = datepicker.getValue(); if (harvest_radio.isSelected()) { - //ToDo method to get current lifecycle group in plant - sowDate = selectedPlant.sowDateFromHarvestDate(datepicker.getValue(), 0); + sowDate = selectedPlant.sowDateFromHarvestDate(sowDate); } return sowDate; } @@ -46,6 +48,9 @@ public class SelectSowDayController { Callback dayCellFactory = getDayCellFactory(); datepicker.setDayCellFactory(dayCellFactory); datepicker.setEditable(false); + + sow_radio.setUserData(GrowthPhaseType.SOW); + harvest_radio.setUserData(GrowthPhaseType.HARVEST); } public void initSaveButton(Button saveButton) { @@ -75,12 +80,10 @@ public class SelectSowDayController { setDisable(true); setStyle("-fx-background-color: #ffc0cb;"); - if (item.compareTo(today) > 0 && (!harvest_radio.isSelected() || selectedPlant.sowDateFromHarvestDate(item, 0).compareTo(today) >= 0)) { - GrowthPhaseType selectedPhase = harvest_radio.isSelected() ? GrowthPhaseType.HARVEST : GrowthPhaseType.SOW; - MonthDay minDate = selectedPlant.getMinDateForGrowthPhase(selectedPhase); - MonthDay maxDate = selectedPlant.getMaxDateForGrowthPhase(selectedPhase); + if (item.compareTo(today) > 0 && (!harvest_radio.isSelected() || selectedPlant.sowDateFromHarvestDate(item).compareTo(today) >= 0)) { + GrowthPhaseType selectedPhase = (GrowthPhaseType) phase_group.getSelectedToggle().getUserData(); - if (dateInRange(item, minDate, maxDate)) { + if (selectedPlant.isDateInPhase(item, selectedPhase)) { setDisable(false); setStyle("-fx-background-color: #32CD32;"); } @@ -88,22 +91,4 @@ public class SelectSowDayController { } }; } - - /** - * Checks if the given {@link LocalDate} is within the given {@link MonthDay} range. - * (regardless of year) - * - * @param subject The date to check - * @param min The start of the date-range - * @param max The end of the date-range - * @return Whether the subject is within the range. - */ - private boolean dateInRange(LocalDate subject, MonthDay min, MonthDay max) { - return subject.getMonth().compareTo(min.getMonth()) >= 0 && - subject.getMonth().compareTo(max.getMonth()) <= 0 && - // if the day is less than the minimum day, the minimum month must not be equal - (subject.getDayOfMonth() >= min.getDayOfMonth() || !subject.getMonth().equals(min.getMonth())) && - // if the day is greater than the maximum day, the maximum month must not be equal - (subject.getDayOfMonth() <= max.getDayOfMonth() || !subject.getMonth().equals(max.getMonth())); - } } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/types/Plant.java b/src/main/java/ch/zhaw/gartenverwaltung/types/Plant.java index 16bd061..1cf6466 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/types/Plant.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/types/Plant.java @@ -42,11 +42,10 @@ public record Plant( /** * get sow date from given harvest day from lifecycle group * @param harvestDate date of the harvest - * @param group lifecycle group * @return LocaleDate of sow date */ - public LocalDate sowDateFromHarvestDate(LocalDate harvestDate, int group) { - return harvestDate.minusDays(timeToHarvest(group)); + public LocalDate sowDateFromHarvestDate(LocalDate harvestDate) { + return harvestDate.minusDays(timeToHarvest(lifecycleGroupFromHarvestDate(harvestDate))); } /** @@ -69,19 +68,43 @@ public record Plant( return (int) DAYS.between(harvest.startDate().atYear(currentYear), sow.startDate().atYear(currentYear)); } - - public MonthDay getMaxDateForGrowthPhase(GrowthPhaseType growthPhase) { + public int lifecycleGroupFromHarvestDate(LocalDate harvestDate) { return lifecycle.stream() - .filter(phase -> phase.type().equals(growthPhase)) + .filter(growthPhase -> growthPhase.type().equals(GrowthPhaseType.HARVEST) && + dateInRange(harvestDate, growthPhase.startDate(), growthPhase.endDate())) + .map(GrowthPhase::group) .findFirst() - .map(GrowthPhase::endDate) - .orElse(MonthDay.of(12, 31)); + .orElse(0); } - public MonthDay getMinDateForGrowthPhase(GrowthPhaseType growthPhase) { + + /** + * Checks if the given {@link LocalDate} is within a {@link GrowthPhase} of the given {@link GrowthPhaseType} + * + * @param date The date to check. + * @param phase The {@link GrowthPhaseType} to match against + * @return Whether the date is within the given {@link GrowthPhaseType} + */ + public boolean isDateInPhase(LocalDate date, GrowthPhaseType phase) { return lifecycle.stream() - .filter(phase -> phase.type().equals(growthPhase)) - .findFirst() - .map(GrowthPhase::startDate) - .orElse(MonthDay.of(1, 1)); + .filter(growthPhase -> growthPhase.type().equals(phase)) + .anyMatch(growthPhase -> dateInRange(date, growthPhase.startDate(), growthPhase.endDate())); + } + + /** + * Checks if the given {@link LocalDate} is within the given {@link MonthDay} range. + * (regardless of year) + * + * @param subject The date to check + * @param min The start of the date-range + * @param max The end of the date-range + * @return Whether the subject is within the range. + */ + private boolean dateInRange(LocalDate subject, MonthDay min, MonthDay max) { + return subject.getMonth().compareTo(min.getMonth()) >= 0 && + subject.getMonth().compareTo(max.getMonth()) <= 0 && + // if the day is less than the minimum day, the minimum month must not be equal + (subject.getDayOfMonth() >= min.getDayOfMonth() || !subject.getMonth().equals(min.getMonth())) && + // if the day is greater than the maximum day, the maximum month must not be equal + (subject.getDayOfMonth() <= max.getDayOfMonth() || !subject.getMonth().equals(max.getMonth())); } } diff --git a/src/main/resources/ch/zhaw/gartenverwaltung/SelectSowDay.fxml b/src/main/resources/ch/zhaw/gartenverwaltung/SelectSowDay.fxml index 75f33c3..1ebcf03 100644 --- a/src/main/resources/ch/zhaw/gartenverwaltung/SelectSowDay.fxml +++ b/src/main/resources/ch/zhaw/gartenverwaltung/SelectSowDay.fxml @@ -18,15 +18,15 @@ - + - + - + From 2f69c488003849939a737bfd5a3428a800aa0fda Mon Sep 17 00:00:00 2001 From: David Guler Date: Wed, 16 Nov 2022 20:37:07 +0100 Subject: [PATCH 3/4] refactor: dedicated loadPaneToDialog function replacing the previous workaround --- .../gartenverwaltung/PlantsController.java | 25 ++++++++----------- .../gartenverwaltung/bootstrap/AppLoader.java | 19 ++++++++++++++ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/main/java/ch/zhaw/gartenverwaltung/PlantsController.java b/src/main/java/ch/zhaw/gartenverwaltung/PlantsController.java index 200ccf9..8b821c1 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/PlantsController.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/PlantsController.java @@ -21,7 +21,6 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.VBox; -import javafx.stage.Stage; import java.io.IOException; import java.time.LocalDate; @@ -74,7 +73,6 @@ public class PlantsController { */ @FXML void selectSowDate() throws IOException { - Stage stage = new Stage(); Dialog dateSelection = new Dialog<>(); dateSelection.setTitle("Select Date"); dateSelection.setHeaderText(String.format("Select Harvest/Sow Date for %s:", selectedPlant.name())); @@ -85,22 +83,21 @@ public class PlantsController { ButtonType sowButton = new ButtonType("Save", ButtonBar.ButtonData.OK_DONE); dialogPane.getButtonTypes().addAll(sowButton, ButtonType.CANCEL); - if (appLoader.loadSceneToStage("SelectSowDay.fxml", stage) instanceof SelectSowDayController controller) { + if (appLoader.loadPaneToDialog("SelectSowDay.fxml", dialogPane) instanceof SelectSowDayController controller) { controller.initSaveButton((Button) dialogPane.lookupButton(sowButton)); controller.setSelectedPlant(selectedPlant); dateSelection.setResultConverter(button -> button.equals(sowButton) ? controller.retrieveResult() : null); - } - dialogPane.setContent(stage.getScene().getRoot()); - dateSelection.showAndWait() - .ifPresent(date -> { - try { - garden.plantAsCrop(selectedPlant, date); - } catch (IOException | HardinessZoneNotSetException | PlantNotFoundException e) { - LOG.log(Level.SEVERE, "Couldn't save Crop", e); - } - plantsRoot.fireEvent(new ChangeViewEvent(ChangeViewEvent.CHANGE_MAIN_VIEW, "MyGarden.fxml")); - }); + dateSelection.showAndWait() + .ifPresent(date -> { + try { + garden.plantAsCrop(selectedPlant, date); + } catch (IOException | HardinessZoneNotSetException | PlantNotFoundException e) { + LOG.log(Level.SEVERE, "Couldn't save Crop", e); + } + plantsRoot.fireEvent(new ChangeViewEvent(ChangeViewEvent.CHANGE_MAIN_VIEW, "MyGarden.fxml")); + }); + } } /** diff --git a/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/AppLoader.java b/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/AppLoader.java index f55f6d5..59a4f7d 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/AppLoader.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/AppLoader.java @@ -12,6 +12,7 @@ import ch.zhaw.gartenverwaltung.models.GardenSchedule; import ch.zhaw.gartenverwaltung.models.PlantListModel; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; +import javafx.scene.control.DialogPane; import javafx.scene.layout.Pane; import javafx.stage.Stage; @@ -78,6 +79,24 @@ public class AppLoader { return controller; } + /** + * Loads the given fxml-file from resources (no caching) and appendeds it's + * contents to the given {@link DialogPane}. + * Performs dependency-injection. + * + * @param fxmlFile The file name to be loaded + * @param appendee The {@link DialogPane} to which the FXML contents are to be appended. + * @return The controller of the loaded scene. + * @throws IOException if the file could not be loaded + */ + public Object loadPaneToDialog(String fxmlFile, DialogPane appendee) throws IOException { + FXMLLoader loader = new FXMLLoader(Objects.requireNonNull(HelloApplication.class.getResource(fxmlFile))); + appendee.setContent(loader.load()); + Object controller = loader.getController(); + annotationInject(controller); + return controller; + } + /** * Loads the given fxml-file from resources and caches the pane. * Performs dependency-injection. From 3a69119eb7722ab80b57e1287c33a7abf055838d Mon Sep 17 00:00:00 2001 From: David Guler Date: Fri, 18 Nov 2022 12:21:50 +0100 Subject: [PATCH 4/4] refactor: bind croplist in schedule --- .../zhaw/gartenverwaltung/MyGardenController.java | 2 ++ .../gartenverwaltung/MyScheduleController.java | 14 +------------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/main/java/ch/zhaw/gartenverwaltung/MyGardenController.java b/src/main/java/ch/zhaw/gartenverwaltung/MyGardenController.java index 3901377..e54e608 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/MyGardenController.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/MyGardenController.java @@ -90,10 +90,12 @@ public class MyGardenController { Label label = new Label(plant.name()); label.setMaxWidth(2000); HBox.setHgrow(label, Priority.ALWAYS); + Button details = new Button("Details"); Button delete = new Button("delete"); details.setOnAction(getGoToCropDetailEvent(crop)); delete.setOnAction(getDeleteCropEvent(crop)); + hBox.getChildren().addAll(imageView, label, details, delete); return hBox; } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/MyScheduleController.java b/src/main/java/ch/zhaw/gartenverwaltung/MyScheduleController.java index 417722a..e895480 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/MyScheduleController.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/MyScheduleController.java @@ -9,9 +9,6 @@ import ch.zhaw.gartenverwaltung.models.GardenSchedule; import ch.zhaw.gartenverwaltung.types.Crop; import ch.zhaw.gartenverwaltung.types.Plant; import ch.zhaw.gartenverwaltung.types.Task; -import javafx.beans.property.ListProperty; -import javafx.beans.property.SimpleListProperty; -import javafx.collections.FXCollections; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.control.ListCell; @@ -37,8 +34,6 @@ public class MyScheduleController { @Inject private PlantList plantList; - private final ListProperty cropListProperty = new SimpleListProperty<>(FXCollections.observableArrayList()); - @FXML private Label day1_label; @@ -90,15 +85,8 @@ public class MyScheduleController { @AfterInject @SuppressWarnings("unused") public void init() { - List cropList; - try { - cropList = garden.getCrops(); - cropListProperty.addAll(cropList); - } catch (IOException e) { - e.printStackTrace(); - } setCellFactoryListView(); - scheduledPlants_listview.itemsProperty().bind(cropListProperty); + scheduledPlants_listview.itemsProperty().bind(garden.getPlantedCrops()); lookForSelectedListEntries(); setDayLabels(); information_label.setText("");