package com.twasyl.slideshowfx.controls.builder.nodes; import com.twasyl.slideshowfx.engine.template.TemplateEngine; import com.twasyl.slideshowfx.engine.template.configuration.SlideElementTemplate; import com.twasyl.slideshowfx.engine.template.configuration.SlideTemplate; import com.twasyl.slideshowfx.engine.template.configuration.TemplateConfiguration; import com.twasyl.slideshowfx.ui.controls.ExtendedTextField; import com.twasyl.slideshowfx.utils.DialogHelper; import com.twasyl.slideshowfx.utils.beans.Pair; import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.TitledPane; import javafx.scene.control.Tooltip; import javafx.scene.layout.FlowPane; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import static com.twasyl.slideshowfx.ui.controls.validators.Validators.isNotEmpty; /** * This class provides a control allowing to open a template configuration file. It defines the complete UI used * to add slides, define the template configuration. * In order to fill the control with a configuration file, the method {@link #fillWithFile(File)}} must be used. * In order to get the configuration as a string, the method {@link #getAsString()} must be used. * * @author Thierry Wasylczenko * @version 1.1 * @since SlideshowFX 1.3 */ public class TemplateConfigurationFilePane extends VBox { private static Logger LOGGER = Logger.getLogger(TemplateConfigurationFilePane.class.getName()); private Path workingPath; // General template configuration private ExtendedTextField templateName = new ExtendedTextField("Name", true); private ExtendedTextField templateFile = new ExtendedTextField("File", true); private ExtendedTextField jsObject = new ExtendedTextField("JS Object", true); private ExtendedTextField templateResourcesDirectory = new ExtendedTextField("Resources' directory", true); private List<TemplateVariable> defaultVariables = new ArrayList<>(); // Template default variables final VBox defaultVariablesPane = new VBox(5); // General slides configuration private ExtendedTextField slidesContainer = new ExtendedTextField("Slides' container", true); private ExtendedTextField slideIdPrefix = new ExtendedTextField("Slide ID prefix", true); private ExtendedTextField slidesTemplateDirectory = new ExtendedTextField("Template directory", true); private ExtendedTextField slidesPresentationDirectory = new ExtendedTextField("Presentation directory", true); private ExtendedTextField slidesThumbnailDirectory = new ExtendedTextField("Thumbnails directory", true); // Slides definitions private List<SlideDefinition> slideDefinitions = new ArrayList<>(); private VBox slideDefinitionsPane = new VBox(5); public TemplateConfigurationFilePane() { this.setSpacing(10); this.initializeMandatoryFields(); this.getChildren().addAll(this.getTemplateGlobalConfigurationPane(), this.getTemplateDefaultVariablePane(), this.getSlidesGlobalConfigurationPane(), getSlidesPane()); this.parentProperty().addListener((parentValue, oldParent, newParent) -> { if (this.prefWidthProperty().isBound()) { this.prefWidthProperty().unbind(); } if (newParent != null && newParent instanceof Region) { this.prefWidthProperty().bind(((Region) newParent).widthProperty()); } }); } /** * Initialize the {@link ExtendedTextField fields} that are mandatory by applying validators to them. */ private void initializeMandatoryFields() { this.templateName.setValidator(isNotEmpty()); this.templateFile.setValidator(isNotEmpty()); this.templateResourcesDirectory.setValidator(isNotEmpty()); this.jsObject.setValidator(isNotEmpty()); this.slidesContainer.setValidator(isNotEmpty()); this.slideIdPrefix.setValidator(isNotEmpty()); this.slidesPresentationDirectory.setValidator(isNotEmpty()); this.slidesTemplateDirectory.setValidator(isNotEmpty()); this.slidesThumbnailDirectory.setValidator(isNotEmpty()); } /** * Build the {@link TitledPane} that contains the UI elements for specifying the global configuration of the * template, as the name, file, resources' directory and JS object. * * @return The properly initialized {@link TitledPane} containing the global configuration elements. */ private TitledPane getTemplateGlobalConfigurationPane() { final FlowPane internalContainer = new FlowPane(5, 5, templateName, templateFile, templateResourcesDirectory, jsObject); final TitledPane templateGlobalConfigurationPane = new TitledPane("Template global configuration", internalContainer); templateGlobalConfigurationPane.setCollapsible(false); return templateGlobalConfigurationPane; } /** * Build the {@link TitledPane} that contains the UI elements for specifying the default template variables. * * @return The properly initialized {@link TitledPane} containing the default template variables elements. */ private TitledPane getTemplateDefaultVariablePane() { final Button addButton = new Button("Add"); addButton.setTooltip(new Tooltip("Add a default template variable")); addButton.setOnAction(event -> this.addDefaultTemplateVariable()); final VBox internalContainer = new VBox(5, defaultVariablesPane, addButton); final TitledPane variablePane = new TitledPane("Template default variables", internalContainer); variablePane.setCollapsible(false); return variablePane; } /** * Build the {@link TitledPane} that contains the UI elements for specifying the global slides' configuration as * the slides' container, slide's ID prefix, presentation directory, template directory and thumbnails directory. * * @return The properly initialized {@link TitledPane} containing the slides' global configuration UI elements. */ private TitledPane getSlidesGlobalConfigurationPane() { final FlowPane internalContainer = new FlowPane(5, 5, slidesContainer, slideIdPrefix, slidesPresentationDirectory, slidesTemplateDirectory, slidesThumbnailDirectory); final TitledPane slidesGlobalConfigurationPane = new TitledPane("Slides global configuration", internalContainer); slidesGlobalConfigurationPane.setCollapsible(false); return slidesGlobalConfigurationPane; } /** * Build the {@link TitledPane} that contains the UI elements for specifying the slides. * * @return The properly initialized {@link TitledPane} containing the slides UI elements. */ private TitledPane getSlidesPane() { final Button addButton = new Button("Add slide"); addButton.setTooltip(new Tooltip("Add a slide")); addButton.setOnAction(event -> this.addSlide()); final VBox internalContainer = new VBox(5, slideDefinitionsPane, addButton); final TitledPane slidesPane = new TitledPane("Slides", internalContainer); slidesPane.setCollapsible(false); return slidesPane; } /** * Get the working path of this control. The working path correspond to the folder on the disk where the template * is built. * * @return The working path of this template. */ public Path getWorkingPath() { return workingPath; } /** * Set the directory in which the template is built. * * @param workingPath The working path of this template. */ public void setWorkingPath(Path workingPath) { this.workingPath = workingPath; } /** * Adds a template variable to the UI. * * @return The UI element added to the editor corresponding to the new template variable. */ private TemplateVariable addDefaultTemplateVariable() { final TemplateVariable variable = new TemplateVariable(); variable.setOnDelete(event -> { final ButtonType answer = DialogHelper.showConfirmationAlert("Delete variable", "Are you sure you want to delete this variable?"); if (answer == ButtonType.YES) { defaultVariables.remove(variable); defaultVariablesPane.getChildren().remove(variable); } }); defaultVariables.add(variable); this.defaultVariablesPane.getChildren().add(variable); return variable; } /** * Add the UI element corresponding to a slide to the current UI. * * @return The element used to define a new slide in the template. */ private SlideDefinition addSlide() { final SlideDefinition slideDefinition = new SlideDefinition(); this.slideDefinitions.add(slideDefinition); this.slideDefinitionsPane.getChildren().add(slideDefinition); slideDefinition.setOnDelete(event -> { final ButtonType answer = DialogHelper.showConfirmationAlert("Delete slide", "Are you sure you want to delete this slide?"); if (answer == ButtonType.YES) { this.slideDefinitions.remove(slideDefinition); this.slideDefinitionsPane.getChildren().remove(slideDefinition); } }); return slideDefinition; } /** * Return the configuration given inside the UI as a JSON structure. This structure is the one defined for the * {@code template-config.json} file and can be put inside this file. * * @return The JSON structure representing this template configuration. */ public String getAsString() { final TemplateConfiguration configuration = new TemplateConfiguration(); configuration.setName(this.templateName.getText()); configuration.setFile(new File(this.workingPath.toFile(), this.templateFile.getText())); configuration.setJsObject(this.jsObject.getText()); configuration.setResourcesDirectory(new File(this.workingPath.toFile(), this.templateResourcesDirectory.getText())); configuration.setDefaultVariables( this.defaultVariables.stream() .filter(TemplateVariable::isValid) .map(variable -> new Pair<>(variable.getName(), variable.getValue())) .collect(Collectors.toSet()) ); configuration.setSlidesContainer(this.slidesContainer.getText()); configuration.setSlideIdPrefix(this.slideIdPrefix.getText()); configuration.setSlidesTemplateDirectory(new File(this.workingPath.toFile(), this.slidesTemplateDirectory.getText())); configuration.setSlidesPresentationDirectory(new File(this.workingPath.toFile(), this.slidesPresentationDirectory.getText())); configuration.setSlidesThumbnailDirectory(new File(this.workingPath.toFile(), this.slidesThumbnailDirectory.getText())); configuration.setSlideTemplates(new ArrayList<>()); configuration.setSlideTemplates( this.slideDefinitions.stream() .filter(SlideDefinition::isValid) .map(definition -> { final SlideTemplate template = new SlideTemplate( definition.getSlideId(), definition.getName(), new File(definition.getFile())); if (!definition.getSlideElements().isEmpty()) { template.setElements( definition.getSlideElements().stream() .filter(SlideElementDefinition::isValid) .map(elementDefinition -> { final SlideElementTemplate elementTemplate = new SlideElementTemplate(); elementTemplate.setId(elementDefinition.getElementId()); elementTemplate.setHtmlId(elementDefinition.getHtmlId()); elementTemplate.setDefaultContent(elementDefinition.getDefaultContent()); return elementTemplate; }) .toArray(SlideElementTemplate[]::new)); } return template; }) .collect(Collectors.toList())); final TemplateEngine engine = new TemplateEngine(); engine.setConfiguration(configuration); engine.setWorkingDirectory(this.workingPath.toFile()); final StringWriter writer = new StringWriter(); try { engine.writeConfiguration(writer); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Can not save the template configuration file", e); } return writer.toString(); } /** * Populate the UI according the given configuration file. * * @param file The configuration file to open. */ public void fillWithFile(final File file) { final TemplateEngine engine = new TemplateEngine(); engine.setWorkingDirectory(this.workingPath.toFile()); try { final TemplateConfiguration configuration = engine.readConfiguration(file); this.templateName.setText(configuration.getName()); this.templateFile.setText(this.getPathRelativeToWorkingPath(configuration.getFile())); this.jsObject.setText(configuration.getJsObject()); this.templateResourcesDirectory.setText(this.getPathRelativeToWorkingPath(configuration.getResourcesDirectory())); configuration.getDefaultVariables() .forEach(variable -> { final TemplateVariable templateVariable = this.addDefaultTemplateVariable(); templateVariable.setName(variable.getKey()); templateVariable.setValue(variable.getValue()); }); this.slidesContainer.setText(configuration.getSlidesContainer()); this.slideIdPrefix.setText(configuration.getSlideIdPrefix()); this.slidesTemplateDirectory.setText(this.getPathRelativeToWorkingPath(configuration.getSlidesTemplateDirectory())); this.slidesPresentationDirectory.setText(this.getPathRelativeToWorkingPath(configuration.getSlidesPresentationDirectory())); this.slidesThumbnailDirectory.setText(this.getPathRelativeToWorkingPath(configuration.getSlidesThumbnailDirectory())); configuration.getSlideTemplates() .forEach(template -> { final SlideDefinition definition = this.addSlide(); definition.setSlideId(template.getId()); definition.setName(template.getName()); definition.setFile(template.getFile().getName()); if (template.getElements() != null) { for (SlideElementTemplate elementTemplate : template.getElements()) { final SlideElementDefinition slideElementDefinition = definition.addSlideElement(); slideElementDefinition.setElementId(elementTemplate.getId()); slideElementDefinition.setHtmlId(elementTemplate.getHtmlId()); slideElementDefinition.setDefaultContent(elementTemplate.getDefaultContent()); } } }); } catch (IOException | IllegalAccessException e) { LOGGER.log(Level.SEVERE, "Can not read the template configuration file", e); } } /** * Indicates if the configuration defined within the UI is considered valid. * * @return {@code true} if the configuration is valid, {@code false} otherwise. */ public boolean isContentValid() { boolean globallyValid = true; if (!this.defaultVariables.isEmpty()) { boolean isValid; for (TemplateVariable variable : this.defaultVariables) { isValid = variable.isValid(); if (globallyValid && !isValid) { globallyValid = false; } } } if (!this.slideDefinitions.isEmpty()) { boolean isValid; for (SlideDefinition slide : this.slideDefinitions) { isValid = slide.isValid(); if (globallyValid && !isValid) { globallyValid = false; } } } boolean isValid = this.templateName.isValid(); if (globallyValid && !isValid) { globallyValid = false; } isValid = this.templateFile.isValid(); if (globallyValid && !isValid) { globallyValid = false; } isValid = this.templateResourcesDirectory.isValid(); if (globallyValid && !isValid) { globallyValid = false; } isValid = this.jsObject.isValid(); if (globallyValid && !isValid) { globallyValid = false; } isValid = this.slidesContainer.isValid(); if (globallyValid && !isValid) { globallyValid = false; } isValid = this.slideIdPrefix.isValid(); if (globallyValid && !isValid) { globallyValid = false; } isValid = this.slidesPresentationDirectory.isValid(); if (globallyValid && !isValid) { globallyValid = false; } isValid = this.slidesTemplateDirectory.isValid(); if (globallyValid && !isValid) { globallyValid = false; } isValid = this.slidesThumbnailDirectory.isValid(); if (globallyValid && !isValid) { globallyValid = false; } return globallyValid; } /** * Get the relative path of the given file from the current working path of the editor. * * @param file The file to determine the relative path. * @return The relative path of the given file from the current working path. */ private String getPathRelativeToWorkingPath(final File file) { return this.workingPath.relativize(file.toPath()).toString(); } }