package de.saring.sportstracker.gui.dialogs; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.CheckBox; import javafx.scene.control.ColorPicker; import javafx.scene.control.ListView; import javafx.scene.control.TextField; import javafx.stage.Window; import javax.inject.Inject; import org.controlsfx.validation.Validator; import de.saring.sportstracker.data.Equipment; import de.saring.sportstracker.data.Exercise; import de.saring.sportstracker.data.SportSubType; import de.saring.sportstracker.data.SportType; import de.saring.sportstracker.gui.STContext; import de.saring.sportstracker.gui.STDocument; import de.saring.util.StringUtils; import de.saring.util.gui.javafx.NameableListCell; /** * Controller (MVC) class of the Sport Type dialog for editing / adding SportType entries. * * @author Stefan Saring */ public class SportTypeDialogController extends AbstractDialogController { private final STDocument document; @FXML private TextField tfName; @FXML private CheckBox cbRecordDistance; @FXML private ColorPicker cpColor; @FXML private ListView<SportSubType> liSportSubtypes; @FXML private ListView<Equipment> liEquipments; @FXML private Button btSportSubtypeEdit; @FXML private Button btSportSubtypeDelete; @FXML private Button btEquipmentEdit; @FXML private Button btEquipmentDelete; /** ViewModel of the edited SportType. */ private SportTypeViewModel sportTypeViewModel; /** * Standard c'tor for dependency injection. * * @param context the SportsTracker UI context * @param document the SportsTracker model/document */ @Inject public SportTypeDialogController(final STContext context, final STDocument document) { super(context); this.document = document; } /** * Displays the Sport Type dialog for the passed SportType instance. * * @param parent parent window of the dialog * @param sportType sport type to be edited */ public void show(final Window parent, final SportType sportType) { // use a copy of the SportType to be edited // => prevents source object modification when dialog is closed using the "Cancel" action this.sportTypeViewModel = new SportTypeViewModel(sportType.clone()); final String dlgTitleKey = sportType.getName() == null ? "st.dlg.sporttype.title.add" : "st.dlg.sporttype.title"; final String dlgTitle = context.getResources().getString(dlgTitleKey); showEditDialog("/fxml/dialogs/SportTypeDialog.fxml", parent, dlgTitle); } @Override protected void setupDialogControls() { liSportSubtypes.setCellFactory(list -> new NameableListCell<>()); liEquipments.setCellFactory(list -> new NameableListCell<>()); setupBinding(); setupValidation(); updateSportSubtypeList(); updateEquipmentList(); // start Sport Subtype edit dialog on double clicks in list liSportSubtypes.setOnMouseClicked(event -> { if (event.getClickCount() > 1) { onEditSportSubtype(null); } }); // start Equipment edit dialog on double clicks in list liEquipments.setOnMouseClicked(event -> { if (event.getClickCount() > 1) { onEditEquipment(null); } }); } /** * Setup of the binding between the view model and the UI controls. */ private void setupBinding() { tfName.textProperty().bindBidirectional(sportTypeViewModel.name); cbRecordDistance.selectedProperty().bindBidirectional(sportTypeViewModel.recordDistance); cpColor.valueProperty().bindBidirectional(sportTypeViewModel.color); // the record distance mode can only be changed, when no exercises exists for // this sport type => disable checkbox, when such exercises were found Optional<Exercise> oExercise = document.getExerciseList().stream() .filter(exercise -> exercise.getSportType().getId() == sportTypeViewModel.id) .findFirst(); cbRecordDistance.setDisable(oExercise.isPresent()); // Edit and Delete buttons must be disabled when there is no selection in the appropriate list final BooleanBinding sportSubtypeSelected = Bindings.isNull( liSportSubtypes.getSelectionModel().selectedItemProperty()); btSportSubtypeEdit.disableProperty().bind(sportSubtypeSelected); btSportSubtypeDelete.disableProperty().bind(sportSubtypeSelected); final BooleanBinding equipmentSelected = Bindings.isNull( liEquipments.getSelectionModel().selectedItemProperty()); btEquipmentEdit.disableProperty().bind(equipmentSelected); btEquipmentDelete.disableProperty().bind(equipmentSelected); } /** * Setup of the validation of the UI controls. */ private void setupValidation() { validationSupport.registerValidator(tfName, Validator.createEmptyValidator(context.getResources().getString("st.dlg.sporttype.error.no_name"))); } @Override protected boolean validateAndStore() { // make sure that the entered name is not in use by other sport types yet final SportType editedSportType = sportTypeViewModel.getSportType(); Optional<SportType> oSportTypeSameName = document.getSportTypeList().stream() .filter(stTemp -> stTemp.getId() != sportTypeViewModel.id && stTemp.getName().equals(editedSportType.getName())) .findFirst(); if (oSportTypeSameName.isPresent()) { tfName.selectAll(); context.showMessageDialog(getWindow(tfName), Alert.AlertType.ERROR, "common.error", "st.dlg.sporttype.error.name_in_use"); tfName.requestFocus(); return false; } // make sure that there's at least one sport subtype if (editedSportType.getSportSubTypeList().size() == 0) { context.showMessageDialog(getWindow(liSportSubtypes), Alert.AlertType.ERROR, "common.error", "st.dlg.sporttype.error.no_subtype"); return false; } // store the edited SportType in the documents list document.getSportTypeList().set(editedSportType); return true; } private void updateSportSubtypeList() { final ObservableList<SportSubType> olSportSubtypes = FXCollections.observableArrayList(); liSportSubtypes.getItems().clear(); sportTypeViewModel.sportSubtypes.forEach(sportType -> olSportSubtypes.add(sportType)); liSportSubtypes.setItems(olSportSubtypes); } private void updateEquipmentList() { final ObservableList<Equipment> olEquipments = FXCollections.observableArrayList(); liEquipments.getItems().clear(); sportTypeViewModel.equipments.forEach(equipment -> olEquipments.add(equipment)); liEquipments.setItems(olEquipments); } /** * Action for adding a new sport subtype. */ @FXML private void onAddSportSubtype(final ActionEvent event) { // create a new SportSubType object and display in the edit dialog final SportSubType newSubType = new SportSubType(sportTypeViewModel.sportSubtypes.getNewID()); editSportSubType(newSubType); } /** * Action for editing the selected sport subtype. */ @FXML private void onEditSportSubtype(final ActionEvent event) { // display edit dialog for selected sport subtype final SportSubType selectedSportSubtype = liSportSubtypes.getSelectionModel().getSelectedItem(); if (selectedSportSubtype != null) { editSportSubType(selectedSportSubtype); } } /** * Action for deleting the selected sport subtype. */ @FXML private void onDeleteSportSubtype(final ActionEvent event) { // display confirmation dialog final Optional<ButtonType> resultDeleteSportSubtype = context.showConfirmationDialog(getWindow(liSportSubtypes), "st.dlg.sporttype.confirm.delete_subtype.title", "st.dlg.sporttype.confirm.delete_subtype.text"); if (!resultDeleteSportSubtype.isPresent() || resultDeleteSportSubtype.get() != ButtonType.OK) { return; } // are there any existing exercises for this sport subtype? final SportSubType selectedSportSubtype = liSportSubtypes.getSelectionModel().getSelectedItem(); final List<Exercise> lRefExercises = document.getExerciseList().stream() .filter(exercise -> exercise.getSportType().getId() == sportTypeViewModel.id && exercise.getSportSubType().equals(selectedSportSubtype)) .collect(Collectors.toList()); // when there are referenced exercises => these exercises needs to be deleted too if (!lRefExercises.isEmpty()) { // show confirmation message box again final Optional<ButtonType> resultDeleteExistingExercises = context.showConfirmationDialog( getWindow(liSportSubtypes), "st.dlg.sporttype.confirm.delete_subtype.title", "st.dlg.sporttype.confirm.delete_subtype_existing.text"); if (!resultDeleteExistingExercises.isPresent() || resultDeleteExistingExercises.get() != ButtonType.OK) { return; } // delete reference exercises lRefExercises.forEach(exercise -> document.getExerciseList().removeByID(exercise.getId())); } // finally delete the sport subtype sportTypeViewModel.sportSubtypes.removeByID(selectedSportSubtype.getId()); updateSportSubtypeList(); } /** * Displays the add/edit dialog for the specified sport subtype name (includes * error checking and dialog redisplay). The modified sport subtype will be * stored in the sport type. * * @param subType the sport subtype to be edited */ private void editSportSubType(final SportSubType subType) { // start with current subtype name String strName = subType.getName(); // title text depends on editing a new or an existing subtype final String dlgTitleKey = strName == null ? "st.dlg.sportsubtype.add.title" : "st.dlg.sportsubtype.edit.title"; while (true) { // display text input dialog for sport subtype name final Optional<String> oResult = context.showTextInputDialog( getWindow(liSportSubtypes), dlgTitleKey, "st.dlg.sportsubtype.name", strName); // exit when user has pressed Cancel button if (!oResult.isPresent()) { return; } strName = StringUtils.getTrimmedTextOrNull(oResult.get()); // check the entered name => display error messages on problems if (strName == null) { // no name was entered context.showMessageDialog(getWindow(liSportSubtypes), Alert.AlertType.ERROR, "common.error", "st.dlg.sportsubtype.error.no_name"); } else { // make sure that the entered name is not in use by other sport subtypes yet final String enteredName = strName; Optional<SportSubType> oSportSubtypeConflict = sportTypeViewModel.sportSubtypes.stream() .filter(sstTemp -> sstTemp.getId() != subType.getId() && sstTemp.getName().equals(enteredName)) .findFirst(); if (oSportSubtypeConflict.isPresent()) { context.showMessageDialog(getWindow(liSportSubtypes), Alert.AlertType.ERROR, "common.error", "st.dlg.sportsubtype.error.in_use"); } else { // the name is OK, store the modified subtype and update the list subType.setName(strName); sportTypeViewModel.sportSubtypes.set(subType); updateSportSubtypeList(); return; } } } } /** * Action for adding a new equipment. */ @FXML private void onAddEquipment(final ActionEvent event) { // create a new Equipment object and display in the edit dialog Equipment newEquipment = new Equipment(sportTypeViewModel.equipments.getNewID()); editEquipment(newEquipment); } /** * Action for editing the selected equipment. */ @FXML private void onEditEquipment(final ActionEvent event) { // display edit dialog for selected equipment final Equipment selectedEquipment = liEquipments.getSelectionModel().getSelectedItem(); if (selectedEquipment!= null) { editEquipment(selectedEquipment); } } /** * Action for deleting the selected equipment. */ @FXML private void onDeleteEquipment(final ActionEvent event) { // display confirmation dialog final Optional<ButtonType> resultDeleteEquipment = context.showConfirmationDialog(getWindow(liEquipments), "st.dlg.sporttype.confirm.delete_equipment.title", "st.dlg.sporttype.confirm.delete_equipment.text"); if (!resultDeleteEquipment.isPresent() || resultDeleteEquipment.get() != ButtonType.OK) { return; } // are there any existing exercises for this equipment? final Equipment selectedEquipment = liEquipments.getSelectionModel().getSelectedItem(); List<Exercise> lRefExercises = document.getExerciseList().stream() .filter(exercise -> exercise.getSportType().getId() == sportTypeViewModel.id && exercise.getEquipment() != null && exercise.getEquipment().equals(selectedEquipment)) .collect(Collectors.toList()); // when there are referenced exercises => the equipment must be deleted in those too if (lRefExercises.size() > 0) { // show confirmation message box again final Optional<ButtonType> resultDeleteEqInExercises = context.showConfirmationDialog( getWindow(liEquipments), "st.dlg.sporttype.confirm.delete_equipment.title", "st.dlg.sporttype.confirm.delete_equipment_existing.text"); if (!resultDeleteEqInExercises.isPresent() || resultDeleteEqInExercises.get() != ButtonType.OK) { return; } // delete equipment in all exercises which use it lRefExercises.forEach(exercise -> exercise.setEquipment(null)); } // finally delete the equipment sportTypeViewModel.equipments.removeByID(selectedEquipment.getId()); updateEquipmentList(); } /** * Displays the add/edit dialog for the specified equipment name (includes * error checking and dialog redisplay). The modified equipment will be * stored in the sport type. * * @param equipment the equipment to be edited */ private void editEquipment(final Equipment equipment) { // start with current subtype name String strName = equipment.getName(); // title text depends on editing a new or an existing equipment final String dlgTitleKey = strName == null ? "st.dlg.equipment.add.title" : "st.dlg.equipment.edit.title"; while (true) { // display text input dialog for equipment name final Optional<String> oResult = context.showTextInputDialog( getWindow(liEquipments), dlgTitleKey, "st.dlg.equipment.name", strName); // exit when user has pressed Cancel button if (!oResult.isPresent()) { return; } strName = StringUtils.getTrimmedTextOrNull(oResult.get()); // check the entered name => display error messages on problems if (strName == null) { // no name was entered context.showMessageDialog(getWindow(liEquipments), Alert.AlertType.ERROR, "common.error", "st.dlg.equipment.error.no_name"); } else { // make sure that the entered name is not in use by other equipment's yet final String enteredName = strName; Optional<Equipment> oEquipmnentConflict = sportTypeViewModel.equipments.stream() .filter(eqTemp -> eqTemp.getId() != equipment.getId() && eqTemp.getName().equals(enteredName)) .findFirst(); if (oEquipmnentConflict.isPresent()) { context.showMessageDialog(getWindow(liEquipments), Alert.AlertType.ERROR, "common.error", "st.dlg.equipment.error.in_use"); } else { // the name is OK, store the modified equipment and update the list equipment.setName(strName); sportTypeViewModel.equipments.set(equipment); updateEquipmentList(); return; } } } } }