package com.dlsc.preferencesfx.model; import static com.dlsc.preferencesfx.util.Constants.DEFAULT_CATEGORY; import static com.dlsc.preferencesfx.util.Constants.DEFAULT_DIVIDER_POSITION; import com.dlsc.formsfx.model.structure.Field; import com.dlsc.formsfx.model.structure.FormElement; import com.dlsc.formsfx.model.util.TranslationService; import com.dlsc.preferencesfx.PreferencesFxEvent; import com.dlsc.preferencesfx.history.History; import com.dlsc.preferencesfx.util.PreferencesFxUtils; import com.dlsc.preferencesfx.util.SearchHandler; import com.dlsc.preferencesfx.util.StorageHandler; import com.dlsc.preferencesfx.view.PreferencesFxDialog; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.event.EventHandler; import javafx.event.EventType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Represents the model which holds all of the data and logic which is not limited to presenters. * * @author François Martin * @author Marco Sanfratello */ public class PreferencesFxModel { private static final Logger LOGGER = LoggerFactory.getLogger(PreferencesFxModel.class.getName()); private ObjectProperty<Category> displayedCategory = new SimpleObjectProperty<>(); private StringProperty searchText = new SimpleStringProperty(); private List<Category> categories; private List<Category> flatCategoriesLst; private StorageHandler storageHandler; private SearchHandler searchHandler; private History history; private ObjectProperty<TranslationService> translationService = new SimpleObjectProperty<>(); private boolean persistWindowState = false; private boolean saveSettings = true; private boolean historyDebugState = false; private boolean oneCategoryLayout; private BooleanProperty instantPersistent = new SimpleBooleanProperty(true); private BooleanProperty buttonsVisible = new SimpleBooleanProperty(true); private DoubleProperty dividerPosition = new SimpleDoubleProperty(DEFAULT_DIVIDER_POSITION); private final Map<EventType<PreferencesFxEvent>, List<EventHandler<? super PreferencesFxEvent>>> eventHandlers = new ConcurrentHashMap<>(); /** * Initializes a new model. * * @param storageHandler the {@link StorageHandler} to use for saving and loading * @param searchHandler the {@link SearchHandler} to use for handling the searches * @param history the {@link History} in which to save the changes and handle undo / redo * @param categories the categories to be displayed, along with the groups and settings */ public PreferencesFxModel( StorageHandler storageHandler, SearchHandler searchHandler, History history, Category[] categories ) { this.storageHandler = storageHandler; this.searchHandler = searchHandler; this.history = history; this.categories = Arrays.asList(categories); if (categories.length == 1 && (categories[0].getChildren() == null || categories[0].getChildren().isEmpty())) { oneCategoryLayout = true; } else { oneCategoryLayout = false; } flatCategoriesLst = PreferencesFxUtils.flattenCategories(this.categories); initializeCategoryTranslation(); if (getCategories().get(DEFAULT_CATEGORY).getGroups() == null && getCategories().get(DEFAULT_CATEGORY).getChildren() != null && getCategories().get(DEFAULT_CATEGORY).getChildren().size() > 0) { setDisplayedCategory(getCategories().get(DEFAULT_CATEGORY).getChildren().get(0)); } else { setDisplayedCategory(getCategories().get(DEFAULT_CATEGORY)); } createBreadcrumbs(this.categories); } /** * Sets up a binding of the TranslationService on the model, so that the Category's title gets * translated properly according to the TranslationService used. */ private void initializeCategoryTranslation() { flatCategoriesLst.forEach(category -> { translationServiceProperty().addListener((observable, oldValue, newValue) -> { category.translate(newValue); // listen for i18n changes in the TranslationService for this Category newValue.addListener(() -> category.translate(newValue)); }); }); } private void createBreadcrumbs(List<Category> categories) { categories.forEach(category -> { if (!Objects.equals(category.getGroups(), null)) { category.getGroups().forEach(group -> group.addToBreadcrumb(category.getBreadcrumb())); } if (!Objects.equals(category.getChildren(), null)) { category.createBreadcrumbs(category.getChildren()); } }); } public List<Category> getCategories() { return categories; } public boolean isPersistWindowState() { return persistWindowState; } public void setPersistWindowState(boolean persistWindowState) { this.persistWindowState = persistWindowState; } public boolean isSaveSettings() { return saveSettings; } public void setSaveSettings(boolean saveSettings) { this.saveSettings = saveSettings; } public History getHistory() { return history; } public StorageHandler getStorageHandler() { return storageHandler; } public boolean getHistoryDebugState() { return historyDebugState; } public void setHistoryDebugState(boolean historyDebugState) { this.historyDebugState = historyDebugState; } // ------ StorageHandler work ------------- /** * Saves the current selected Category. */ public void saveSelectedCategory() { storageHandler.saveSelectedCategory(displayedCategory.get().getBreadcrumb()); } /** * Loads the last selected Category before exiting the Preferences window. * * @return last selected Category */ public Category loadSelectedCategory() { String breadcrumb = storageHandler.loadSelectedCategory(); Category defaultCategory = getCategories().get(DEFAULT_CATEGORY); if (breadcrumb == null) { return defaultCategory; } return flatCategoriesLst.stream() .filter(category -> category.getBreadcrumb().equals(breadcrumb)) .findAny().orElse(defaultCategory); } /** * Saves all of the values of the settings using a {@link StorageHandler}. */ private void saveSettingValues() { PreferencesFxUtils.categoriesToSettings( getFlatCategoriesLst() ).forEach(setting -> { if (setting.hasValue()) { setting.saveSettingValue(storageHandler); } }); } /** * Load all of the values of the settings using a {@link StorageHandler} and attaches a listener * for {@link History}, so that it will be notified of changes to the setting's values. */ public void loadSettingValues() { PreferencesFxUtils.categoriesToSettings(flatCategoriesLst) .forEach(setting -> { if (setting.hasValue()) { LOGGER.trace("Loading: " + setting.getBreadcrumb()); if (saveSettings) { setting.loadSettingValue(storageHandler); } history.attachChangeListener(setting); } }); } public Category getDisplayedCategory() { return displayedCategory.get(); } public void setDisplayedCategory(Category displayedCategory) { LOGGER.trace("Change displayed category to: " + displayedCategory); this.displayedCategory.set(displayedCategory); } public ReadOnlyObjectProperty<Category> displayedCategoryProperty() { return displayedCategory; } public String getSearchText() { return searchText.get(); } public void setSearchText(String searchText) { this.searchText.set(searchText); } public StringProperty searchTextProperty() { return searchText; } public List<Category> getFlatCategoriesLst() { return flatCategoriesLst; } public SearchHandler getSearchHandler() { return searchHandler; } public boolean getButtonsVisible() { return buttonsVisible.get(); } public BooleanProperty buttonsVisibleProperty() { return buttonsVisible; } public void setButtonsVisible(boolean buttonsVisible) { this.buttonsVisible.set(buttonsVisible); } public boolean isInstantPersistent() { return instantPersistent.get(); } public BooleanProperty instantPersistentProperty() { return instantPersistent; } public void setInstantPersistent(boolean instantPersistent) { this.instantPersistent.set(instantPersistent); } public TranslationService getTranslationService() { return translationService.get(); } public ObjectProperty<TranslationService> translationServiceProperty() { return translationService; } public void setTranslationService(TranslationService translationService) { this.translationService.set(translationService); } public double getDividerPosition() { return dividerPosition.get(); } public DoubleProperty dividerPositionProperty() { return dividerPosition; } public void setDividerPosition(double dividerPosition) { this.dividerPosition.set(dividerPosition); } public boolean isOneCategoryLayout() { return oneCategoryLayout; } /** * Registers an event handler. The handler is called when a {@link PreferencesFxEvent} of the * specified type is being fired. * * @param eventType the type of the events to receive by the handler * @param eventHandler the handler to register * @throws NullPointerException if the event type or handler is null */ public void addEventHandler(EventType<PreferencesFxEvent> eventType, EventHandler<? super PreferencesFxEvent> eventHandler) { if (eventType == null) { throw new NullPointerException("Argument eventType must not be null"); } if (eventHandler == null) { throw new NullPointerException("Argument eventHandler must not be null"); } this.eventHandlers.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>()).add( eventHandler); } /** * Unregisters a previously registered event handler. One handler might have been registered for * different event types, so the caller needs to specify the particular event type from which to * unregister the handler. * * @param eventType the event type from which to unregister * @param eventHandler the handler to unregister * @throws NullPointerException if the event type or handler is null */ public void removeEventHandler(EventType<PreferencesFxEvent> eventType, EventHandler<? super PreferencesFxEvent> eventHandler) { if (eventType == null) { throw new NullPointerException("Argument eventType must not be null"); } if (eventHandler == null) { throw new NullPointerException("Argument eventHandler must not be null"); } List<EventHandler<? super PreferencesFxEvent>> list = this.eventHandlers.get(eventType); if (list != null) { list.remove(eventHandler); } } private void fireEvent(PreferencesFxEvent event) { List<EventHandler<? super PreferencesFxEvent>> list = this.eventHandlers.get(event.getEventType()); if (list == null) { return; } for (EventHandler<? super PreferencesFxEvent> eventHandler : list) { if (!event.isConsumed()) { eventHandler.handle(event); } } } /** * Saves the settings, when {@link #isSaveSettings()} returns {@code true}. */ public void saveSettings() { LOGGER.trace("Save"); if (isSaveSettings()) { if (!isInstantPersistent()) { applyFieldChanges(); } saveSettingValues(); fireEvent(PreferencesFxEvent.preferencesSavedEvent()); } history.clear(false); } /** * Undos all changes made, clears the history and saves the settings. * Typically called when the cancel button of the {@link PreferencesFxDialog} is pressed. * Can also be called explicity in case of using PreferencesFX as a node to undo all changes. */ public void discardChanges() { LOGGER.trace("Discard"); if (!isInstantPersistent()) { discardFieldChanges(); } else { history.clear(true); // save settings after undoing them if (saveSettings) { saveSettingValues(); } } fireEvent(PreferencesFxEvent.preferencesNotSavedEvent()); } private void applyFieldChanges() { PreferencesFxUtils.categoriesToElements(getFlatCategoriesLst()) .stream() .filter(element -> element instanceof Field) // only Fields can be persisted .map(Field.class::cast) .forEach(FormElement::persist); } private void discardFieldChanges() { PreferencesFxUtils.categoriesToElements(getFlatCategoriesLst()) .stream() .filter(element -> element instanceof Field) // only Fields can be persisted .map(Field.class::cast) .forEach(FormElement::reset); } }